diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 834c2cda..00000000 --- a/.coveragerc +++ /dev/null @@ -1,10 +0,0 @@ -[run] -branch = True -data_file = .coverage -source=forum -omit = - forum/settings/test.py - */migrations/* - *admin.py - */static/* - */templates/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a4838e9..2e509cd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,43 +1,42 @@ -# This workflow can be troubleshooted with act https://github.com/nektos/act -# For example: act --job run_tests --matrix toxenv:e2e -name: Run tests +name: CI on: push: branches: [master] pull_request: - branches: - - "**" - workflow_dispatch: - -defaults: - run: - shell: bash # opts into error checking + # Allow this workflow to be called from other workflows + workflow_call: jobs: run_tests: - name: tests - runs-on: ${{ matrix.os }} + name: ${{ matrix.toxenv }} + runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - os: [ubuntu-latest] - python-version: ['3.12'] - toxenv: [py, quality, django42, django52, e2e, package] + python-version: ["3.12"] + toxenv: [quality, docs, mypy, django42, django52, e2e, package] steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: setup python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - python-version: ${{ matrix.python-version }} + enable-cache: true + python-version: "${{ matrix.python-version }}" - - name: Install pip - run: pip install -r requirements/pip.txt + - name: Install CI dependencies + run: uv sync --group ci - - name: Install Dependencies - run: pip install -r requirements/ci.txt + - name: Run tox + run: uv run tox -e ${{ matrix.toxenv }} - - name: Run Tests - env: - TOXENV: ${{ matrix.toxenv }} - run: tox + - name: Upload coverage to Codecov + if: matrix.toxenv == 'django52' + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6.0.2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: unittests + fail_ci_if_error: true diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml deleted file mode 100644 index 542ecfca..00000000 --- a/.github/workflows/pypi-publish.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Publish package to PyPi - -on: - release: - types: [published] - -jobs: - push: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 - - name: setup python - uses: actions/setup-python@e9aba2c848f5ebd159c070c61ea2c4e2b122355e # v2.3.4 - with: - python-version: 3.12 - - - name: Install Dependencies - run: pip install setuptools wheel - - - name: Install pip - run: pip install -r requirements/pip.txt - - - name: Build package - run: python setup.py sdist bdist_wheel - - - name: Publish to PyPi - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_UPLOAD_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..96ff1db9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,84 @@ +name: Release + +on: + push: + branches: [master] + +jobs: + run_ci: + uses: ./.github/workflows/ci.yml + secrets: inherit + permissions: + contents: read + + release: + needs: run_ci + runs-on: ubuntu-latest + if: github.ref_name == 'master' + concurrency: + group: ${{ github.workflow }}-release-${{ github.ref_name }} + cancel-in-progress: false + + permissions: + contents: write + + steps: + # Checkout the repository at the branch that triggered the workflow. + # Python Semantic Release will automatically convert shallow clones to + # full clones if needed. We reset to workflow sha to prevent accidentally + # releasing un-evaluated changes if branch was updated during workflow. + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.ref_name }} + + - name: Force branch to workflow sha + run: git reset --hard ${{ github.sha }} + + - name: Run Semantic Release + id: release + uses: python-semantic-release/python-semantic-release@350c48fcb3ffcdfd2e0a235206bc2ecea6b69df0 # v10.5.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + git_committer_name: "github-actions" + git_committer_email: "actions@users.noreply.github.com" + changelog: "false" + + - name: Upload to GitHub Release Assets + uses: python-semantic-release/publish-action@310a9983a0ae878b29f3aac778d7c77c1db27378 # v10.5.3 + if: steps.release.outputs.released == 'true' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ steps.release.outputs.tag }} + + - name: Upload distribution artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: steps.release.outputs.released == 'true' + with: + name: distribution-artifacts + path: dist + if-no-files-found: error + + outputs: + released: ${{ steps.release.outputs.released || 'false' }} + version: ${{ steps.release.outputs.version }} + + publish_to_pypi: + runs-on: ubuntu-latest + needs: release + if: github.ref_name == 'master' && needs.release.outputs.released == 'true' + + permissions: + contents: read + + steps: + - name: Download build artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: distribution-artifacts + path: dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdba # v1.12.4 + with: + password: ${{ secrets.PYPI_UPLOAD_TOKEN }} diff --git a/.github/workflows/upgrade-python-requirements.yml b/.github/workflows/upgrade-python-requirements.yml index 293fcc90..bf567d65 100644 --- a/.github/workflows/upgrade-python-requirements.yml +++ b/.github/workflows/upgrade-python-requirements.yml @@ -8,13 +8,13 @@ on: branch: description: "Target branch against which to create requirements PR" required: true - default: 'main' + default: 'master' jobs: call-upgrade-python-requirements-workflow: uses: openedx/.github/.github/workflows/upgrade-python-requirements.yml@master with: - branch: ${{ github.event.inputs.branch || 'main' }} + branch: ${{ github.event.inputs.branch || 'master' }} # optional parameters below; fill in if you'd like github or email notifications # user_reviewers: "" # team_reviewers: "" diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index ae320716..00000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,68 +0,0 @@ -Change Log -########## - -.. - All enhancements and patches to forum will be documented - in this file. It adheres to the structure of https://keepachangelog.com/ , - but in reStructuredText instead of Markdown (for ease of incorporation into - Sphinx documentation and the PyPI description). - - This project adheres to Semantic Versioning (https://semver.org/). - -.. There should always be an "Unreleased" section for changes pending release. - -Unreleased -********** - -* Add support for Typesense as the search backend. -* Fix: Set default value for ``pinned`` field on ``CommentThread`` to ``False`` - to prevent NULL sort bug. - -[0.4.0] – 2026-03-12 -********************* - -Breaking Changes ----------------- - -* Drop Python 3.11 support; Python 3.12 is now required. -* Upgrade typesense-python from 1.x to 2.0. This release requires - **Typesense Server >= v30.0** (previously >= v28.0). See the - `typesense-python compatibility table - `_ - for details. If you are running an older Typesense server you must - upgrade it before deploying this version of openedx-forum. - -0.3.4 – 2025-08-13 -****************** - -Added ------ - -* CI check to validate the Python package. - -Fixed ------ - -* CHANGELOG header formatting. - -0.3.3 – 2025-08-12 -****************** - -Fixed ------ - -* Do not raise runtime errors if an incorrect course ID is provided when - checking if the MySQL backend is enabled. - - -0.3.0 – 2025-04-23 -****************** - -* Add support for django 5.2 - -* - -0.1.0 – 2024-11-12 -****************** - -* First release on PyPI. diff --git a/MANIFEST.in b/MANIFEST.in index 3461197b..bb1960c1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,3 @@ -include CHANGELOG.rst include LICENSE.txt include README.rst -include requirements/base.in -include requirements/constraints.txt recursive-include forum *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg diff --git a/Makefile b/Makefile index ed97a608..09c84112 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,17 @@ -.PHONY: clean clean_tox compile_translations coverage docs dummy_translations \ - extract_translations fake_translations help pull_translations push_translations \ - quality requirements selfcheck test test-all upgrade install_transifex_client - -SRC_FILES_PROD = forum tests test_utils manage.py -SRC_FILES = ${SRC_FILES_PROD} setup.py +.PHONY: help requirements upgrade lint mypy format test docs clean \ + extract_translations compile_translations dummy_translations \ + pull_translations push_translations validate_translations \ + build_dummy_translations detect_changed_source_translations \ + test-pii test-e2e e2e-start-services e2e-stop-services selfcheck coverage \ + install_transifex_client .DEFAULT_GOAL := help # For opening files in a browser. Use like: $(BROWSER)relative/path/to/file.html BROWSER := python -m webbrowser file://$(CURDIR)/ +SRC_FILES_PROD = forum tests test_utils manage.py + help: ## display this help message @echo "Please use \`make ' where is one of" @awk -F ':.*?## ' '/^[a-zA-Z]/ && NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort @@ -19,88 +21,52 @@ clean: ## remove generated byte code, coverage reports, and build artifacts find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + - coverage erase rm -fr build/ rm -fr dist/ rm -fr *.egg-info + rm -fr .pytest_cache/ + rm -fr .ruff_cache/ -clean_tox: ## clear tox requirements cache - rm -fr .tox - -coverage: clean ## generate and view HTML coverage report - pytest --cov-report html - $(BROWSER)htmlcov/index.html - -docs: ## generate Sphinx HTML documentation, including API docs - tox -e docs - $(BROWSER)docs/_build/html/index.html - -# Define PIP_COMPILE_OPTS="-v" to get more information during make compile-requirements. -compile-requirements: export CUSTOM_COMPILE_COMMAND=make upgrade -compile-requirements: ## Re-compile *.in requirements to *.txt - pip install --quiet -r requirements/pip-tools.txt - # Make sure to compile files after any other files they include! - pip-compile $(COMPILE_OPTS) --allow-unsafe requirements/pip.in - pip-compile $(COMPILE_OPTS) requirements/pip-tools.in - pip install --quiet -r requirements/pip.txt - pip install --quiet -r requirements/pip-tools.txt - pip-compile $(COMPILE_OPTS) requirements/base.in - pip-compile $(COMPILE_OPTS) requirements/test.in - pip-compile $(COMPILE_OPTS) requirements/doc.in - pip-compile $(COMPILE_OPTS) requirements/quality.in - pip-compile $(COMPILE_OPTS) requirements/ci.in - pip-compile $(COMPILE_OPTS) requirements/dev.in - # Let tox control the Django version for tests - sed '/^[dD]jango==/d' requirements/test.txt > requirements/test.tmp - mv requirements/test.tmp requirements/test.txt - -format: - black ${SRC_FILES} - -upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in - $(MAKE) compile-requirements COMPILE_OPTS="--upgrade" - -piptools: ## install pinned version of pip-compile and pip-sync - pip install -r requirements/pip.txt - pip install -r requirements/pip-tools.txt - -requirements: clean_tox piptools ## install development environment requirements - pip-sync -q requirements/dev.txt requirements/private.* +requirements: ## Sync dev dependencies + uv sync --group dev + uv tool install tox --with tox-uv -test-all: selfcheck clean test test-quality test-pii test-e2e ## run all tests +upgrade: ## Upgrade python dependencies and regenerate uv constraints + uv run --with edx-lint edx_lint write_uv_constraints pyproject.toml + uv lock --upgrade -test: ## run unit tests - pytest +lint: ## Run linting checks + uv run tox -e quality -test-quality: test-lint test-codestyle test-mypy test-format ## run static coverage tests +mypy: ## Run mypy type checks + uv run tox -e mypy -test-lint: ## run pylint - pylint ${SRC_FILES} +format: ## Auto-fix formatting issues + uv run ruff check --fix . + uv run ruff format . -test-codestyle: ## run pycodestyle, pydocstyle - pycodestyle ${SRC_FILES} - pydocstyle ${SRC_FILES} +test: ## Run unit tests + uv run pytest -test-isort: ## run isort checks - isort --check-only --diff ${SRC_FILES} - -test-mypy: ## run type tests - mypy ${SRC_FILES_PROD} +docs: ## generate Sphinx HTML documentation, including API docs + uv run tox -e docs + $(BROWSER)docs/_build/html/index.html -test-format: ## Run code formatting tests - black --check ${SRC_FILES} +coverage: clean ## generate and view HTML coverage report + uv run pytest --cov-report html + $(BROWSER)htmlcov/index.html -test-pii: export DJANGO_SETTINGS_MODULE=forum.settings.test -test-pii: ## # check for PII annotations on all Django models - code_annotations django_find_annotations --config_file .pii_annotations.yml --lint --report --coverage +test-pii: ## check for PII annotations on all Django models + DJANGO_SETTINGS_MODULE=forum.settings.test \ + uv run code_annotations django_find_annotations --config_file .pii_annotations.yml --lint --report --coverage -test-e2e: e2e-stop-services e2e-start-services # run end-to-end tests - pytest tests/e2e +test-e2e: e2e-stop-services e2e-start-services ## run end-to-end tests + uv run pytest tests/e2e -e2e-start-services: # Start dependency containers necessary for e2e tests +e2e-start-services: ## Start dependency containers necessary for e2e tests docker compose -f tests/e2e/docker-compose.yml --project-name forum_e2e up -d -e2e-stop-services: # Stop dependency containers necessary for e2e tests +e2e-stop-services: ## Stop dependency containers necessary for e2e tests docker compose -f tests/e2e/docker-compose.yml --project-name forum_e2e down selfcheck: ## check that the Makefile is well-formed @@ -142,8 +108,8 @@ build_dummy_translations: extract_translations dummy_translations compile_transl validate_translations: build_dummy_translations detect_changed_source_translations ## validate translations install_transifex_client: ## Install the Transifex client - # Instaling client will skip CHANGELOG and LICENSE files from git changes + # Installing client will skip CHANGELOG and LICENSE files from git changes # so remind the user to commit the change first before installing client. git diff -s --exit-code HEAD || { echo "Please commit changes first."; exit 1; } curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash - git checkout -- LICENSE README.md ## overwritten by Transifex installer + git checkout -- LICENSE README.rst diff --git a/docs/changelog.rst b/docs/changelog.rst index 565b0521..b6a44016 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1 +1,7 @@ -.. include:: ../CHANGELOG.rst +Changelog +========= + +This project uses `Python Semantic Release `_ +to automatically generate a changelog from commit messages. + +See the `GitHub Releases page `_ for the full changelog. diff --git a/docs/conf.py b/docs/conf.py index 5acf7f36..80420280 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,37 +10,22 @@ All configuration values have a default; values that are commented out serve to show the default. """ + import os -import re import sys from datetime import datetime from subprocess import check_call from django import setup as django_setup - -def get_version(*file_paths): - """ - Extract the version string from the file. - - Input: - - file_paths: relative path fragments to file with - version string - """ - filename = os.path.join(os.path.dirname(__file__), *file_paths) - version_file = open(filename, encoding="utf8").read() - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) - if version_match: - return version_match.group(1) - raise RuntimeError('Unable to find version string.') - +from forum import __version__ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(REPO_ROOT) -VERSION = get_version('../forum', '__init__.py') +VERSION = __version__ # Configure Django for autodoc usage -os.environ['DJANGO_SETTINGS_MODULE'] = 'forum.settings.test' +os.environ["DJANGO_SETTINGS_MODULE"] = "forum.settings.test" django_setup() # If extensions (or modules to document with autodoc) are in another directory, @@ -61,46 +46,47 @@ def get_version(*file_paths): # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.ifconfig', - 'sphinx.ext.napoleon' + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.ifconfig", + "sphinx.ext.napoleon", ] # A list of warning types to suppress arbitrary warning messages. suppress_warnings = [ - 'image.nonlocal_uri', + "image.nonlocal_uri", + "ref.python", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # # source_encoding = 'utf-8-sig' # The top level toctree document. -top_level_doc = 'index' +top_level_doc = "index" # General information about the project. -project = 'forum' -copyright = f'{datetime.now().year}, Axim Collaborative, Inc.' # pylint: disable=redefined-builtin -author = 'Axim Collaborative, Inc.' -project_title = 'forum' +project = "forum" +copyright = f"{datetime.now().year}, Axim Collaborative, Inc." # pylint: disable=redefined-builtin +author = "Axim Collaborative, Inc." +project_title = "forum" documentation_title = f"{project_title}" # Set display_github to False if you don't want "edit on Github" button html_context = { "display_github": True, # Integrate GitHub "github_user": "edx", # Username - "github_repo": 'forum', # Repo name + "github_repo": "forum", # Repo name "github_version": "main", # Version "conf_py_path": "/docs/", # Path in the checkout to the docs root } @@ -119,7 +105,7 @@ def get_version(*file_paths): # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = 'en' +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: @@ -134,12 +120,12 @@ def get_version(*file_paths): # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [ - '_build', - 'Thumbs.db', - '.DS_Store', + "_build", + "Thumbs.db", + ".DS_Store", # This file is intended as a guide for developers browsing the source tree, # not to be rendered into the output docs. - 'decisions/README.rst', + "decisions/README.rst", ] # The reST default role (used for this markup: `text`) to use for all @@ -162,7 +148,7 @@ def get_version(*file_paths): # 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 = [] @@ -178,7 +164,7 @@ def get_version(*file_paths): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_book_theme' +html_theme = "sphinx_book_theme" # 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 @@ -186,7 +172,7 @@ def get_version(*file_paths): # html_theme_options = { "repository_url": "https://github.com/openedx/forum", - "repository_branch": 'main', + "repository_branch": "main", "path_to_docs": "docs/", "home_page_in_toc": True, "use_repository_button": True, @@ -213,7 +199,7 @@ def get_version(*file_paths): rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/" >Creative Commons Attribution-ShareAlike 4.0 International License. - """ + """, } # Add any paths that contain custom themes here, relative to this directory. @@ -243,7 +229,7 @@ def get_version(*file_paths): # 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"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied @@ -323,7 +309,7 @@ def get_version(*file_paths): # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = f'{project}doc' +htmlhelp_basename = f"{project}doc" # -- Options for LaTeX output --------------------------------------------- @@ -331,15 +317,12 @@ def get_version(*file_paths): # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -348,10 +331,9 @@ def get_version(*file_paths): # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). -latex_target = f'{project}.tex' +latex_target = f"{project}.tex" latex_documents = [ - (top_level_doc, latex_target, documentation_title, - author, 'manual'), + (top_level_doc, latex_target, documentation_title, author, "manual"), ] # The name of an image file (relative to this directory) to place at the top of @@ -391,10 +373,7 @@ def get_version(*file_paths): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (top_level_doc, project_title, documentation_title, - [author], 1) -] +man_pages = [(top_level_doc, project_title, documentation_title, [author], 1)] # If true, show URL addresses after external links. # @@ -407,9 +386,7 @@ def get_version(*file_paths): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (top_level_doc, project_title, documentation_title, - author, project_title, 'openedx forum app', - 'Miscellaneous'), + (top_level_doc, project_title, documentation_title, author, project_title, "openedx forum app", "Miscellaneous"), ] # Documents to append as an appendix to all manuals. @@ -483,7 +460,7 @@ def get_version(*file_paths): # epub_post_files = [] # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # The depth of the table of contents in toc.ncx. # @@ -516,9 +493,9 @@ def get_version(*file_paths): # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'python': ('https://docs.python.org/3.8', None), - 'django': ('https://docs.djangoproject.com/en/3.2/', 'https://docs.djangoproject.com/en/3.2/_objects/'), - 'model_utils': ('https://django-model-utils.readthedocs.io/en/latest/', None), + "python": ("https://docs.python.org/3.12", None), + "django": ("https://docs.djangoproject.com/en/4.2/", "https://docs.djangoproject.com/en/4.2/_objects/"), + "model_utils": ("https://django-model-utils.readthedocs.io/en/latest/", None), } @@ -530,17 +507,18 @@ def on_init(app): # pylint: disable=unused-argument avoid checking in the generated reStructuredText files. """ docs_path = os.path.abspath(os.path.dirname(__file__)) - root_path = os.path.abspath(os.path.join(docs_path, '..')) - apidoc_path = 'sphinx-apidoc' - if hasattr(sys, 'real_prefix'): # Check to see if we are in a virtualenv + root_path = os.path.abspath(os.path.join(docs_path, "..")) + apidoc_path = "sphinx-apidoc" + if hasattr(sys, "real_prefix"): # Check to see if we are in a virtualenv # If we are, assemble the path manually - bin_path = os.path.abspath(os.path.join(sys.prefix, 'bin')) + bin_path = os.path.abspath(os.path.join(sys.prefix, "bin")) apidoc_path = os.path.join(bin_path, apidoc_path) - check_call([apidoc_path, '-o', docs_path, os.path.join(root_path, 'forum'), - os.path.join(root_path, 'forum/migrations')]) + check_call( + [apidoc_path, "-o", docs_path, os.path.join(root_path, "forum"), os.path.join(root_path, "forum/migrations")] + ) def setup(app): """Sphinx extension: run sphinx-apidoc.""" - event = 'builder-inited' + event = "builder-inited" app.connect(event, on_init) diff --git a/docs/references/v2_python_native_apis_responses.rst b/docs/references/v2_python_native_apis_responses.rst index 416cf099..c726ffa2 100644 --- a/docs/references/v2_python_native_apis_responses.rst +++ b/docs/references/v2_python_native_apis_responses.rst @@ -312,8 +312,8 @@ The `pin_thread` API pins a discussion thread at the top for users to easily acc "id": "66df1595a3a68c001d742c05", "body": "

test question 

", "course_id": "course-v1:Arbisoft+SE002+2024_S2", - "anonymous": False, - "anonymous_to_peers": False, + "anonymous": false, + "anonymous_to_peers": false, "created_at": "2024-09-09T15:34:45Z", "updated_at": "2024-09-18T16:27:05Z", "at_position_list": [], @@ -328,16 +328,16 @@ The `pin_thread` API pins a discussion thread at the top for users to easily acc }, "abuse_flaggers": [], "edit_history": [], - "closed": False, + "closed": false, "type": "thread", "thread_type": "question", "title": "test question", "context": "course", "last_activity_at": "2024-09-18T09:19:38Z", - "closed_by": None, + "closed_by": null, "tags": [], - "group_id": None, - "pinned": True + "group_id": null, + "pinned": true } Unpin Thread(unpin_thread) API @@ -353,8 +353,8 @@ The `unpin_thread` API unpins a previously pinned thread, removing its elevated "id": "66df1595a3a68c001d742c05", "body": "

test question 

", "course_id": "course-v1:Arbisoft+SE002+2024_S2", - "anonymous": False, - "anonymous_to_peers": False, + "anonymous": false, + "anonymous_to_peers": false, "created_at": "2024-09-09T15:34:45Z", "updated_at": "2024-09-18T16:27:49Z", "at_position_list": [], @@ -369,14 +369,14 @@ The `unpin_thread` API unpins a previously pinned thread, removing its elevated }, "abuse_flaggers": [], "edit_history": [], - "closed": False, + "closed": false, "type": "thread", "thread_type": "question", "title": "test question", "context": "course", "last_activity_at": "2024-09-18T09:19:38Z", - "closed_by": None, + "closed_by": null, "tags": [], - "group_id": None, - "pinned": False + "group_id": null, + "pinned": false } diff --git a/forum/__init__.py b/forum/__init__.py index 15fb549a..a2cf5a7b 100644 --- a/forum/__init__.py +++ b/forum/__init__.py @@ -2,4 +2,9 @@ Openedx forum app. """ -__version__ = "0.4.3" +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("openedx-forum") +except PackageNotFoundError: # pragma: no cover + __version__ = "0.0.0" diff --git a/forum/admin.py b/forum/admin.py index 3d624a8e..8a7f14fe 100644 --- a/forum/admin.py +++ b/forum/admin.py @@ -1,19 +1,20 @@ """Admin module for forum.""" from django.contrib import admin + from forum.models import ( - ForumUser, - CourseStat, - CommentThread, + AbuseFlagger, Comment, + CommentThread, + CourseStat, EditHistory, - AbuseFlagger, + ForumUser, HistoricalAbuseFlagger, - ReadState, LastReadTime, - UserVote, - Subscription, MongoContent, + ReadState, + Subscription, + UserVote, ) diff --git a/forum/api/comments.py b/forum/api/comments.py index c95a1d4b..ed7ce879 100644 --- a/forum/api/comments.py +++ b/forum/api/comments.py @@ -4,7 +4,7 @@ import logging import math -from typing import Any, Optional +from typing import Any from django.core.exceptions import ObjectDoesNotExist from rest_framework.serializers import ValidationError @@ -20,7 +20,7 @@ def prepare_comment_api_response( comment: dict[str, Any], backend: Any, - exclude_fields: Optional[list[str]] = None, + exclude_fields: list[str] | None = None, ) -> dict[str, Any]: """ Return serialized validated data. @@ -55,9 +55,7 @@ def prepare_comment_api_response( return serializer.data -def get_parent_comment( - comment_id: str, course_id: Optional[str] = None -) -> dict[str, Any]: +def get_parent_comment(comment_id: str, course_id: str | None = None) -> dict[str, Any]: """ Get a parent comment. @@ -73,9 +71,7 @@ def get_parent_comment( comment = backend.validate_object("Comment", comment_id) except ObjectDoesNotExist as exc: log.error("Forumv2RequestError for get parent comment request.") - raise ForumV2RequestError( - f"Comment does not exists with Id: {comment_id}" - ) from exc + raise ForumV2RequestError(f"Comment does not exists with Id: {comment_id}") from exc return prepare_comment_api_response( comment, backend, @@ -109,9 +105,7 @@ def create_child_comment( parent_comment = backend.validate_object("Comment", parent_comment_id) except ObjectDoesNotExist as exc: log.error("Forumv2RequestError for create child comment request.") - raise ForumV2RequestError( - f"Comment does not exists with Id: {parent_comment_id}" - ) from exc + raise ForumV2RequestError(f"Comment does not exists with Id: {parent_comment_id}") from exc comment_id = backend.create_comment( { @@ -147,16 +141,16 @@ def create_child_comment( def update_comment( comment_id: str, - body: Optional[str] = None, - course_id: Optional[str] = None, - user_id: Optional[str] = None, - anonymous: Optional[bool] = None, - anonymous_to_peers: Optional[bool] = None, - endorsed: Optional[bool] = None, - closed: Optional[bool] = None, - editing_user_id: Optional[str] = None, - edit_reason_code: Optional[str] = None, - endorsement_user_id: Optional[str] = None, + body: str | None = None, + course_id: str | None = None, + user_id: str | None = None, + anonymous: bool | None = None, + anonymous_to_peers: bool | None = None, + endorsed: bool | None = None, + closed: bool | None = None, + editing_user_id: str | None = None, + edit_reason_code: str | None = None, + endorsement_user_id: str | None = None, ) -> dict[str, Any]: """ Update an existing child/parent comment. @@ -181,9 +175,7 @@ def update_comment( backend.validate_object("Comment", comment_id) except ObjectDoesNotExist as exc: log.error("Forumv2RequestError for update comment request.") - raise ForumV2RequestError( - f"Comment does not exists with Id: {comment_id}" - ) from exc + raise ForumV2RequestError(f"Comment does not exists with Id: {comment_id}") from exc updated_comment = backend.update_comment_and_get_updated_comment( comment_id, @@ -204,15 +196,13 @@ def update_comment( return prepare_comment_api_response( updated_comment, backend, - exclude_fields=( - ["endorsement", "sk"] if updated_comment.get("parent_id") else ["sk"] - ), + exclude_fields=(["endorsement", "sk"] if updated_comment.get("parent_id") else ["sk"]), ) except ValidationError as error: raise error -def delete_comment(comment_id: str, course_id: Optional[str] = None) -> dict[str, Any]: +def delete_comment(comment_id: str, course_id: str | None = None) -> dict[str, Any]: """ Delete a comment. @@ -228,9 +218,7 @@ def delete_comment(comment_id: str, course_id: Optional[str] = None) -> dict[str comment = backend.validate_object("Comment", comment_id) except ObjectDoesNotExist as exc: log.error("Forumv2RequestError for delete comment request.") - raise ForumV2RequestError( - f"Comment does not exists with Id: {comment_id}" - ) from exc + raise ForumV2RequestError(f"Comment does not exists with Id: {comment_id}") from exc data = prepare_comment_api_response( comment, backend, @@ -273,9 +261,7 @@ def create_parent_comment( backend.validate_object("CommentThread", thread_id) except ObjectDoesNotExist as exc: log.error("Forumv2RequestError for create parent comment request.") - raise ForumV2RequestError( - f"Thread does not exists with Id: {thread_id}" - ) from exc + raise ForumV2RequestError(f"Thread does not exists with Id: {thread_id}") from exc comment_id = backend.create_comment( { @@ -315,7 +301,7 @@ def get_course_id_by_comment(comment_id: str) -> str | None: def get_user_comments( user_id: str, course_id: str, - flagged: Optional[bool] = False, + flagged: bool | None = False, page: int = 1, per_page: int = 10, ) -> dict[str, Any]: diff --git a/forum/api/flags.py b/forum/api/flags.py index cd7c71e9..d1b53860 100644 --- a/forum/api/flags.py +++ b/forum/api/flags.py @@ -2,7 +2,7 @@ This module contains the functions to update the flag status of a comment. """ -from typing import Any, Optional +from typing import Any from forum.backend import get_backend from forum.serializers.comment import CommentSerializer @@ -13,9 +13,9 @@ def update_comment_flag( comment_id: str, action: str, - user_id: Optional[str] = None, - update_all: Optional[bool] = False, - course_id: Optional[str] = None, + user_id: str | None = None, + update_all: bool | None = False, + course_id: str | None = None, ) -> dict[str, Any]: """ Update the flag status of a comment. @@ -38,18 +38,12 @@ def update_comment_flag( raise ForumV2RequestError("User / Comment doesn't exist") if action == "flag": - updated_comment = backend.flag_as_abuse( - user_id, comment_id, entity_type="Comment" - ) + updated_comment = backend.flag_as_abuse(user_id, comment_id, entity_type="Comment") elif action == "unflag": if update_all: - updated_comment = backend.un_flag_all_as_abuse( - comment_id, entity_type="Comment" - ) + updated_comment = backend.un_flag_all_as_abuse(comment_id, entity_type="Comment") else: - updated_comment = backend.un_flag_as_abuse( - user_id, comment_id, entity_type="Comment" - ) + updated_comment = backend.un_flag_as_abuse(user_id, comment_id, entity_type="Comment") else: raise ForumV2RequestError("Invalid action") @@ -70,9 +64,9 @@ def update_comment_flag( def update_thread_flag( thread_id: str, action: str, - user_id: Optional[str] = None, - update_all: Optional[bool] = False, - course_id: Optional[str] = None, + user_id: str | None = None, + update_all: bool | None = False, + course_id: str | None = None, ) -> dict[str, Any]: """ Update the flag status of a thread. @@ -92,18 +86,12 @@ def update_thread_flag( raise ForumV2RequestError("User / Thread doesn't exist") if action == "flag": - updated_thread = backend.flag_as_abuse( - user_id, thread_id, entity_type="CommentThread" - ) + updated_thread = backend.flag_as_abuse(user_id, thread_id, entity_type="CommentThread") elif action == "unflag": if update_all: - updated_thread = backend.un_flag_all_as_abuse( - thread_id, entity_type="CommentThread" - ) + updated_thread = backend.un_flag_all_as_abuse(thread_id, entity_type="CommentThread") else: - updated_thread = backend.un_flag_as_abuse( - user_id, thread_id, entity_type="CommentThread" - ) + updated_thread = backend.un_flag_as_abuse(user_id, thread_id, entity_type="CommentThread") else: raise ForumV2RequestError("Invalid action") diff --git a/forum/api/pins.py b/forum/api/pins.py index 33a027aa..7ba02fc7 100644 --- a/forum/api/pins.py +++ b/forum/api/pins.py @@ -3,7 +3,7 @@ """ import logging -from typing import Any, Optional +from typing import Any from forum.backend import get_backend from forum.serializers.thread import ThreadSerializer @@ -16,7 +16,7 @@ def pin_unpin_thread( user_id: str, thread_id: str, action: str, - course_id: Optional[str] = None, + course_id: str | None = None, ) -> dict[str, Any]: """ Helper method to Pin or Unpin a thread. @@ -39,9 +39,7 @@ def pin_unpin_thread( return thread_data -def pin_thread( - user_id: str, thread_id: str, course_id: Optional[str] = None -) -> dict[str, Any]: +def pin_thread(user_id: str, thread_id: str, course_id: str | None = None) -> dict[str, Any]: """ Pin a thread. Parameters: @@ -54,9 +52,7 @@ def pin_thread( return pin_unpin_thread(user_id, thread_id, "pin", course_id) -def unpin_thread( - user_id: str, thread_id: str, course_id: Optional[str] = None -) -> dict[str, Any]: +def unpin_thread(user_id: str, thread_id: str, course_id: str | None = None) -> dict[str, Any]: """ Unpin a thread. Parameters: diff --git a/forum/api/search.py b/forum/api/search.py index bec053d4..13312117 100644 --- a/forum/api/search.py +++ b/forum/api/search.py @@ -2,7 +2,7 @@ API for search. """ -from typing import Any, Optional +from typing import Any from forum.backend import get_backend from forum.constants import FORUM_DEFAULT_PAGE, FORUM_DEFAULT_PER_PAGE @@ -14,9 +14,9 @@ def _get_thread_ids_from_indexes( context: str, group_ids: list[int], text: str, - commentable_ids: Optional[list[str]] = None, - course_id: Optional[str] = None, -) -> tuple[list[str], Optional[str]]: + commentable_ids: list[str] | None = None, + course_id: str | None = None, +) -> tuple[list[str], str | None]: """ Retrieve thread IDs based on the search text and suggested corrections if necessary. @@ -31,7 +31,7 @@ def _get_thread_ids_from_indexes( - A list of thread IDs that match the search criteria. - A suggested correction for the search text, or None if no correction is found. """ - corrected_text: Optional[str] = None + corrected_text: str | None = None thread_search = get_thread_search_backend() thread_ids = thread_search.get_thread_ids( @@ -61,10 +61,10 @@ def search_threads( text: str, user_id: str, course_id: str, - group_ids: Optional[list[int]] = None, - commentable_ids: Optional[list[str]] = None, - author_id: Optional[str] = None, - thread_type: Optional[str] = None, + group_ids: list[int] | None = None, + commentable_ids: list[str] | None = None, + author_id: str | None = None, + thread_type: str | None = None, sort_key: str = "date", context: str = "course", flagged: bool = False, @@ -82,9 +82,7 @@ def search_threads( group_ids = group_ids or [] commentable_ids = commentable_ids or [] - thread_ids, corrected_text = _get_thread_ids_from_indexes( - context, group_ids, text, commentable_ids, course_id - ) + thread_ids, corrected_text = _get_thread_ids_from_indexes(context, group_ids, text, commentable_ids, course_id) backend = get_backend(course_id)() diff --git a/forum/api/subscriptions.py b/forum/api/subscriptions.py index f21639e0..ff2c2ff7 100644 --- a/forum/api/subscriptions.py +++ b/forum/api/subscriptions.py @@ -2,22 +2,22 @@ API for subscriptions. """ -from typing import Any, Optional +from typing import Any from django.http import QueryDict from rest_framework.request import Request from rest_framework.test import APIRequestFactory from forum.backend import get_backend +from forum.constants import FORUM_DEFAULT_PAGE, FORUM_DEFAULT_PER_PAGE from forum.pagination import ForumPagination from forum.serializers.subscriptions import SubscriptionSerializer from forum.serializers.thread import ThreadSerializer from forum.utils import ForumV2RequestError -from forum.constants import FORUM_DEFAULT_PAGE, FORUM_DEFAULT_PER_PAGE def validate_user_and_thread( - user_id: str, source_id: str, course_id: Optional[str] = None + user_id: str, source_id: str, course_id: str | None = None ) -> tuple[dict[str, Any], dict[str, Any]]: """ Validate if user and thread exist. @@ -30,33 +30,25 @@ def validate_user_and_thread( return user, thread -def create_subscription( - user_id: str, source_id: str, course_id: Optional[str] = None -) -> dict[str, Any]: +def create_subscription(user_id: str, source_id: str, course_id: str | None = None) -> dict[str, Any]: """ Create a subscription for a user. """ backend = get_backend(course_id)() _, _ = validate_user_and_thread(user_id, source_id, course_id=course_id) - subscription = backend.subscribe_user( - user_id, source_id, source_type="CommentThread" - ) + subscription = backend.subscribe_user(user_id, source_id, source_type="CommentThread") serializer = SubscriptionSerializer(subscription) return serializer.data -def delete_subscription( - user_id: str, source_id: str, course_id: Optional[str] = None -) -> dict[str, Any]: +def delete_subscription(user_id: str, source_id: str, course_id: str | None = None) -> dict[str, Any]: """ Delete a subscription for a user. """ backend = get_backend(course_id)() _, _ = validate_user_and_thread(user_id, source_id, course_id=course_id) - subscription = backend.get_subscription( - user_id, source_id, source_type="CommentThread" - ) + subscription = backend.get_subscription(user_id, source_id, source_type="CommentThread") if not subscription: raise ForumV2RequestError("Subscription doesn't exist") @@ -68,18 +60,18 @@ def delete_subscription( def get_user_subscriptions( user_id: str, course_id: str, - author_id: Optional[str] = None, - thread_type: Optional[str] = None, - flagged: Optional[bool] = False, - unread: Optional[bool] = False, - unanswered: Optional[bool] = False, - unresponded: Optional[bool] = False, - count_flagged: Optional[bool] = False, - sort_key: Optional[str] = "date", - page: Optional[int] = FORUM_DEFAULT_PAGE, - per_page: Optional[int] = FORUM_DEFAULT_PER_PAGE, - group_id: Optional[str] = None, - group_ids: Optional[str] = None, + author_id: str | None = None, + thread_type: str | None = None, + flagged: bool | None = False, + unread: bool | None = False, + unanswered: bool | None = False, + unresponded: bool | None = False, + count_flagged: bool | None = False, + sort_key: str | None = "date", + page: int | None = FORUM_DEFAULT_PAGE, + per_page: int | None = FORUM_DEFAULT_PER_PAGE, + group_id: str | None = None, + group_ids: str | None = None, ) -> dict[str, Any]: """ Get a user's subscriptions. @@ -108,7 +100,7 @@ def get_user_subscriptions( def get_thread_subscriptions( - thread_id: str, page: int = 1, per_page: int = 20, course_id: Optional[str] = None + thread_id: str, page: int = 1, per_page: int = 20, course_id: str | None = None ) -> dict[str, Any]: """ Retrieve subscriptions to a specific thread. @@ -132,9 +124,7 @@ def get_thread_subscriptions( drf_request = Request(request) paginator = ForumPagination() - paginated_subscriptions = paginator.paginate_queryset( - subscriptions_list, drf_request - ) + paginated_subscriptions = paginator.paginate_queryset(subscriptions_list, drf_request) subscriptions = SubscriptionSerializer(paginated_subscriptions, many=True) subscriptions_count = len(subscriptions.data) diff --git a/forum/api/threads.py b/forum/api/threads.py index fc089758..c24e8ee3 100644 --- a/forum/api/threads.py +++ b/forum/api/threads.py @@ -3,7 +3,7 @@ """ import logging -from typing import Any, Optional +from typing import Any from django.core.exceptions import ObjectDoesNotExist from rest_framework.serializers import ValidationError @@ -65,10 +65,10 @@ def get_thread_data(thread: dict[str, Any]) -> dict[str, Any]: def prepare_thread_api_response( thread: dict[str, Any], backend: Any, - include_context: Optional[bool] = False, - data_or_params: Optional[dict[str, Any]] = None, - include_data_from_params: Optional[bool] = False, - course_id: Optional[str] = None, + include_context: bool | None = False, + data_or_params: dict[str, Any] | None = None, + include_data_from_params: bool | None = False, + course_id: str | None = None, ) -> dict[str, Any]: """Serialize thread data for the api response.""" thread_data = get_thread_data(thread) @@ -84,12 +84,8 @@ def prepare_thread_api_response( context["user_id"] = user_id if include_data_from_params: - thread_data["resp_skip"] = get_int_value_from_collection( - data_or_params, "resp_skip", 0 - ) - thread_data["resp_limit"] = get_int_value_from_collection( - data_or_params, "resp_limit", 100 - ) + thread_data["resp_skip"] = get_int_value_from_collection(data_or_params, "resp_skip", 0) + thread_data["resp_limit"] = get_int_value_from_collection(data_or_params, "resp_limit", 100) params = [ "recursive", "with_responses", @@ -117,8 +113,8 @@ def prepare_thread_api_response( def get_thread( thread_id: str, - params: Optional[dict[str, Any]] = None, - course_id: Optional[str] = None, + params: dict[str, Any] | None = None, + course_id: str | None = None, ) -> dict[str, Any]: """ Get the thread for the given thread_id. @@ -141,9 +137,7 @@ def get_thread( thread = backend.validate_object("CommentThread", thread_id) except ObjectDoesNotExist as exc: log.error("Forumv2RequestError for get thread request.") - raise ForumV2RequestError( - f"Thread does not exist with Id: {thread_id}" - ) from exc + raise ForumV2RequestError(f"Thread does not exist with Id: {thread_id}") from exc try: return prepare_thread_api_response( @@ -159,7 +153,7 @@ def get_thread( raise ForumV2RequestError("Failed to prepare thread API response") from error -def delete_thread(thread_id: str, course_id: Optional[str] = None) -> dict[str, Any]: +def delete_thread(thread_id: str, course_id: str | None = None) -> dict[str, Any]: """ Delete the thread for the given thread_id. @@ -173,9 +167,7 @@ def delete_thread(thread_id: str, course_id: Optional[str] = None) -> dict[str, thread = backend.validate_object("CommentThread", thread_id) except ObjectDoesNotExist as exc: log.error("Forumv2RequestError for delete thread request.") - raise ForumV2RequestError( - f"Thread does not exist with Id: {thread_id}" - ) from exc + raise ForumV2RequestError(f"Thread does not exist with Id: {thread_id}") from exc backend.delete_comments_of_a_thread(thread_id) thread = backend.validate_object("CommentThread", thread_id) @@ -189,30 +181,28 @@ def delete_thread(thread_id: str, course_id: Optional[str] = None) -> dict[str, backend.delete_subscriptions_of_a_thread(thread_id) result = backend.delete_thread(thread_id) if result and not (thread["anonymous"] or thread["anonymous_to_peers"]): - backend.update_stats_for_course( - thread["author_id"], thread["course_id"], threads=-1 - ) + backend.update_stats_for_course(thread["author_id"], thread["course_id"], threads=-1) return serialized_data def update_thread( thread_id: str, - title: Optional[str] = None, - body: Optional[str] = None, - course_id: Optional[str] = None, - anonymous: Optional[bool] = None, - anonymous_to_peers: Optional[bool] = None, - closed: Optional[bool] = None, - commentable_id: Optional[str] = None, - user_id: Optional[str] = None, - editing_user_id: Optional[str] = None, - pinned: Optional[bool] = None, - thread_type: Optional[str] = None, - edit_reason_code: Optional[str] = None, - close_reason_code: Optional[str] = None, - closing_user_id: Optional[str] = None, - endorsed: Optional[bool] = None, + title: str | None = None, + body: str | None = None, + course_id: str | None = None, + anonymous: bool | None = None, + anonymous_to_peers: bool | None = None, + closed: bool | None = None, + commentable_id: str | None = None, + user_id: str | None = None, + editing_user_id: str | None = None, + pinned: bool | None = None, + thread_type: str | None = None, + edit_reason_code: str | None = None, + close_reason_code: str | None = None, + closing_user_id: str | None = None, + endorsed: bool | None = None, ) -> dict[str, Any]: """ Update the thread for the given thread_id. @@ -228,9 +218,7 @@ def update_thread( thread = backend.validate_object("CommentThread", thread_id) except ObjectDoesNotExist as exc: log.error("Forumv2RequestError for update thread request.") - raise ForumV2RequestError( - f"Thread does not exist with Id: {thread_id}" - ) from exc + raise ForumV2RequestError(f"Thread does not exist with Id: {thread_id}") from exc data = { "title": title, @@ -254,19 +242,13 @@ def update_thread( update_thread_data["original_body"] = thread.get("body") if update_thread_data.get("closed"): - missing_fields = {"close_reason_code", "closed_by_id"} - set( - update_thread_data.keys() - ) + missing_fields = {"close_reason_code", "closed_by_id"} - set(update_thread_data.keys()) if missing_fields: - raise ForumV2RequestError( - f"Missing required fields: {', '.join(missing_fields)}" - ) + raise ForumV2RequestError(f"Missing required fields: {', '.join(missing_fields)}") backend.update_thread(thread_id, **update_thread_data) thread = backend.get_thread(thread_id) if thread is None: - log.error( - "Forumv2RequestError for update thread request - retrieving updated thread to send in response." - ) + log.error("Forumv2RequestError for update thread request - retrieving updated thread to send in response.") raise ForumV2RequestError(f"Thread no longer exists with Id: {thread_id}") try: @@ -290,7 +272,7 @@ def create_thread( anonymous_to_peers: bool = False, commentable_id: str = "course", thread_type: str = "discussion", - group_id: Optional[int] = None, + group_id: int | None = None, context: str = "course", ) -> dict[str, Any]: """ @@ -330,9 +312,7 @@ def create_thread( raise ForumV2RequestError(f"Failed to create thread with data: {data}") if not (anonymous or anonymous_to_peers): - backend.update_stats_for_course( - thread["author_id"], thread["course_id"], threads=1 - ) + backend.update_stats_for_course(thread["author_id"], thread["course_id"], threads=1) try: return prepare_thread_api_response( @@ -348,22 +328,22 @@ def create_thread( def get_user_threads( course_id: str, - author_id: Optional[str] = None, - thread_type: Optional[str] = None, - flagged: Optional[bool] = None, - unread: Optional[bool] = None, - unanswered: Optional[bool] = None, - unresponded: Optional[bool] = None, - count_flagged: Optional[bool] = None, - sort_key: Optional[str] = None, - page: Optional[str] = None, - per_page: Optional[str] = None, - request_id: Optional[str] = None, - commentable_ids: Optional[str] = None, - user_id: Optional[str] = None, - group_id: Optional[int] = None, - group_ids: Optional[int] = None, - context: Optional[str] = None, + author_id: str | None = None, + thread_type: str | None = None, + flagged: bool | None = None, + unread: bool | None = None, + unanswered: bool | None = None, + unresponded: bool | None = None, + count_flagged: bool | None = None, + sort_key: str | None = None, + page: str | None = None, + per_page: str | None = None, + request_id: str | None = None, + commentable_ids: str | None = None, + user_id: str | None = None, + group_id: int | None = None, + group_ids: int | None = None, + context: str | None = None, **kwargs: Any, ) -> dict[str, Any]: """ diff --git a/forum/api/users.py b/forum/api/users.py index 8355f8d3..de902223 100644 --- a/forum/api/users.py +++ b/forum/api/users.py @@ -4,7 +4,7 @@ import logging import math -from typing import Any, Optional +from typing import Any from forum.backend import get_backend from forum.constants import FORUM_DEFAULT_PAGE, FORUM_DEFAULT_PER_PAGE @@ -17,9 +17,9 @@ def get_user( user_id: str, - group_ids: Optional[list[int]] = None, - course_id: Optional[str] = None, - complete: Optional[bool] = False, + group_ids: list[int] | None = None, + course_id: str | None = None, + complete: bool | None = False, ) -> dict[str, Any]: """Get user data by user_id.""" """ @@ -48,11 +48,11 @@ def get_user( def update_user( user_id: str, - username: Optional[str] = None, - default_sort_key: Optional[str] = None, - course_id: Optional[str] = None, - group_ids: Optional[list[int]] = None, - complete: Optional[bool] = False, + username: str | None = None, + default_sort_key: str | None = None, + course_id: str | None = None, + group_ids: list[int] | None = None, + complete: bool | None = False, ) -> dict[str, Any]: """Update user.""" backend = get_backend(course_id)() @@ -86,8 +86,8 @@ def create_user( user_id: str, username: str, default_sort_key: str = "date", - course_id: Optional[str] = None, - group_ids: Optional[list[int]] = None, + course_id: str | None = None, + group_ids: list[int] | None = None, complete: bool = False, ) -> dict[str, Any]: """Create user.""" @@ -98,9 +98,7 @@ def create_user( if user_by_id or user_by_username: raise ForumV2RequestError(f"user already exists with id: {id}") - backend.find_or_create_user( - user_id, username=username, default_sort_key=default_sort_key - ) + backend.find_or_create_user(user_id, username=username, default_sort_key=default_sort_key) user = backend.get_user(user_id) if not user: raise ForumV2RequestError(f"user not found with id: {user_id}") @@ -114,9 +112,7 @@ def create_user( return serializer.data -def update_username( - user_id: str, new_username: str, course_id: Optional[str] = None -) -> dict[str, str]: +def update_username(user_id: str, new_username: str, course_id: str | None = None) -> dict[str, str]: """Update username.""" backend = get_backend(course_id)() user = backend.get_user(user_id) @@ -127,9 +123,7 @@ def update_username( return {"message": "Username updated successfully"} -def retire_user( - user_id: str, retired_username: str, course_id: Optional[str] = None -) -> dict[str, str]: +def retire_user(user_id: str, retired_username: str, course_id: str | None = None) -> dict[str, str]: """Retire user.""" backend = get_backend(course_id)() user = backend.get_user(user_id) @@ -153,8 +147,8 @@ def mark_thread_as_read( user_id: str, source_id: str, complete: bool = False, - course_id: Optional[str] = None, - group_ids: Optional[list[int]] = None, + course_id: str | None = None, + group_ids: list[int] | None = None, ) -> dict[str, Any]: """Mark thread as read.""" backend = get_backend(course_id)() @@ -186,18 +180,18 @@ def mark_thread_as_read( def get_user_active_threads( user_id: str, course_id: str, - author_id: Optional[str] = None, - thread_type: Optional[str] = None, - flagged: Optional[bool] = False, - unread: Optional[bool] = False, - unanswered: Optional[bool] = False, - unresponded: Optional[bool] = False, - count_flagged: Optional[bool] = False, - sort_key: Optional[str] = "user_activity", - page: Optional[int] = FORUM_DEFAULT_PAGE, - per_page: Optional[int] = FORUM_DEFAULT_PER_PAGE, - group_id: Optional[str] = None, - is_moderator: Optional[bool] = False, + author_id: str | None = None, + thread_type: str | None = None, + flagged: bool | None = False, + unread: bool | None = False, + unanswered: bool | None = False, + unresponded: bool | None = False, + count_flagged: bool | None = False, + sort_key: str | None = "user_activity", + page: int | None = FORUM_DEFAULT_PAGE, + per_page: int | None = FORUM_DEFAULT_PER_PAGE, + group_id: str | None = None, + is_moderator: bool | None = False, ) -> dict[str, Any]: """Get user active threads.""" backend = get_backend(course_id)() @@ -215,22 +209,14 @@ def get_user_active_threads( if flagged: active_contents = [ - content - for content in active_contents - if content["abuse_flaggers"] and len(content["abuse_flaggers"]) > 0 + content for content in active_contents if content["abuse_flaggers"] and len(content["abuse_flaggers"]) > 0 ] - active_contents = sorted( - active_contents, key=lambda x: x["updated_at"], reverse=True - ) + active_contents = sorted(active_contents, key=lambda x: x["updated_at"], reverse=True) active_thread_ids = list( - set( - ( - content["comment_thread_id"] - if content["_type"] == "Comment" - else content["_id"] - ) + { + (content["comment_thread_id"] if content["_type"] == "Comment" else content["_id"]) for content in active_contents - ) + } ) params: dict[str, Any] = { @@ -271,16 +257,12 @@ def get_user_active_threads( for thread in collection: thread["_id"] = str(thread.pop("_id")) thread["type"] = str(thread.get("_type", "")).lower() - data["collection"] = ThreadSerializer( - collection, many=True, backend=backend - ).data + data["collection"] = ThreadSerializer(collection, many=True, backend=backend).data return data -def _get_user_data( - user_stats: dict[str, Any], exclude_from_stats: list[str] -) -> dict[str, Any]: +def _get_user_data(user_stats: dict[str, Any], exclude_from_stats: list[str]) -> dict[str, Any]: """Get user data from user stats.""" user_data = {"username": user_stats["username"]} for k, v in user_stats["course_stats"].items(): @@ -289,9 +271,7 @@ def _get_user_data( return user_data -def _get_stats_for_usernames( - course_id: str, usernames: list[str], backend: Any -) -> list[dict[str, Any]]: +def _get_stats_for_usernames(course_id: str, usernames: list[str], backend: Any) -> list[dict[str, Any]]: """Get stats for specific usernames.""" users = backend.get_users() stats_query = [] @@ -302,16 +282,14 @@ def _get_stats_for_usernames( if course_stats: for course_stat in course_stats: if course_stat["course_id"] == course_id: - stats_query.append( - {"username": user["username"], "course_stats": course_stat} - ) + stats_query.append({"username": user["username"], "course_stats": course_stat}) break return sorted(stats_query, key=lambda u: usernames.index(u["username"])) def get_user_course_stats( course_id: str, - usernames: Optional[str] = None, + usernames: str | None = None, page: int = FORUM_DEFAULT_PAGE, per_page: int = FORUM_DEFAULT_PER_PAGE, sort_key: str = "", @@ -328,19 +306,14 @@ def get_user_course_stats( data = [] if not usernames_list: - paginated_stats = backend.get_paginated_user_stats( - course_id, page, per_page, sort_criterion - ) + paginated_stats = backend.get_paginated_user_stats(course_id, page, per_page, sort_criterion) num_pages = 0 page = 0 total_count = 0 if paginated_stats.get("pagination"): total_count = paginated_stats["pagination"][0]["total_count"] num_pages = max(1, math.ceil(total_count / per_page)) - data = [ - _get_user_data(user_stats, exclude_from_stats) - for user_stats in paginated_stats["data"] - ] + data = [_get_user_data(user_stats, exclude_from_stats) for user_stats in paginated_stats["data"]] else: stats_query = _get_stats_for_usernames(course_id, usernames_list, backend) total_count = len(stats_query) @@ -348,11 +321,7 @@ def get_user_course_stats( data = [ { "username": user_stats["username"], - **{ - k: v - for k, v in user_stats["course_stats"].items() - if k not in exclude_from_stats - }, + **{k: v for k, v in user_stats["course_stats"].items() if k not in exclude_from_stats}, } for user_stats in stats_query ] diff --git a/forum/api/votes.py b/forum/api/votes.py index 6e02def9..27a5d964 100644 --- a/forum/api/votes.py +++ b/forum/api/votes.py @@ -2,7 +2,7 @@ API for votes. """ -from typing import Any, Optional +from typing import Any from forum.backend import get_backend from forum.serializers.comment import CommentSerializer @@ -14,7 +14,7 @@ def _get_thread_and_user( thread_id: str, user_id: str, - course_id: Optional[str] = None, + course_id: str | None = None, ) -> tuple[dict[str, Any], dict[str, Any]]: """ Fetches the thread and user based on provided IDs. @@ -41,9 +41,7 @@ def _get_thread_and_user( return thread, user -def _prepare_thread_response( - thread: dict[str, Any], user: dict[str, Any], backend: Any -) -> dict[str, Any]: +def _prepare_thread_response(thread: dict[str, Any], user: dict[str, Any], backend: Any) -> dict[str, Any]: """ Prepares the serialized response data after voting. @@ -70,9 +68,7 @@ def _prepare_thread_response( return serializer.data -def update_thread_votes( - thread_id: str, user_id: str, value: str, course_id: Optional[str] = None -) -> dict[str, Any]: +def update_thread_votes(thread_id: str, user_id: str, value: str, course_id: str | None = None) -> dict[str, Any]: """ Updates the votes for a thread. @@ -94,13 +90,9 @@ def update_thread_votes( raise ForumV2RequestError(str(error)) from error if vote_serializer.data["value"] == "up": - is_updated = backend.upvote_content( - thread_id, user_id, entity_type="CommentThread" - ) + is_updated = backend.upvote_content(thread_id, user_id, entity_type="CommentThread") else: - is_updated = backend.downvote_content( - thread_id, user_id, entity_type="CommentThread" - ) + is_updated = backend.downvote_content(thread_id, user_id, entity_type="CommentThread") if is_updated: thread = backend.get_thread(thread_id) or {} @@ -108,9 +100,7 @@ def update_thread_votes( return _prepare_thread_response(thread, user, backend) -def delete_thread_vote( - thread_id: str, user_id: str, course_id: Optional[str] = None -) -> dict[str, Any]: +def delete_thread_vote(thread_id: str, user_id: str, course_id: str | None = None) -> dict[str, Any]: """ Deletes the vote for a thread. @@ -134,9 +124,7 @@ def delete_thread_vote( return _prepare_thread_response(deleted_thread, user, backend) -def _get_comment_and_user( - comment_id: str, user_id: str, backend: Any -) -> tuple[dict[str, Any], dict[str, Any]]: +def _get_comment_and_user(comment_id: str, user_id: str, backend: Any) -> tuple[dict[str, Any], dict[str, Any]]: """ Fetches the comment and user based on provided IDs. @@ -161,9 +149,7 @@ def _get_comment_and_user( return comment, user -def _prepare_comment_response( - comment: dict[str, Any], user: dict[str, Any], backend: Any -) -> dict[str, Any]: +def _prepare_comment_response(comment: dict[str, Any], user: dict[str, Any], backend: Any) -> dict[str, Any]: """ Prepares the serialized response data after voting. @@ -191,9 +177,7 @@ def _prepare_comment_response( return serializer.data -def update_comment_votes( - comment_id: str, user_id: str, value: str, course_id: Optional[str] = None -) -> dict[str, Any]: +def update_comment_votes(comment_id: str, user_id: str, value: str, course_id: str | None = None) -> dict[str, Any]: """ Updates the votes for a comment. @@ -217,9 +201,7 @@ def update_comment_votes( if vote_serializer.data["value"] == "up": is_updated = backend.upvote_content(comment_id, user_id, entity_type="Comment") else: - is_updated = backend.downvote_content( - comment_id, user_id, entity_type="Comment" - ) + is_updated = backend.downvote_content(comment_id, user_id, entity_type="Comment") updated_comment = None if is_updated: @@ -231,9 +213,7 @@ def update_comment_votes( return _prepare_comment_response(updated_comment, user, backend) -def delete_comment_vote( - comment_id: str, user_id: str, course_id: Optional[str] = None -) -> dict[str, Any]: +def delete_comment_vote(comment_id: str, user_id: str, course_id: str | None = None) -> dict[str, Any]: """ Deletes the vote for a comment. diff --git a/forum/apps.py b/forum/apps.py index f7ec0750..934a07c7 100644 --- a/forum/apps.py +++ b/forum/apps.py @@ -33,4 +33,4 @@ def ready(self) -> None: """ Import Signals. """ - import forum.signals # pylint: disable=import-outside-toplevel, unused-import + import forum.signals # noqa: F401 diff --git a/forum/backends/backend.py b/forum/backends/backend.py index 2f7a4081..4fee68ec 100644 --- a/forum/backends/backend.py +++ b/forum/backends/backend.py @@ -1,22 +1,18 @@ """Forum backend interface class.""" -from typing import Any, Optional +from typing import Any class AbstractBackend: """Abstract backend interface class.""" @classmethod - def update_stats_for_course( - cls, user_id: str, course_id: str, **kwargs: Any - ) -> None: + def update_stats_for_course(cls, user_id: str, course_id: str, **kwargs: Any) -> None: """Update statistics for a course.""" raise NotImplementedError @classmethod - def flag_as_abuse( - cls, user_id: str, entity_id: str, **kwargs: Any - ) -> dict[str, Any]: + def flag_as_abuse(cls, user_id: str, entity_id: str, **kwargs: Any) -> dict[str, Any]: """Flag an entity as abuse.""" raise NotImplementedError @@ -28,9 +24,7 @@ def update_stats_after_unflag( raise NotImplementedError @classmethod - def un_flag_as_abuse( - cls, user_id: str, entity_id: str, **kwargs: Any - ) -> dict[str, Any]: + def un_flag_as_abuse(cls, user_id: str, entity_id: str, **kwargs: Any) -> dict[str, Any]: """Unflag an entity as abuse.""" raise NotImplementedError @@ -41,11 +35,7 @@ def un_flag_all_as_abuse(cls, entity_id: str, **kwargs: Any) -> dict[str, Any]: @staticmethod def update_vote( - content_id: str, - user_id: str, - vote_type: str = "", - is_deleted: bool = False, - **kwargs: Any + content_id: str, user_id: str, vote_type: str = "", is_deleted: bool = False, **kwargs: Any ) -> bool: """Update vote for a content.""" raise NotImplementedError @@ -66,9 +56,7 @@ def remove_vote(cls, entity_id: str, user_id: str, **kwargs: Any) -> bool: raise NotImplementedError @staticmethod - def validate_thread_and_user( - user_id: str, thread_id: str - ) -> tuple[dict[str, Any], dict[str, Any]]: + def validate_thread_and_user(user_id: str, thread_id: str) -> tuple[dict[str, Any], dict[str, Any]]: """Validate a thread and user.""" raise NotImplementedError @@ -97,9 +85,7 @@ def get_abuse_flagged_count(thread_ids: list[str]) -> dict[str, int]: raise NotImplementedError @staticmethod - def get_read_states( - thread_ids: list[str], user_id: str, course_id: str - ) -> dict[str, list[Any]]: + def get_read_states(thread_ids: list[str], user_id: str, course_id: str) -> dict[str, list[Any]]: """Get read states.""" raise NotImplementedError @@ -109,9 +95,7 @@ def get_endorsed(thread_ids: list[str]) -> dict[str, bool]: raise NotImplementedError @staticmethod - def get_user_read_state_by_course_id( - user_id: str, course_id: str - ) -> dict[str, Any]: + def get_user_read_state_by_course_id(user_id: str, course_id: str) -> dict[str, Any]: """Get user read state by course id.""" raise NotImplementedError @@ -122,8 +106,8 @@ def handle_threads_query( user_id: str, course_id: str, group_ids: list[int], - author_id: Optional[str], - thread_type: Optional[str], + author_id: str | None, + thread_type: str | None, filter_flagged: bool, filter_unread: bool, filter_unanswered: bool, @@ -134,7 +118,7 @@ def handle_threads_query( per_page: int, context: str = "course", raw_query: bool = False, - commentable_ids: Optional[list[str]] = None, + commentable_ids: list[str] | None = None, is_moderator: bool = False, ) -> dict[str, Any]: """Handle threads query.""" @@ -163,7 +147,7 @@ def threads_presentor( raise NotImplementedError @staticmethod - def get_username_from_id(user_id: str) -> Optional[str]: + def get_username_from_id(user_id: str) -> str | None: """Get username from id.""" raise NotImplementedError @@ -173,16 +157,12 @@ def validate_object(model: str, obj_id: str) -> Any: raise NotImplementedError @staticmethod - def find_subscribed_threads( - user_id: str, course_id: Optional[str] = None - ) -> list[str]: + def find_subscribed_threads(user_id: str, course_id: str | None = None) -> list[str]: """Find subscribed threads.""" raise NotImplementedError @staticmethod - def subscribe_user( - user_id: str, source_id: str, source_type: str - ) -> dict[str, Any] | None: + def subscribe_user(user_id: str, source_id: str, source_type: str) -> dict[str, Any] | None: """Subscribe user.""" raise NotImplementedError @@ -202,7 +182,7 @@ def delete_subscriptions_of_a_thread(thread_id: str) -> None: raise NotImplementedError @staticmethod - def validate_params(params: dict[str, Any], user_id: Optional[str] = None) -> Any: + def validate_params(params: dict[str, Any], user_id: str | None = None) -> Any: """Validate params.""" raise NotImplementedError @@ -233,9 +213,7 @@ def filter_standalone_threads(comment_ids: list[str]) -> list[str]: raise NotImplementedError @classmethod - def user_to_hash( - cls, user_id: str, params: Optional[dict[str, Any]] = None - ) -> dict[str, Any]: + def user_to_hash(cls, user_id: str, params: dict[str, Any] | None = None) -> dict[str, Any]: """User to hash.""" raise NotImplementedError @@ -290,9 +268,7 @@ def get_user_by_username(username: str | None) -> dict[str, Any] | None: raise NotImplementedError @staticmethod - def find_or_create_user( - user_id: str, username: Optional[str] = "", default_sort_key: Optional[str] = "" - ) -> str: + def find_or_create_user(user_id: str, username: str | None = "", default_sort_key: str | None = "") -> str: """Find or create user.""" raise NotImplementedError @@ -342,9 +318,7 @@ def get_user(user_id: str, get_full_dict: bool = True) -> dict[str, Any] | None: raise NotImplementedError @staticmethod - def get_subscription( - subscriber_id: str, source_id: str, **kwargs: Any - ) -> dict[str, Any] | None: + def get_subscription(subscriber_id: str, source_id: str, **kwargs: Any) -> dict[str, Any] | None: """Get subscription.""" raise NotImplementedError @@ -369,9 +343,7 @@ def update_thread(thread_id: str, **kwargs: Any) -> int: raise NotImplementedError @staticmethod - def get_filtered_threads( - query: dict[str, Any], ids_only: bool = False - ) -> list[dict[str, Any]]: + def get_filtered_threads(query: dict[str, Any], ids_only: bool = False) -> list[dict[str, Any]]: """Get filtered threads.""" raise NotImplementedError @@ -390,16 +362,16 @@ def get_thread_id_by_comment_id(parent_comment_id: str) -> str: @staticmethod def update_comment_and_get_updated_comment( comment_id: str, - body: Optional[str] = None, - course_id: Optional[str] = None, - user_id: Optional[str] = None, - anonymous: Optional[bool] = False, - anonymous_to_peers: Optional[bool] = False, - endorsed: Optional[bool] = False, - closed: Optional[bool] = False, - editing_user_id: Optional[str] = None, - edit_reason_code: Optional[str] = None, - endorsement_user_id: Optional[str] = None, + body: str | None = None, + course_id: str | None = None, + user_id: str | None = None, + anonymous: bool | None = False, + anonymous_to_peers: bool | None = False, + endorsed: bool | None = False, + closed: bool | None = False, + editing_user_id: str | None = None, + edit_reason_code: str | None = None, + endorsement_user_id: str | None = None, ) -> dict[str, Any] | None: """Update comment and get updated comment.""" raise NotImplementedError @@ -455,7 +427,7 @@ def get_votes_dict(up: list[str], down: list[str]) -> dict[str, Any]: return votes @staticmethod - def find_thread(**kwargs: Any) -> Optional[dict[str, Any]]: + def find_thread(**kwargs: Any) -> dict[str, Any] | None: """ Retrieves a first matching thread from the database. """ @@ -464,7 +436,7 @@ def find_thread(**kwargs: Any) -> Optional[dict[str, Any]]: @staticmethod def find_comment( is_parent_comment: bool = True, with_abuse_flaggers: bool = False, **kwargs: Any - ) -> Optional[dict[str, Any]]: + ) -> dict[str, Any] | None: """ Retrieves a first matching comment from the database. """ diff --git a/forum/backends/mysql/__init__.py b/forum/backends/mysql/__init__.py index 72a7b01c..6fe82a4f 100644 --- a/forum/backends/mysql/__init__.py +++ b/forum/backends/mysql/__init__.py @@ -1,5 +1,5 @@ """MySQL backend for forum v2.""" -from forum.backends.mysql.models import Content, CommentThread, Comment +from forum.backends.mysql.models import Comment, CommentThread, Content MODEL_INDICES: tuple[type[Content], ...] = (CommentThread, Comment) diff --git a/forum/backends/mysql/api.py b/forum/backends/mysql/api.py index da55a552..c997bb5f 100644 --- a/forum/backends/mysql/api.py +++ b/forum/backends/mysql/api.py @@ -3,7 +3,7 @@ import math import random from datetime import timedelta -from typing import Any, Optional, Union +from typing import Any from django.contrib.auth.models import User # pylint: disable=E5142 from django.contrib.contenttypes.models import ContentType @@ -49,14 +49,10 @@ class MySQLBackend(AbstractBackend): """MySQL backend api.""" @classmethod - def update_stats_for_course( - cls, user_id: str, course_id: str, **kwargs: Any - ) -> None: + def update_stats_for_course(cls, user_id: str, course_id: str, **kwargs: Any) -> None: """Update stats for a course.""" user = User.objects.get(pk=user_id) - course_stat, created = CourseStat.objects.get_or_create( - user=user, course_id=course_id - ) + course_stat, created = CourseStat.objects.get_or_create(user=user, course_id=course_id) if created: course_stat.active_flags = 0 course_stat.inactive_flags = 0 @@ -72,9 +68,7 @@ def update_stats_for_course( cls.build_course_stats(user_id, course_id) @staticmethod - def _get_entity_from_type( - entity_id: str, entity_type: str - ) -> Union[Comment, CommentThread, None]: + def _get_entity_from_type(entity_id: str, entity_type: str) -> Comment | CommentThread | None: """Get entity from type.""" try: if entity_type == "Comment": @@ -85,37 +79,27 @@ def _get_entity_from_type( return None @classmethod - def flag_as_abuse( - cls, user_id: str, entity_id: str, **kwargs: Any - ) -> dict[str, Any]: + def flag_as_abuse(cls, user_id: str, entity_id: str, **kwargs: Any) -> dict[str, Any]: """Flag an entity as abuse.""" user = User.objects.get(pk=user_id) - entity = cls._get_entity_from_type( - entity_id, entity_type=kwargs.get("entity_type", "") - ) + entity = cls._get_entity_from_type(entity_id, entity_type=kwargs.get("entity_type", "")) if not entity: raise ValueError("Entity doesn't exist.") abuse_flaggers = entity.abuse_flaggers first_flag_added = False if user.pk not in abuse_flaggers: - AbuseFlagger.objects.create( - user=user, content=entity, flagged_at=timezone.now() - ) + AbuseFlagger.objects.create(user=user, content=entity, flagged_at=timezone.now()) first_flag_added = len(abuse_flaggers) == 1 if first_flag_added: cls.update_stats_for_course(user_id, entity.course_id, active_flags=1) return entity.to_dict() @classmethod - def un_flag_as_abuse( - cls, user_id: str, entity_id: str, **kwargs: Any - ) -> dict[str, Any]: + def un_flag_as_abuse(cls, user_id: str, entity_id: str, **kwargs: Any) -> dict[str, Any]: """Unflag an entity as abuse.""" user = User.objects.get(pk=user_id) - entity = cls._get_entity_from_type( - entity_id, entity_type=kwargs.get("entity_type", "") - ) + entity = cls._get_entity_from_type(entity_id, entity_type=kwargs.get("entity_type", "")) if not entity: raise ValueError("Entity doesn't exist.") @@ -138,16 +122,12 @@ def un_flag_as_abuse( @classmethod def un_flag_all_as_abuse(cls, entity_id: str, **kwargs: Any) -> dict[str, Any]: """Unflag all users from an entity.""" - entity = cls._get_entity_from_type( - entity_id, entity_type=kwargs.get("entity_type", "") - ) + entity = cls._get_entity_from_type(entity_id, entity_type=kwargs.get("entity_type", "")) if not entity: raise ValueError("Entity doesn't exist.") has_no_historical_flags = len(entity.historical_abuse_flaggers) == 0 - historical_abuse_flaggers = list( - set(entity.historical_abuse_flaggers) | set(entity.abuse_flaggers) - ) + historical_abuse_flaggers = list(set(entity.historical_abuse_flaggers) | set(entity.abuse_flaggers)) for flagger_id in historical_abuse_flaggers: # Skip if HistoricalAbuseFlagger already exists for this user and entity if not HistoricalAbuseFlagger.objects.filter( @@ -160,9 +140,7 @@ def un_flag_all_as_abuse(cls, entity_id: str, **kwargs: Any) -> dict[str, Any]: user_id=flagger_id, flagged_at=timezone.now(), ) - AbuseFlagger.objects.filter( - content_object_id=entity.pk, content_type=entity.content_type - ).delete() + AbuseFlagger.objects.filter(content_object_id=entity.pk, content_type=entity.content_type).delete() cls.update_stats_after_unflag( entity.author.pk, entity.pk, @@ -177,15 +155,11 @@ def update_stats_after_unflag( cls, user_id: str, entity_id: str, has_no_historical_flags: bool, **kwargs: Any ) -> None: """Update the stats for the course after unflagging an entity.""" - entity = cls._get_entity_from_type( - entity_id, entity_type=kwargs.get("entity_type", "") - ) + entity = cls._get_entity_from_type(entity_id, entity_type=kwargs.get("entity_type", "")) if not entity: raise ObjectDoesNotExist - first_historical_flag = ( - has_no_historical_flags and not entity.historical_abuse_flaggers - ) + first_historical_flag = has_no_historical_flags and not entity.historical_abuse_flaggers if first_historical_flag: cls.update_stats_for_course(user_id, entity.course_id, inactive_flags=1) @@ -211,9 +185,7 @@ def update_vote( :return: True if the vote was successfully updated, False otherwise. """ user = User.objects.get(pk=user_id) - content = cls._get_entity_from_type( - content_id, entity_type=kwargs.get("entity_type", "") - ) + content = cls._get_entity_from_type(content_id, entity_type=kwargs.get("entity_type", "")) if not content: raise ValueError("Entity doesn't exist.") @@ -255,9 +227,7 @@ def upvote_content(cls, entity_id: str, user_id: str, **kwargs: Any) -> bool: Returns: bool: True if the vote was successfully updated, False otherwise. """ - return cls.update_vote( - entity_id, user_id, vote_type="up", entity_type=kwargs.get("entity_type") - ) + return cls.update_vote(entity_id, user_id, vote_type="up", entity_type=kwargs.get("entity_type")) @classmethod def downvote_content(cls, entity_id: str, user_id: str, **kwargs: Any) -> bool: @@ -271,9 +241,7 @@ def downvote_content(cls, entity_id: str, user_id: str, **kwargs: Any) -> bool: Returns: bool: True if the vote was successfully updated, False otherwise. """ - return cls.update_vote( - entity_id, user_id, vote_type="down", entity_type=kwargs.get("entity_type") - ) + return cls.update_vote(entity_id, user_id, vote_type="down", entity_type=kwargs.get("entity_type")) @classmethod def remove_vote(cls, entity_id: str, user_id: str, **kwargs: Any) -> bool: @@ -287,14 +255,10 @@ def remove_vote(cls, entity_id: str, user_id: str, **kwargs: Any) -> bool: Returns: bool: True if the vote was successfully removed, False otherwise. """ - return cls.update_vote( - entity_id, user_id, is_deleted=True, entity_type=kwargs.get("entity_type") - ) + return cls.update_vote(entity_id, user_id, is_deleted=True, entity_type=kwargs.get("entity_type")) @staticmethod - def validate_thread_and_user( - user_id: str, thread_id: str - ) -> tuple[dict[str, Any], dict[str, Any]]: + def validate_thread_and_user(user_id: str, thread_id: str) -> tuple[dict[str, Any], dict[str, Any]]: """ Validate thread and user. @@ -310,14 +274,10 @@ def validate_thread_and_user( """ try: # Optimize: Use select_related to avoid N+1 queries - thread = CommentThread.objects.select_related("author", "closed_by").get( - pk=int(thread_id) - ) + thread = CommentThread.objects.select_related("author", "closed_by").get(pk=int(thread_id)) user = ( ForumUser.objects.select_related("user") - .prefetch_related( - "user__course_stats", "user__read_states__last_read_times" - ) + .prefetch_related("user__course_stats", "user__read_states__last_read_times") .get(user__pk=user_id) ) except ObjectDoesNotExist as exc: @@ -361,14 +321,10 @@ def get_pinned_unpinned_thread_serialized_data( # Optimize: Use select_related to avoid N+1 queries user = ( ForumUser.objects.select_related("user") - .prefetch_related( - "user__course_stats", "user__read_states__last_read_times" - ) + .prefetch_related("user__course_stats", "user__read_states__last_read_times") .get(user__pk=user_id) ) - updated_thread = CommentThread.objects.select_related( - "author", "closed_by" - ).get(pk=thread_id) + updated_thread = CommentThread.objects.select_related("author", "closed_by").get(pk=thread_id) user_data = user.to_dict() context = { "user_id": user_data["_id"], @@ -405,9 +361,7 @@ def handle_pin_unpin_thread_request( """ user, _ = cls.validate_thread_and_user(user_id, thread_id) cls.pin_unpin_thread(thread_id, action) - return cls.get_pinned_unpinned_thread_serialized_data( - user["_id"], thread_id, serializer_class - ) + return cls.get_pinned_unpinned_thread_serialized_data(user["_id"], thread_id, serializer_class) @staticmethod def get_abuse_flagged_count(thread_ids: list[str]) -> dict[str, int]: @@ -424,9 +378,7 @@ def get_abuse_flagged_count(thread_ids: list[str]) -> dict[str, int]: comment_content_type = ContentType.objects.get_for_model(Comment) # Get all comments for these threads - comment_ids = Comment.objects.filter( - comment_thread__pk__in=thread_ids - ).values_list("pk", flat=True) + comment_ids = Comment.objects.filter(comment_thread__pk__in=thread_ids).values_list("pk", flat=True) if not comment_ids: return {} @@ -442,11 +394,7 @@ def get_abuse_flagged_count(thread_ids: list[str]) -> dict[str, int]: ) # Map comment IDs back to thread IDs - comment_to_thread = dict( - Comment.objects.filter(pk__in=comment_ids).values_list( - "pk", "comment_thread_id" - ) - ) + comment_to_thread = dict(Comment.objects.filter(pk__in=comment_ids).values_list("pk", "comment_thread_id")) result: dict[str, int] = {} for item in abuse_flagged_counts: @@ -459,9 +407,7 @@ def get_abuse_flagged_count(thread_ids: list[str]) -> dict[str, int]: return result @staticmethod - def get_read_states( - thread_ids: list[str], user_id: str, course_id: str - ) -> dict[str, list[Any]]: + def get_read_states(thread_ids: list[str], user_id: str, course_id: str) -> dict[str, list[Any]]: """ Retrieves the read state and unread comment count for each thread in the provided list. @@ -488,18 +434,16 @@ def get_read_states( except (ValueError, TypeError): return read_states - threads = CommentThread.objects.filter(pk__in=thread_ids_int).values( - "pk", "last_activity_at" - ) + threads = CommentThread.objects.filter(pk__in=thread_ids_int).values("pk", "last_activity_at") thread_dict = {thread["pk"]: thread for thread in threads} read_state = ReadState.objects.filter(user=user, course_id=course_id).first() if not read_state: return read_states - last_read_times = read_state.last_read_times.select_related( - "comment_thread" - ).filter(comment_thread_id__in=thread_ids_int) + last_read_times = read_state.last_read_times.select_related("comment_thread").filter( + comment_thread_id__in=thread_ids_int + ) for read_date in last_read_times: thread_id = read_date.comment_thread.pk @@ -512,9 +456,7 @@ def get_read_states( # Count unread comments for this thread unread_comment_count = ( - Comment.objects.filter( - comment_thread_id=thread_id, created_at__gte=read_date.timestamp - ) + Comment.objects.filter(comment_thread_id=thread_id, created_at__gte=read_date.timestamp) .exclude(author__pk=user_id) .count() ) @@ -523,9 +465,7 @@ def get_read_states( return read_states @staticmethod - def get_filtered_thread_ids( - thread_ids: list[str], context: str, group_ids: list[str] - ) -> set[str]: + def get_filtered_thread_ids(thread_ids: list[str], context: str, group_ids: list[str]) -> set[str]: """ Filters thread IDs based on context and group ID criteria. @@ -537,10 +477,8 @@ def get_filtered_thread_ids( Returns: set: A set of filtered thread IDs based on the context and group ID criteria. """ - context_threads = CommentThread.objects.filter( - pk__in=thread_ids, context=context - ) - context_thread_ids = set(thread.pk for thread in context_threads) + context_threads = CommentThread.objects.filter(pk__in=thread_ids, context=context) + context_thread_ids = {thread.pk for thread in context_threads} if not group_ids: return context_thread_ids @@ -549,7 +487,7 @@ def get_filtered_thread_ids( Q(group_id__in=group_ids) | Q(group_id__isnull=True), id__in=thread_ids, ) - group_thread_ids = set(thread.pk for thread in group_threads) + group_thread_ids = {thread.pk for thread in group_threads} return context_thread_ids.union(group_thread_ids) @@ -574,9 +512,7 @@ def get_endorsed(thread_ids: list[str]) -> dict[str, bool]: return {str(thread_id): True for thread_id in endorsed_thread_ids} @staticmethod - def get_user_read_state_by_course_id( - user_id: str, course_id: str - ) -> dict[str, Any]: + def get_user_read_state_by_course_id(user_id: str, course_id: str) -> dict[str, Any]: """ Retrieves the user's read state for a specific course. @@ -637,8 +573,8 @@ def handle_threads_query( user_id: str, course_id: str, group_ids: list[int], - author_id: Optional[str], - thread_type: Optional[str], + author_id: str | None, + thread_type: str | None, filter_flagged: bool, filter_unread: bool, filter_unanswered: bool, @@ -649,7 +585,7 @@ def handle_threads_query( per_page: int, context: str = "course", raw_query: bool = False, - commentable_ids: Optional[list[str]] = None, + commentable_ids: list[str] | None = None, is_moderator: bool = False, ) -> dict[str, Any]: """ @@ -695,23 +631,17 @@ def handle_threads_query( except User.DoesNotExist as exc: raise ValueError("User does not exist") from exc # Base query - base_query = CommentThread.objects.filter( - pk__in=mysql_comment_thread_ids, context=context - ) + base_query = CommentThread.objects.filter(pk__in=mysql_comment_thread_ids, context=context) # Group filtering if group_ids: - base_query = base_query.filter( - Q(group_id__in=group_ids) | Q(group_id__isnull=True) - ) + base_query = base_query.filter(Q(group_id__in=group_ids) | Q(group_id__isnull=True)) # Author filtering if author_id: base_query = base_query.filter(author__pk=author_id) if user and int(author_id) != user.pk: - base_query = base_query.filter( - anonymous=False, anonymous_to_peers=False - ) + base_query = base_query.filter(anonymous=False, anonymous_to_peers=False) # Thread type filtering if thread_type: @@ -743,10 +673,7 @@ def handle_threads_query( ) base_query = base_query.filter( - pk__in=list( - set(mysql_comment_thread_ids) & set(flagged_comments) - | set(flagged_threads) - ) + pk__in=list(set(mysql_comment_thread_ids) & set(flagged_comments) | set(flagged_threads)) ) # Unanswered questions filtering @@ -762,9 +689,7 @@ def handle_threads_query( # Unresponded threads filtering if filter_unresponded: - base_query = base_query.annotate(num_comments=Count("comment")).filter( - num_comments=0 - ) + base_query = base_query.annotate(num_comments=Count("comment")).filter(num_comments=0) # filter by topics: if commentable_ids are provided, commentable_id is basically topic id # For moderators: show all topics (no filtering by commentable_ids) # For learners: apply commentable_ids filtering (cohorted topics shown as archived) @@ -782,9 +707,7 @@ def handle_threads_query( # Only annotate votes_point if sorting by votes to avoid performance issues # Otherwise calculate votes separately in bulk if sort_key == "votes": - comment_thread_content_type = ContentType.objects.get_for_model( - CommentThread - ) + comment_thread_content_type = ContentType.objects.get_for_model(CommentThread) base_query = base_query.annotate( votes_point=Coalesce( Subquery( @@ -803,18 +726,12 @@ def handle_threads_query( base_query = base_query.select_related("author", "closed_by") - comment_threads = ( - base_query.order_by(*sort_criteria) if sort_criteria else base_query - ) + comment_threads = base_query.order_by(*sort_criteria) if sort_criteria else base_query thread_count = base_query.count() if raw_query: comment_threads = comment_threads.prefetch_related("comment_set") - return { - "result": [ - comment_thread.to_dict() for comment_thread in comment_threads - ] - } + return {"result": [comment_thread.to_dict() for comment_thread in comment_threads]} if filter_unread and user: read_state = cls.get_user_read_state_by_course_id(str(user.pk), course_id) @@ -828,10 +745,7 @@ def handle_threads_query( # Note: iterator() doesn't support prefetch_related, so we don't use it here for thread in comment_threads.iterator(): thread_key = str(thread.pk) - if ( - thread_key not in read_dates - or read_dates[thread_key] < thread.last_activity_at - ): + if thread_key not in read_dates or read_dates[thread_key] < thread.last_activity_at: if skipped >= to_skip: if len(threads) == per_page: has_more = True @@ -854,9 +768,7 @@ def handle_threads_query( if len(threads) == 0: collection = [] else: - collection = cls.threads_presentor( - threads, user_id, course_id, count_flagged - ) + collection = cls.threads_presentor(threads, user_id, course_id, count_flagged) return { "collection": collection, @@ -887,9 +799,7 @@ def prepare_thread( dict[str, Any]: A dictionary representing the prepared thread data. """ # Optimize: Use select_related to avoid N+1 queries - thread = CommentThread.objects.select_related("author", "closed_by").get( - pk=thread_id - ) + thread = CommentThread.objects.select_related("author", "closed_by").get(pk=thread_id) return { **thread.to_dict(), "type": "thread", @@ -920,26 +830,20 @@ def threads_presentor( list[dict[str, Any]]: A list of prepared thread data. """ - threads = CommentThread.objects.filter(pk__in=thread_ids).select_related( - "author", "closed_by" - ) + threads = CommentThread.objects.filter(pk__in=thread_ids).select_related("author", "closed_by") threads_dict = {thread.pk: thread for thread in threads} read_states = cls.get_read_states(thread_ids, user_id, course_id) threads_endorsed = cls.get_endorsed(thread_ids) - threads_flagged = ( - cls.get_abuse_flagged_count(thread_ids) if count_flagged else {} - ) + threads_flagged = cls.get_abuse_flagged_count(thread_ids) if count_flagged else {} presenters = [] for thread_id in thread_ids: thread = threads_dict.get(int(thread_id)) if not thread: continue - is_read, unread_count = read_states.get( - str(thread.pk), (False, thread.comment_count) - ) + is_read, unread_count = read_states.get(str(thread.pk), (False, thread.comment_count)) is_endorsed = threads_endorsed.get(thread.pk, False) abuse_flagged_count = threads_flagged.get(str(thread.pk), 0) presenters.append( @@ -955,7 +859,7 @@ def threads_presentor( return presenters @staticmethod - def get_username_from_id(user_id: str) -> Optional[str]: + def get_username_from_id(user_id: str) -> str | None: """ Retrieve the username associated with a given user ID. @@ -997,9 +901,7 @@ def validate_object(model: str, obj_id: str) -> Any: return instance.to_dict() @staticmethod - def find_subscribed_threads( - user_id: str, course_id: Optional[str] = None - ) -> list[str]: + def find_subscribed_threads(user_id: str, course_id: str | None = None) -> list[str]: """ Find threads that a user is subscribed to in a specific course. @@ -1014,9 +916,7 @@ def find_subscribed_threads( subscriber__pk=user_id, source_content_type=ContentType.objects.get_for_model(CommentThread), ) - thread_ids = [ - str(subscription.source_object_id) for subscription in subscriptions - ] + thread_ids = [str(subscription.source_object_id) for subscription in subscriptions] if course_id: thread_ids = list( CommentThread.objects.filter( @@ -1028,9 +928,7 @@ def find_subscribed_threads( return thread_ids @classmethod - def subscribe_user( - cls, user_id: str, source_id: str, source_type: str - ) -> dict[str, Any] | None: + def subscribe_user(cls, user_id: str, source_id: str, source_type: str) -> dict[str, Any] | None: """Subscribe a user to a source.""" source = cls._get_entity_from_type(source_id, source_type) if source is None: @@ -1044,9 +942,7 @@ def subscribe_user( return subscription.to_dict() @classmethod - def unsubscribe_user( - cls, user_id: str, source_id: str, source_type: Optional[str] = "" - ) -> None: + def unsubscribe_user(cls, user_id: str, source_id: str, source_type: str | None = "") -> None: """Unsubscribe a user from a source.""" source = cls._get_entity_from_type(source_id, source_type or "") if source is None: @@ -1076,9 +972,7 @@ def delete_subscriptions_of_a_thread(cls, thread_id: str) -> None: ).delete() @staticmethod - def validate_params( - params: dict[str, Any], user_id: Optional[str] = None - ) -> Response | None: + def validate_params(params: dict[str, Any], user_id: str | None = None) -> Response | None: """ Validate the request parameters. @@ -1170,9 +1064,7 @@ def get_threads( } if user_id: context["user_id"] = user_id - serializer = serializer( - threads.pop("collection"), many=True, context=context, backend=cls - ) + serializer = serializer(threads.pop("collection"), many=True, context=context, backend=cls) threads["collection"] = serializer.data return threads @@ -1183,9 +1075,9 @@ def get_user_voted_ids(cls, user_id: str, vote: str) -> list[str]: raise ValueError("Invalid vote type") vote_value = 1 if vote == "up" else -1 - voted_ids = UserVote.objects.filter( - user__pk=user_id, vote=vote_value - ).values_list("content_object_id", flat=True) + voted_ids = UserVote.objects.filter(user__pk=user_id, vote=vote_value).values_list( + "content_object_id", flat=True + ) return list(voted_ids) @staticmethod @@ -1198,9 +1090,7 @@ def filter_standalone_threads(comment_ids: list[str]) -> list[str]: ) @classmethod - def user_to_hash( - cls, user_id: str, params: Optional[dict[str, Any]] = None - ) -> dict[str, Any]: + def user_to_hash(cls, user_id: str, params: dict[str, Any] | None = None) -> dict[str, Any]: """ Converts user data to a hash """ @@ -1245,9 +1135,7 @@ def user_to_hash( ) comment_ids = list(comments.values_list("pk", flat=True)) if params.get("group_ids"): - group_threads = threads.filter( - group_id__in=params["group_ids"] + [None] - ) + group_threads = threads.filter(group_id__in=params["group_ids"] + [None]) group_thread_ids = [str(thread.pk) for thread in group_threads] threads_count = len(group_thread_ids) comment_thread_ids = cls.filter_standalone_threads(comment_ids) @@ -1255,13 +1143,9 @@ def user_to_hash( group_comment_threads = CommentThread.objects.filter( id__in=comment_thread_ids, group_id__in=params["group_ids"] + [None] ) - group_comment_thread_ids = [ - str(thread.pk) for thread in group_comment_threads - ] + group_comment_thread_ids = [str(thread.pk) for thread in group_comment_threads] comments_count = sum( - 1 - for comment_thread_id in comment_thread_ids - if comment_thread_id in group_comment_thread_ids + 1 for comment_thread_id in comment_thread_ids if comment_thread_id in group_comment_thread_ids ) else: thread_ids = [str(thread.pk) for thread in threads] @@ -1295,9 +1179,7 @@ def unsubscribe_all(user_id: str) -> None: # Kept method signature same as mongo implementation @staticmethod - def retire_all_content( - user_id: str, username: str - ) -> None: # pylint: disable=W0613 + def retire_all_content(user_id: str, username: str) -> None: # pylint: disable=W0613 """Retire all content from user.""" comments = Comment.objects.filter(author__pk=user_id) for comment in comments: @@ -1323,9 +1205,7 @@ def find_or_create_read_state(user_id: str, thread_id: str) -> dict[str, Any]: except (User.DoesNotExist, CommentThread.DoesNotExist) as exc: raise ObjectDoesNotExist from exc - read_state, _ = ReadState.objects.get_or_create( - user=user, course_id=thread.course_id - ) + read_state, _ = ReadState.objects.get_or_create(user=user, course_id=thread.course_id) return read_state.to_dict() @classmethod @@ -1333,9 +1213,7 @@ def mark_as_read(cls, user_id: str, thread_id: str) -> None: """Mark thread as read.""" user = User.objects.get(pk=user_id) thread = CommentThread.objects.get(pk=thread_id) - read_state, _ = ReadState.objects.get_or_create( - user=user, course_id=thread.course_id - ) + read_state, _ = ReadState.objects.get_or_create(user=user, course_id=thread.course_id) LastReadTime.objects.update_or_create( read_state=read_state, @@ -1403,9 +1281,7 @@ def build_course_stats(cls, author_id: str, course_id: str) -> None: threads_ids = [thread.pk for thread in threads] active_flags_comments = ( - AbuseFlagger.objects.filter( - content_object_id__in=comment_ids, content_type=Comment().content_type - ) + AbuseFlagger.objects.filter(content_object_id__in=comment_ids, content_type=Comment().content_type) .values("content_object_id") .annotate(count=Count("content_object_id")) .count() @@ -1505,8 +1381,8 @@ def generate_id() -> str: @staticmethod def find_or_create_user( user_id: str, - username: Optional[str] = None, - default_sort_key: Optional[str] = "date", + username: str | None = None, + default_sort_key: str | None = "date", ) -> str: """Find or create user.""" username = username or user_id @@ -1520,9 +1396,7 @@ def find_or_create_user( raise ValueError(f"User with username {username} already exists") user = User.objects.create(pk=int(user_id), username=username) - forum_user, _ = ForumUser.objects.get_or_create( - user=user, defaults={"default_sort_key": default_sort_key} - ) + forum_user, _ = ForumUser.objects.get_or_create(user=user, defaults={"default_sort_key": default_sort_key}) return forum_user.user.pk @staticmethod @@ -1558,9 +1432,7 @@ def update_child_count_in_parent_comment(parent_id: str, count: int) -> None: Returns: None. """ - Comment.objects.filter(pk=int(parent_id)).update( - child_count=F("child_count") + count - ) + Comment.objects.filter(pk=int(parent_id)).update(child_count=F("child_count") + count) @classmethod def create_comment(cls, data: dict[str, Any]) -> str: @@ -1595,9 +1467,7 @@ def create_comment(cls, data: dict[str, Any]) -> str: cls.update_child_count_in_parent_comment(data["parent_id"], 1) cls.update_stats_for_course(data["author_id"], data["course_id"], replies=1) else: - cls.update_stats_for_course( - data["author_id"], data["course_id"], responses=1 - ) + cls.update_stats_for_course(data["author_id"], data["course_id"], responses=1) return str(new_comment.pk) @classmethod @@ -1657,9 +1527,7 @@ def update_comment(comment_id: str, **kwargs: Any) -> int: if kwargs.get("anonymous_to_peers"): comment.anonymous_to_peers = kwargs["anonymous_to_peers"] if kwargs.get("comment_thread_id"): - comment.comment_thread = CommentThread.objects.get( - pk=kwargs["comment_thread_id"] - ) + comment.comment_thread = CommentThread.objects.get(pk=kwargs["comment_thread_id"]) if kwargs.get("visible"): comment.visible = kwargs["visible"] if kwargs.get("author_id"): @@ -1686,9 +1554,7 @@ def update_comment(comment_id: str, **kwargs: Any) -> int: ).values_list("user_id", flat=True) new_abuse_flaggers = [ - int(user_id) - for user_id in kwargs["abuse_flaggers"] - if int(user_id) not in existing_abuse_flaggers + int(user_id) for user_id in kwargs["abuse_flaggers"] if int(user_id) not in existing_abuse_flaggers ] for user_id in new_abuse_flaggers: @@ -1773,9 +1639,9 @@ def update_comment(comment_id: str, **kwargs: Any) -> int: def get_thread_id_from_comment(comment_id: str) -> dict[str, Any] | None: """Return thread_id from comment_id.""" # Optimize: Use select_related to avoid N+1 queries - comment = Comment.objects.select_related( - "comment_thread__author", "comment_thread__closed_by" - ).get(pk=comment_id) + comment = Comment.objects.select_related("comment_thread__author", "comment_thread__closed_by").get( + pk=comment_id + ) if comment.comment_thread: return comment.comment_thread.to_dict() raise ValueError("Comment doesn't have the thread.") @@ -1801,13 +1667,9 @@ def get_thread(thread_id: str) -> dict[str, Any] | None: return thread.to_dict() @classmethod - def get_subscription( - cls, subscriber_id: str, source_id: str, **kwargs: Any - ) -> dict[str, Any] | None: + def get_subscription(cls, subscriber_id: str, source_id: str, **kwargs: Any) -> dict[str, Any] | None: """Return subscription from subscriber_id and source_id.""" - source = cls._get_entity_from_type( - source_id, entity_type=kwargs.get("source_type", "") - ) + source = cls._get_entity_from_type(source_id, entity_type=kwargs.get("source_type", "")) if not source: return None try: @@ -1823,9 +1685,7 @@ def get_subscription( @classmethod def get_subscriptions(cls, query: dict[str, Any]) -> list[dict[str, Any]]: """Return subscriptions from filter.""" - source = cls._get_entity_from_type( - entity_id=query["source_id"], entity_type=query.get("source_type", "") - ) + source = cls._get_entity_from_type(entity_id=query["source_id"], entity_type=query.get("source_type", "")) if not source: return [] @@ -1918,9 +1778,7 @@ def update_thread( ).values_list("user_id", flat=True) new_abuse_flaggers = [ - int(user_id) - for user_id in kwargs["abuse_flaggers"] - if int(user_id) not in existing_abuse_flaggers + int(user_id) for user_id in kwargs["abuse_flaggers"] if int(user_id) not in existing_abuse_flaggers ] for user_id in new_abuse_flaggers: @@ -1991,9 +1849,7 @@ def get_user_thread_filter(course_id: str) -> dict[str, Any]: return {"course_id": course_id} @staticmethod - def get_filtered_threads( - query: dict[str, Any], ids_only: bool = False - ) -> list[dict[str, Any]]: + def get_filtered_threads(query: dict[str, Any], ids_only: bool = False) -> list[dict[str, Any]]: """Return a list of threads that match the given filter.""" threads = CommentThread.objects.filter(**query) if ids_only: @@ -2057,16 +1913,16 @@ def get_thread_id_by_comment_id(parent_comment_id: str) -> str: @staticmethod def update_comment_and_get_updated_comment( comment_id: str, - body: Optional[str] = None, - course_id: Optional[str] = None, - user_id: Optional[str] = None, - anonymous: Optional[bool] = False, - anonymous_to_peers: Optional[bool] = False, - endorsed: Optional[bool] = None, - closed: Optional[bool] = False, - editing_user_id: Optional[str] = None, - edit_reason_code: Optional[str] = None, - endorsement_user_id: Optional[str] = None, + body: str | None = None, + course_id: str | None = None, + user_id: str | None = None, + anonymous: bool | None = False, + anonymous_to_peers: bool | None = False, + endorsed: bool | None = None, + closed: bool | None = False, + editing_user_id: str | None = None, + edit_reason_code: str | None = None, + endorsement_user_id: str | None = None, ) -> dict[str, Any] | None: """ Update an existing child/parent comment. @@ -2199,10 +2055,7 @@ def get_paginated_user_stats( """Get paginated user stats.""" users = ( - User.objects.filter( - Q(course_stats__course_id=course_id) - & Q(course_stats__course_id__isnull=False) - ) + User.objects.filter(Q(course_stats__course_id=course_id) & Q(course_stats__course_id__isnull=False)) .select_related("forum") .prefetch_related("course_stats", "read_states__last_read_times") .order_by( @@ -2219,16 +2072,10 @@ def get_paginated_user_stats( fu.user.pk: fu for fu in ForumUser.objects.filter(user__pk__in=user_ids) .select_related("user") - .prefetch_related( - "user__course_stats", "user__read_states__last_read_times" - ) + .prefetch_related("user__course_stats", "user__read_states__last_read_times") } - forum_users = [ - forum_users_dict[user_id] - for user_id in user_ids - if user_id in forum_users_dict - ] + forum_users = [forum_users_dict[user_id] for user_id in user_ids if user_id in forum_users_dict] return { "pagination": [{"total_count": paginator.count}], "data": [user.to_dict(course_id=course_id) for user in forum_users], @@ -2245,12 +2092,8 @@ def get_contents(**kwargs: Any) -> list[dict[str, Any]]: Returns: A list of comments and comment threads. """ - comment_filters = { - key: value for key, value in kwargs.items() if hasattr(Comment, key) - } - thread_filters = { - key: value for key, value in kwargs.items() if hasattr(CommentThread, key) - } + comment_filters = {key: value for key, value in kwargs.items() if hasattr(Comment, key)} + thread_filters = {key: value for key, value in kwargs.items() if hasattr(CommentThread, key)} comments = Comment.objects.filter(**comment_filters) threads = CommentThread.objects.filter(**thread_filters) @@ -2264,7 +2107,7 @@ def get_contents(**kwargs: Any) -> list[dict[str, Any]]: return result @staticmethod - def find_thread(**kwargs: Any) -> Optional[dict[str, Any]]: + def find_thread(**kwargs: Any) -> dict[str, Any] | None: """ Retrieves a first matching thread from the database. """ @@ -2274,7 +2117,7 @@ def find_thread(**kwargs: Any) -> Optional[dict[str, Any]]: @staticmethod def find_comment( is_parent_comment: bool = True, with_abuse_flaggers: bool = False, **kwargs: Any - ) -> Optional[dict[str, Any]]: + ) -> dict[str, Any] | None: """ Retrieves a first matching thread from the database. """ @@ -2300,35 +2143,23 @@ def get_user_contents_by_username(username: str) -> list[dict[str, Any]]: """ Retrieve all threads and comments authored by a specific user. """ - contents = [ - comment.to_dict() - for comment in Comment.objects.filter(author__username=username) - ] + [ - thread.to_dict() - for thread in CommentThread.objects.filter(author__username=username) + contents = [comment.to_dict() for comment in Comment.objects.filter(author__username=username)] + [ + thread.to_dict() for thread in CommentThread.objects.filter(author__username=username) ] return contents @staticmethod def get_user_post_counts(user_id: str, course_id: str) -> dict[str, int]: """Return thread_count and comment_count for user in course.""" - thread_count = CommentThread.objects.filter( - author_id=user_id, course_id=course_id - ).count() - comment_count = Comment.objects.filter( - author_id=user_id, course_id=course_id - ).count() + thread_count = CommentThread.objects.filter(author_id=user_id, course_id=course_id).count() + comment_count = Comment.objects.filter(author_id=user_id, course_id=course_id).count() return {"thread_count": thread_count, "comment_count": comment_count} @staticmethod def delete_user_posts(user_id: str, course_id: str) -> dict[str, int]: """Delete all threads and comments by user in course. Returns counts before deletion.""" - thread_count = CommentThread.objects.filter( - author_id=user_id, course_id=course_id - ).count() - comment_count = Comment.objects.filter( - author_id=user_id, course_id=course_id - ).count() + thread_count = CommentThread.objects.filter(author_id=user_id, course_id=course_id).count() + comment_count = Comment.objects.filter(author_id=user_id, course_id=course_id).count() Comment.objects.filter(author_id=user_id, course_id=course_id).delete() CommentThread.objects.filter(author_id=user_id, course_id=course_id).delete() return {"thread_count": thread_count, "comment_count": comment_count} diff --git a/forum/backends/mysql/models.py b/forum/backends/mysql/models.py index 211d1ff5..26468fd7 100644 --- a/forum/backends/mysql/models.py +++ b/forum/backends/mysql/models.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime -from typing import Any, Optional +from typing import Any from django.contrib.auth.models import User # pylint: disable=E5142 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation @@ -22,14 +22,10 @@ class ForumUser(models.Model): class Meta: app_label = "forum" - user: models.OneToOneField[User, User] = models.OneToOneField( - User, related_name="forum", on_delete=models.CASCADE - ) - default_sort_key: models.CharField[str, str] = models.CharField( - max_length=25, default="date" - ) + user: models.OneToOneField[User, User] = models.OneToOneField(User, related_name="forum", on_delete=models.CASCADE) + default_sort_key: models.CharField[str, str] = models.CharField(max_length=25, default="date") - def to_dict(self, course_id: Optional[str] = None) -> dict[str, Any]: + def to_dict(self, course_id: str | None = None) -> dict[str, Any]: """Return a dictionary representation of the model.""" course_stats = CourseStat.objects.filter(user=self.user) read_states = ReadState.objects.filter(user=self.user) @@ -45,11 +41,7 @@ def to_dict(self, course_id: Optional[str] = None) -> dict[str, Any]: "external_id": self.user.pk, "username": self.user.username, "email": self.user.email, - "course_stats": ( - course_stat.to_dict() - if course_stat - else [stat.to_dict() for stat in course_stats] - ), + "course_stats": (course_stat.to_dict() if course_stat else [stat.to_dict() for stat in course_stats]), "read_states": [state.to_dict() for state in read_states], } @@ -63,12 +55,10 @@ class CourseStat(models.Model): threads: models.IntegerField[int, int] = models.IntegerField(default=0) responses: models.IntegerField[int, int] = models.IntegerField(default=0) replies: models.IntegerField[int, int] = models.IntegerField(default=0) - last_activity_at: models.DateTimeField[Optional[datetime], datetime] = ( - models.DateTimeField(default=None, null=True, blank=True) - ) - user: models.ForeignKey[User, User] = models.ForeignKey( - User, related_name="course_stats", on_delete=models.CASCADE + last_activity_at: models.DateTimeField[datetime | None, datetime] = models.DateTimeField( + default=None, null=True, blank=True ) + user: models.ForeignKey[User, User] = models.ForeignKey(User, related_name="course_stats", on_delete=models.CASCADE) def to_dict(self) -> dict[str, Any]: """Return a dictionary representation of the model.""" @@ -93,16 +83,14 @@ class Content(models.Model): index_name = "" - author: models.ForeignKey[User, User] = models.ForeignKey( - User, on_delete=models.CASCADE - ) - author_username: models.CharField[Optional[str], str] = models.CharField( + author: models.ForeignKey[User, User] = models.ForeignKey(User, on_delete=models.CASCADE) + author_username: models.CharField[str | None, str] = models.CharField( max_length=255, null=True, blank=True, help_text="Username at time of posting, preserved for historical accuracy", ) - retired_username: models.CharField[Optional[str], str] = models.CharField( + retired_username: models.CharField[str | None, str] = models.CharField( max_length=255, null=True, blank=True, @@ -113,18 +101,10 @@ class Content(models.Model): visible: models.BooleanField[bool, bool] = models.BooleanField(default=True) endorsed: models.BooleanField[bool, bool] = models.BooleanField(default=False) anonymous: models.BooleanField[bool, bool] = models.BooleanField(default=False) - anonymous_to_peers: models.BooleanField[bool, bool] = models.BooleanField( - default=False - ) - group_id: models.PositiveIntegerField[int, int] = models.PositiveIntegerField( - null=True - ) - created_at: models.DateTimeField[datetime, datetime] = models.DateTimeField( - auto_now_add=True - ) - updated_at: models.DateTimeField[datetime, datetime] = models.DateTimeField( - auto_now=True - ) + anonymous_to_peers: models.BooleanField[bool, bool] = models.BooleanField(default=False) + group_id: models.PositiveIntegerField[int, int] = models.PositiveIntegerField(null=True) + created_at: models.DateTimeField[datetime, datetime] = models.DateTimeField(auto_now_add=True) + updated_at: models.DateTimeField[datetime, datetime] = models.DateTimeField(auto_now=True) uservote = GenericRelation( "UserVote", object_id_field="content_object_id", @@ -145,9 +125,9 @@ def content_type(self) -> ContentType: def abuse_flaggers(self) -> list[int]: """Return a list of users who have flagged the content for abuse.""" return list( - AbuseFlagger.objects.filter( - content_object_id=self.pk, content_type=self.content_type - ).values_list("user_id", flat=True) + AbuseFlagger.objects.filter(content_object_id=self.pk, content_type=self.content_type).values_list( + "user_id", flat=True + ) ) @property @@ -162,9 +142,7 @@ def historical_abuse_flaggers(self) -> list[int]: @property def edit_history(self) -> QuerySet[EditHistory]: """Return a list of edit history for the content.""" - return EditHistory.objects.filter( - content_object_id=self.pk, content_type=self.content_type - ) + return EditHistory.objects.filter(content_object_id=self.pk, content_type=self.content_type) @property def votes(self) -> models.QuerySet[UserVote]: @@ -234,19 +212,11 @@ class CommentThread(Content): thread_type: models.CharField[str, str] = models.CharField( max_length=50, choices=THREAD_TYPE_CHOICES, default="discussion" ) - context: models.CharField[str, str] = models.CharField( - max_length=50, choices=CONTEXT_CHOICES, default="course" - ) + context: models.CharField[str, str] = models.CharField(max_length=50, choices=CONTEXT_CHOICES, default="course") closed: models.BooleanField[bool, bool] = models.BooleanField(default=False) - pinned: models.BooleanField[Optional[bool], bool] = models.BooleanField( - default=False - ) - last_activity_at: models.DateTimeField[Optional[datetime], datetime] = ( - models.DateTimeField(null=True, blank=True) - ) - close_reason_code: models.CharField[Optional[str], str] = models.CharField( - max_length=255, null=True, blank=True - ) + pinned: models.BooleanField[bool | None, bool] = models.BooleanField(default=False) + last_activity_at: models.DateTimeField[datetime | None, datetime] = models.DateTimeField(null=True, blank=True) + close_reason_code: models.CharField[str | None, str] = models.CharField(max_length=255, null=True, blank=True) closed_by: models.ForeignKey[User, User] = models.ForeignKey( User, related_name="threads_closed", @@ -291,9 +261,7 @@ def to_dict(self) -> dict[str, Any]: "votes": self.get_votes, "visible": self.visible, "abuse_flaggers": [str(flagger) for flagger in self.abuse_flaggers], - "historical_abuse_flaggers": [ - str(flagger) for flagger in self.historical_abuse_flaggers - ], + "historical_abuse_flaggers": [str(flagger) for flagger in self.historical_abuse_flaggers], "thread_type": self.thread_type, "_type": "CommentThread", "commentable_id": self.commentable_id, @@ -310,9 +278,7 @@ def to_dict(self) -> dict[str, Any]: "closed_by_id": str(self.closed_by.pk) if self.closed_by else None, "close_reason_code": self.close_reason_code, "author_id": str(self.author.pk), - "author_username": self.author_username - or self.retired_username - or self.author.username, + "author_username": self.author_username or self.retired_username or self.author.username, "updated_at": self.updated_at, "created_at": self.created_at, "last_activity_at": self.last_activity_at, @@ -330,9 +296,7 @@ def doc_to_hash(self) -> dict[str, Any]: "body": self.body, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, - "last_activity_at": ( - self.last_activity_at.isoformat() if self.last_activity_at else None - ), + "last_activity_at": (self.last_activity_at.isoformat() if self.last_activity_at else None), "comment_count": self.comment_count, "votes_point": self.get_votes.get("point"), "context": self.context, @@ -350,9 +314,7 @@ class Meta: models.Index(fields=["author"]), models.Index(fields=["author", "course_id"]), models.Index(fields=["course_id", "anonymous", "anonymous_to_peers"]), - models.Index( - fields=["author", "course_id", "anonymous", "anonymous_to_peers"] - ), + models.Index(fields=["author", "course_id", "anonymous", "anonymous_to_peers"]), ] @@ -361,27 +323,17 @@ class Comment(Content): index_name = "comments" - endorsement: models.JSONField[dict[str, Any], dict[str, Any]] = models.JSONField( - default=dict - ) - sort_key: models.CharField[Optional[str], str] = models.CharField( - max_length=255, null=True, blank=True - ) - child_count: models.PositiveIntegerField[int, int] = models.PositiveIntegerField( - default=0 - ) - retired_username: models.CharField[Optional[str], str] = models.CharField( - max_length=255, null=True, blank=True - ) + endorsement: models.JSONField[dict[str, Any], dict[str, Any]] = models.JSONField(default=dict) + sort_key: models.CharField[str | None, str] = models.CharField(max_length=255, null=True, blank=True) + child_count: models.PositiveIntegerField[int, int] = models.PositiveIntegerField(default=0) + retired_username: models.CharField[str | None, str] = models.CharField(max_length=255, null=True, blank=True) comment_thread: models.ForeignKey[CommentThread, CommentThread] = models.ForeignKey( CommentThread, on_delete=models.CASCADE ) parent: models.ForeignKey[Comment, Comment] = models.ForeignKey( "self", on_delete=models.CASCADE, null=True, blank=True ) - depth: models.PositiveIntegerField[int, int] = models.PositiveIntegerField( - default=0 - ) + depth: models.PositiveIntegerField[int, int] = models.PositiveIntegerField(default=0) def get_sort_key(self) -> str: """Get the sort key for the comment""" @@ -407,9 +359,7 @@ def get_list(**kwargs: Any) -> list[dict[str, Any]]: result = [] if sort: if sort == 1: - result = sorted( - comments, key=lambda x: (x.sort_key is None, x.sort_key or "") - ) + result = sorted(comments, key=lambda x: (x.sort_key is None, x.sort_key or "")) elif sort == -1: result = sorted( comments, @@ -478,9 +428,7 @@ def to_dict(self) -> dict[str, Any]: "votes": self.get_votes, "visible": self.visible, "abuse_flaggers": [str(flagger) for flagger in self.abuse_flaggers], - "historical_abuse_flaggers": [ - str(flagger) for flagger in self.historical_abuse_flaggers - ], + "historical_abuse_flaggers": [str(flagger) for flagger in self.historical_abuse_flaggers], "parent_ids": self.get_parent_ids(), "parent_id": str(self.parent.pk) if self.parent else "None", "at_position_list": [], @@ -493,9 +441,7 @@ def to_dict(self) -> dict[str, Any]: "author_id": str(self.author.pk), "comment_thread_id": str(self.comment_thread.pk), "child_count": self.child_count, - "author_username": self.author_username - or self.retired_username - or self.author.username, + "author_username": self.author_username or self.retired_username or self.author.username, "sk": str(self.pk), "updated_at": self.updated_at, "created_at": self.created_at, @@ -535,9 +481,7 @@ class Meta: models.Index(fields=["comment_thread", "endorsed"]), models.Index(fields=["course_id", "parent", "endorsed"]), models.Index(fields=["course_id", "anonymous", "anonymous_to_peers"]), - models.Index( - fields=["author", "course_id", "anonymous", "anonymous_to_peers"] - ), + models.Index(fields=["author", "course_id", "anonymous", "anonymous_to_peers"]), ] @@ -555,25 +499,17 @@ class EditHistory(models.Model): ("violates-guidelines", _("Violates community guidelines")), ] - reason_code: models.CharField[Optional[str], str] = models.CharField( + reason_code: models.CharField[str | None, str] = models.CharField( max_length=100, blank=True, null=True, choices=DISCUSSION_MODERATION_EDIT_REASON_CODES, ) original_body: models.TextField[str, str] = models.TextField() - editor: models.ForeignKey[User, User] = models.ForeignKey( - User, on_delete=models.CASCADE - ) - created_at: models.DateTimeField[datetime, datetime] = models.DateTimeField( - auto_now_add=True - ) - content_type: models.ForeignKey[ContentType] = models.ForeignKey( - ContentType, on_delete=models.CASCADE - ) - content_object_id: models.PositiveIntegerField[int, int] = ( - models.PositiveIntegerField() - ) + editor: models.ForeignKey[User, User] = models.ForeignKey(User, on_delete=models.CASCADE) + created_at: models.DateTimeField[datetime, datetime] = models.DateTimeField(auto_now_add=True) + content_type: models.ForeignKey[ContentType] = models.ForeignKey(ContentType, on_delete=models.CASCADE) + content_object_id: models.PositiveIntegerField[int, int] = models.PositiveIntegerField() content: GenericForeignKey = GenericForeignKey("content_type", "content_object_id") class Meta: @@ -588,19 +524,11 @@ class Meta: class AbuseFlagger(models.Model): """Abuse flagger model class""" - content_type: models.ForeignKey[ContentType] = models.ForeignKey( - ContentType, on_delete=models.CASCADE - ) - content_object_id: models.PositiveIntegerField[int, int] = ( - models.PositiveIntegerField() - ) + content_type: models.ForeignKey[ContentType] = models.ForeignKey(ContentType, on_delete=models.CASCADE) + content_object_id: models.PositiveIntegerField[int, int] = models.PositiveIntegerField() content: GenericForeignKey = GenericForeignKey("content_type", "content_object_id") - user: models.ForeignKey[User, User] = models.ForeignKey( - User, on_delete=models.CASCADE - ) - flagged_at: models.DateTimeField[datetime, datetime] = models.DateTimeField( - default=timezone.now - ) + user: models.ForeignKey[User, User] = models.ForeignKey(User, on_delete=models.CASCADE) + flagged_at: models.DateTimeField[datetime, datetime] = models.DateTimeField(default=timezone.now) class Meta: app_label = "forum" @@ -614,19 +542,11 @@ class Meta: class HistoricalAbuseFlagger(models.Model): """Historical abuse flagger model class""" - content_type: models.ForeignKey[ContentType] = models.ForeignKey( - ContentType, on_delete=models.CASCADE - ) - content_object_id: models.PositiveIntegerField[int, int] = ( - models.PositiveIntegerField() - ) + content_type: models.ForeignKey[ContentType] = models.ForeignKey(ContentType, on_delete=models.CASCADE) + content_object_id: models.PositiveIntegerField[int, int] = models.PositiveIntegerField() content: GenericForeignKey = GenericForeignKey("content_type", "content_object_id") - user: models.ForeignKey[User, User] = models.ForeignKey( - User, on_delete=models.CASCADE - ) - flagged_at: models.DateTimeField[datetime, datetime] = models.DateTimeField( - default=timezone.now - ) + user: models.ForeignKey[User, User] = models.ForeignKey(User, on_delete=models.CASCADE) + flagged_at: models.DateTimeField[datetime, datetime] = models.DateTimeField(default=timezone.now) class Meta: app_label = "forum" @@ -641,18 +561,14 @@ class ReadState(models.Model): """Read state model.""" course_id: models.CharField[str, str] = models.CharField(max_length=255) - user: models.ForeignKey[User, User] = models.ForeignKey( - User, related_name="read_states", on_delete=models.CASCADE - ) + user: models.ForeignKey[User, User] = models.ForeignKey(User, related_name="read_states", on_delete=models.CASCADE) last_read_times: models.QuerySet[LastReadTime] def to_dict(self) -> dict[str, Any]: """Return a dictionary representation of the model.""" last_read_times = {} for last_read_time in self.last_read_times.all(): - last_read_times[str(last_read_time.comment_thread.pk)] = ( - last_read_time.timestamp - ) + last_read_times[str(last_read_time.comment_thread.pk)] = last_read_time.timestamp return { "_id": str(self.pk), "last_read_times": last_read_times, @@ -690,19 +606,11 @@ class Meta: class UserVote(models.Model): """User votes model class""" - user: models.ForeignKey[User, User] = models.ForeignKey( - User, on_delete=models.CASCADE - ) - content_type: models.ForeignKey[ContentType] = models.ForeignKey( - ContentType, on_delete=models.CASCADE - ) - content_object_id: models.PositiveIntegerField[int, int] = ( - models.PositiveIntegerField() - ) + user: models.ForeignKey[User, User] = models.ForeignKey(User, on_delete=models.CASCADE) + content_type: models.ForeignKey[ContentType] = models.ForeignKey(ContentType, on_delete=models.CASCADE) + content_object_id: models.PositiveIntegerField[int, int] = models.PositiveIntegerField() content: GenericForeignKey = GenericForeignKey("content_type", "content_object_id") - vote: models.IntegerField[int, int] = models.IntegerField( - validators=[validate_upvote_or_downvote] - ) + vote: models.IntegerField[int, int] = models.IntegerField(validators=[validate_upvote_or_downvote]) class Meta: app_label = "forum" @@ -717,24 +625,14 @@ class Meta: class Subscription(models.Model): """Subscription model class""" - subscriber: models.ForeignKey[User, User] = models.ForeignKey( - User, on_delete=models.CASCADE - ) - source_content_type: models.ForeignKey[ContentType, ContentType] = ( - models.ForeignKey(ContentType, on_delete=models.CASCADE) - ) - source_object_id: models.PositiveIntegerField[int, int] = ( - models.PositiveIntegerField() - ) - source: GenericForeignKey = GenericForeignKey( - "source_content_type", "source_object_id" - ) - created_at: models.DateTimeField[datetime, datetime] = models.DateTimeField( - auto_now_add=True - ) - updated_at: models.DateTimeField[datetime, datetime] = models.DateTimeField( - auto_now=True + subscriber: models.ForeignKey[User, User] = models.ForeignKey(User, on_delete=models.CASCADE) + source_content_type: models.ForeignKey[ContentType, ContentType] = models.ForeignKey( + ContentType, on_delete=models.CASCADE ) + source_object_id: models.PositiveIntegerField[int, int] = models.PositiveIntegerField() + source: GenericForeignKey = GenericForeignKey("source_content_type", "source_object_id") + created_at: models.DateTimeField[datetime, datetime] = models.DateTimeField(auto_now_add=True) + updated_at: models.DateTimeField[datetime, datetime] = models.DateTimeField(auto_now=True) def to_dict(self) -> dict[str, Any]: """Return a dictionary representation of the model.""" @@ -752,9 +650,7 @@ class Meta: unique_together = ("subscriber", "source_content_type", "source_object_id") indexes = [ models.Index(fields=["subscriber"]), - models.Index( - fields=["subscriber", "source_object_id", "source_content_type"] - ), + models.Index(fields=["subscriber", "source_object_id", "source_content_type"]), models.Index(fields=["subscriber", "source_content_type"]), models.Index(fields=["source_object_id", "source_content_type"]), ] @@ -763,12 +659,8 @@ class Meta: class MongoContent(models.Model): """MongoContent model class.""" - content_type: models.ForeignKey[ContentType] = models.ForeignKey( - ContentType, on_delete=models.CASCADE, null=True - ) - content_object_id: models.PositiveIntegerField[int, int] = ( - models.PositiveIntegerField(null=True) - ) + content_type: models.ForeignKey[ContentType] = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True) + content_object_id: models.PositiveIntegerField[int, int] = models.PositiveIntegerField(null=True) content: GenericForeignKey = GenericForeignKey("content_type", "content_object_id") mongo_id: models.CharField[str, str] = models.CharField(max_length=50, unique=True) diff --git a/forum/handlers.py b/forum/handlers.py index 2ef1578b..046c3ac9 100644 --- a/forum/handlers.py +++ b/forum/handlers.py @@ -4,12 +4,13 @@ import logging from typing import Any -from django.db.models.signals import post_save, post_delete + +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver +from forum.models import Comment, CommentThread from forum.search import get_document_search_backend from forum.utils import get_str_value_from_collection -from forum.models import Comment, CommentThread log = logging.getLogger(__name__) @@ -118,9 +119,7 @@ def handle_deletion(sender: Any, instance: Any, **kwargs: dict[str, Any]) -> Non @receiver(post_save, sender=CommentThread) @receiver(post_save, sender=Comment) -def handle_comment_thread_and_comment( - sender: Any, instance: Any, created: bool, **kwargs: dict[str, Any] -) -> None: +def handle_comment_thread_and_comment(sender: Any, instance: Any, created: bool, **kwargs: dict[str, Any]) -> None: """ Handle the insertion or update of a comment thread or comment in the MySQL database. diff --git a/forum/management/commands/delete_unused_forum_indices.py b/forum/management/commands/delete_unused_forum_indices.py index 613de964..6fdb1954 100644 --- a/forum/management/commands/delete_unused_forum_indices.py +++ b/forum/management/commands/delete_unused_forum_indices.py @@ -10,9 +10,7 @@ class Command(BaseCommand): Django management command for the deletion of unused search indices. """ - help = ( - "Delete all Elasticsearch indices that are not the latest for each model type." - ) + help = "Delete all Elasticsearch indices that are not the latest for each model type." def handle(self, *args: list[str], **kwargs: dict[str, str]) -> None: """ @@ -22,8 +20,4 @@ def handle(self, *args: list[str], **kwargs: dict[str, str]) -> None: """ search_backend = get_index_search_backend() indices_deleted_count = search_backend.delete_unused_indices() - self.stdout.write( - self.style.SUCCESS( - f"{indices_deleted_count} unused indices deleted successfully." - ) - ) + self.stdout.write(self.style.SUCCESS(f"{indices_deleted_count} unused indices deleted successfully.")) diff --git a/forum/management/commands/forum_delete_course_from_mongodb.py b/forum/management/commands/forum_delete_course_from_mongodb.py index dfd0098c..2c79ad94 100644 --- a/forum/management/commands/forum_delete_course_from_mongodb.py +++ b/forum/management/commands/forum_delete_course_from_mongodb.py @@ -14,9 +14,7 @@ class Command(BaseCommand): help = "Delete data from MongoDB for specific courses or all courses" def add_arguments(self, parser: CommandParser) -> None: - parser.add_argument( - "courses", nargs="+", type=str, help="List of course IDs or `all`" - ) + parser.add_argument("courses", nargs="+", type=str, help="List of course IDs or `all`") parser.add_argument( "--dry-run", action="store_true", @@ -31,9 +29,7 @@ def handle(self, *args: str, **options: dict[str, Any]) -> None: dry_run: bool = bool(options["dry_run"]) if dry_run: - self.stdout.write( - self.style.WARNING("Performing dry run. No data will be deleted.") - ) + self.stdout.write(self.style.WARNING("Performing dry run. No data will be deleted.")) if "all" in courses: courses = get_all_course_ids(db) @@ -43,10 +39,6 @@ def handle(self, *args: str, **options: dict[str, Any]) -> None: delete_course_data(db, course_id, dry_run, self.stdout) if dry_run: - self.stdout.write( - self.style.SUCCESS("Dry run completed. No data was deleted.") - ) + self.stdout.write(self.style.SUCCESS("Dry run completed. No data was deleted.")) else: - self.stdout.write( - self.style.SUCCESS("Data deletion completed successfully") - ) + self.stdout.write(self.style.SUCCESS("Data deletion completed successfully")) diff --git a/forum/management/commands/forum_migrate_course_from_mongodb_to_mysql.py b/forum/management/commands/forum_migrate_course_from_mongodb_to_mysql.py index 833fd96a..f41eab88 100644 --- a/forum/management/commands/forum_migrate_course_from_mongodb_to_mysql.py +++ b/forum/management/commands/forum_migrate_course_from_mongodb_to_mysql.py @@ -2,8 +2,7 @@ from typing import Any -from django.core.management.base import BaseCommand -from django.core.management.base import CommandParser +from django.core.management.base import BaseCommand, CommandParser from forum.migration_helpers import ( get_all_course_ids, @@ -21,9 +20,7 @@ class Command(BaseCommand): def add_arguments(self, parser: CommandParser) -> None: """Add arguments to the command.""" - parser.add_argument( - "courses", nargs="+", type=str, help="List of course IDs or `all`" - ) + parser.add_argument("courses", nargs="+", type=str, help="List of course IDs or `all`") def handle(self, *args: str, **options: dict[str, Any]) -> None: """Handle the command.""" diff --git a/forum/management/commands/rebuild_forum_indices.py b/forum/management/commands/rebuild_forum_indices.py index 427050f7..5f07a44b 100644 --- a/forum/management/commands/rebuild_forum_indices.py +++ b/forum/management/commands/rebuild_forum_indices.py @@ -49,11 +49,7 @@ def handle(self, *args: list[str], **kwargs: dict[str, int]) -> None: search_backend = get_index_search_backend() batch_size = get_int_value_from_collection(kwargs, "batch_size", 500) - extra_catchup_minutes = get_int_value_from_collection( - kwargs, "extra_catchup_minutes", 5 - ) + extra_catchup_minutes = get_int_value_from_collection(kwargs, "extra_catchup_minutes", 5) - search_backend.rebuild_indices( - batch_size=batch_size, extra_catchup_minutes=extra_catchup_minutes - ) + search_backend.rebuild_indices(batch_size=batch_size, extra_catchup_minutes=extra_catchup_minutes) self.stdout.write(self.style.SUCCESS("Forum indices rebuilt successfully.")) diff --git a/forum/migration_helpers.py b/forum/migration_helpers.py index d51d4584..a4f29506 100644 --- a/forum/migration_helpers.py +++ b/forum/migration_helpers.py @@ -1,7 +1,7 @@ """Migration commands helper methods.""" -from typing import Any import logging +from typing import Any from django.contrib.auth.models import User # pylint: disable=E5142 from django.core.management.base import OutputWrapper @@ -23,7 +23,7 @@ Subscription, UserVote, ) -from forum.utils import make_aware, get_trunc_title +from forum.utils import get_trunc_title, make_aware logger = logging.getLogger(__name__) @@ -165,8 +165,7 @@ def create_or_update_comment(comment_data: dict[str, Any]) -> None: mongo_thread = MongoContent.objects.filter(mongo_id=mongo_thread_id).first() if not mongo_thread: logger.warning( - f"Thread mapping not found for comment {comment_data.get('_id')} " - f"(mongo_thread_id={mongo_thread_id})" + f"Thread mapping not found for comment {comment_data.get('_id')} (mongo_thread_id={mongo_thread_id})" ) return thread = CommentThread.objects.filter(pk=mongo_thread.content_object_id).first() @@ -181,14 +180,9 @@ def create_or_update_comment(comment_data: dict[str, Any]) -> None: parent_id = str(comment_data["parent_id"]) mongo_parent_comment = MongoContent.objects.filter(mongo_id=parent_id).first() if not mongo_parent_comment: - logger.warning( - f"Parent mapping not found for comment {comment_data.get('_id')} " - f"(parent_id={parent_id})" - ) + logger.warning(f"Parent mapping not found for comment {comment_data.get('_id')} (parent_id={parent_id})") return - parent = Comment.objects.filter( - id=mongo_parent_comment.content_object_id - ).first() + parent = Comment.objects.filter(id=mongo_parent_comment.content_object_id).first() if not parent: logger.warning( f"Skipping comment {comment_data.get('_id')}: parent object not found " @@ -196,9 +190,7 @@ def create_or_update_comment(comment_data: dict[str, Any]) -> None: ) return - mongo_comment, _ = MongoContent.objects.get_or_create( - mongo_id=str(comment_data["_id"]) - ) + mongo_comment, _ = MongoContent.objects.get_or_create(mongo_id=str(comment_data["_id"])) if not mongo_comment.content_object_id: comment = Comment.objects.create( author=author, @@ -254,13 +246,9 @@ def create_or_update_edit_history(content: dict[str, Any]) -> None: content_type = CommentThread if content["_type"] == "CommentThread" else Comment mongo_content = MongoContent.objects.filter(mongo_id=str(content["_id"])).first() if not mongo_content: - logger.warning( - f"Skipping edit history for content {content.get('_id')}: mapping not found" - ) + logger.warning(f"Skipping edit history for content {content.get('_id')}: mapping not found") return - content_object = content_type.objects.filter( - pk=mongo_content.content_object_id - ).first() + content_object = content_type.objects.filter(pk=mongo_content.content_object_id).first() if not content_object: logger.warning( f"Skipping edit history for content {content.get('_id')}: target object not found " @@ -288,13 +276,9 @@ def create_or_update_abuse_flaggers(content: dict[str, Any]) -> None: content_type = CommentThread if content["_type"] == "CommentThread" else Comment mongo_content = MongoContent.objects.filter(mongo_id=str(content["_id"])).first() if not mongo_content: - logger.warning( - f"Skipping abuse flaggers for content {content.get('_id')}: mapping not found" - ) + logger.warning(f"Skipping abuse flaggers for content {content.get('_id')}: mapping not found") return - content_object = content_type.objects.filter( - pk=mongo_content.content_object_id - ).first() + content_object = content_type.objects.filter(pk=mongo_content.content_object_id).first() if not content_object: logger.warning( f"Skipping abuse flaggers for content {content.get('_id')}: target object not found " @@ -334,18 +318,12 @@ def migrate_subscriptions(db: Database[dict[str, Any]], content_id: str) -> None user = get_user_or_none(sub["subscriber_id"]) if not user: continue - content_type = ( - CommentThread if sub["source_type"] == "CommentThread" else Comment - ) + content_type = CommentThread if sub["source_type"] == "CommentThread" else Comment mongo_content = MongoContent.objects.filter(mongo_id=str(content_id)).first() if not mongo_content: - logger.warning( - f"Skipping subscription for source {content_id}: mapping not found" - ) + logger.warning(f"Skipping subscription for source {content_id}: mapping not found") continue - content = content_type.objects.filter( - pk=mongo_content.content_object_id - ).first() + content = content_type.objects.filter(pk=mongo_content.content_object_id).first() if not content: logger.warning( f"Skipping subscription for source {content_id}: target object not found " @@ -391,12 +369,8 @@ def migrate_read_states(db: Database[dict[str, Any]], course_id: str) -> None: for read_state in user_data.get("read_states", []): if read_state["course_id"] == course_id: rs, _ = ReadState.objects.get_or_create(user=user, course_id=course_id) - for thread_id, timestamp in read_state.get( - "last_read_times", {} - ).items(): - mongo_content = MongoContent.objects.filter( - mongo_id=thread_id - ).first() + for thread_id, timestamp in read_state.get("last_read_times", {}).items(): + mongo_content = MongoContent.objects.filter(mongo_id=thread_id).first() thread = mongo_content and mongo_content.content # For older courses using cs_comment_service, the thread may be None @@ -405,9 +379,7 @@ def migrate_read_states(db: Database[dict[str, Any]], course_id: str) -> None: # have a thread with deleted thread_id from read_states. if not thread: continue - existing_read_time = LastReadTime.objects.filter( - read_state=rs, comment_thread=thread - ).first() + existing_read_time = LastReadTime.objects.filter(read_state=rs, comment_thread=thread).first() if not existing_read_time: LastReadTime.objects.create( read_state=rs, @@ -428,18 +400,12 @@ def delete_course_data( """Delete content (threads and comments).""" contents = db.contents.find({"course_id": course_id}) for content in contents: - subscriptions = ( - db.subscriptions.delete_many({"source_id": str(content["_id"])}) - if not dry_run - else None - ) + subscriptions = db.subscriptions.delete_many({"source_id": str(content["_id"])}) if not dry_run else None stdout.write( f"Subscription documents to be deleted: {subscriptions.deleted_count if subscriptions else 'N/A (dry run)'}" ) - content_result = ( - db.contents.delete_many({"course_id": course_id}) if not dry_run else None - ) + content_result = db.contents.delete_many({"course_id": course_id}) if not dry_run else None stdout.write( f"Content documents to be deleted: {content_result.deleted_count if content_result else 'N/A (dry run)'}" ) diff --git a/forum/migrations/0001_initial.py b/forum/migrations/0001_initial.py index db34332f..ebe22588 100644 --- a/forum/migrations/0001_initial.py +++ b/forum/migrations/0001_initial.py @@ -1,14 +1,14 @@ # Generated by Django 4.2.14 on 2024-10-10 15:57 -from django.conf import settings -from django.db import migrations, models import django.db.models.deletion import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + import forum.utils class Migration(migrations.Migration): - initial = True dependencies = [ @@ -67,9 +67,7 @@ class Migration(migrations.Migration): ), ( "commentable_id", - models.CharField( - blank=True, default=None, max_length=255, null=True - ), + models.CharField(blank=True, default=None, max_length=255, null=True), ), ( "author", @@ -379,9 +377,7 @@ class Migration(migrations.Migration): ("content_object_id", models.PositiveIntegerField()), ( "vote", - models.IntegerField( - validators=[forum.utils.validate_upvote_or_downvote] - ), + models.IntegerField(validators=[forum.utils.validate_upvote_or_downvote]), ), ( "content_type", @@ -401,9 +397,7 @@ class Migration(migrations.Migration): options={ "indexes": [ models.Index(fields=["vote"], name="forum_userv_vote_2ccbcf_idx"), - models.Index( - fields=["user", "vote"], name="forum_userv_user_id_7ab0c3_idx" - ), + models.Index(fields=["user", "vote"], name="forum_userv_user_id_7ab0c3_idx"), models.Index( fields=["content_type", "content_object_id"], name="forum_userv_content_55d9ad_idx", @@ -444,9 +438,7 @@ class Migration(migrations.Migration): ], options={ "indexes": [ - models.Index( - fields=["subscriber"], name="forum_subsc_subscri_34fd6c_idx" - ), + models.Index(fields=["subscriber"], name="forum_subsc_subscri_34fd6c_idx"), models.Index( fields=[ "subscriber", @@ -464,16 +456,12 @@ class Migration(migrations.Migration): name="forum_subsc_source__ef74be_idx", ), ], - "unique_together": { - ("subscriber", "source_content_type", "source_object_id") - }, + "unique_together": {("subscriber", "source_content_type", "source_object_id")}, }, ), migrations.AddIndex( model_name="readstate", - index=models.Index( - fields=["user", "course_id"], name="forum_reads_user_id_30cd0a_idx" - ), + index=models.Index(fields=["user", "course_id"], name="forum_reads_user_id_30cd0a_idx"), ), migrations.AlterUniqueTogether( name="readstate", @@ -488,9 +476,7 @@ class Migration(migrations.Migration): ), migrations.AddIndex( model_name="lastreadtime", - index=models.Index( - fields=["comment_thread"], name="forum_lastr_comment_652102_idx" - ), + index=models.Index(fields=["comment_thread"], name="forum_lastr_comment_652102_idx"), ), migrations.AlterUniqueTogether( name="lastreadtime", @@ -516,9 +502,7 @@ class Migration(migrations.Migration): ), migrations.AddIndex( model_name="edithistory", - index=models.Index( - fields=["editor"], name="forum_edith_editor__159ea4_idx" - ), + index=models.Index(fields=["editor"], name="forum_edith_editor__159ea4_idx"), ), migrations.AddIndex( model_name="edithistory", @@ -529,9 +513,7 @@ class Migration(migrations.Migration): ), migrations.AddIndex( model_name="edithistory", - index=models.Index( - fields=["created_at"], name="forum_edith_created_67bba8_idx" - ), + index=models.Index(fields=["created_at"], name="forum_edith_created_67bba8_idx"), ), migrations.AlterUniqueTogether( name="coursestat", @@ -539,21 +521,15 @@ class Migration(migrations.Migration): ), migrations.AddIndex( model_name="commentthread", - index=models.Index( - fields=["context"], name="forum_comme_context_cfe3d0_idx" - ), + index=models.Index(fields=["context"], name="forum_comme_context_cfe3d0_idx"), ), migrations.AddIndex( model_name="commentthread", - index=models.Index( - fields=["author"], name="forum_comme_author__04774e_idx" - ), + index=models.Index(fields=["author"], name="forum_comme_author__04774e_idx"), ), migrations.AddIndex( model_name="commentthread", - index=models.Index( - fields=["author", "course_id"], name="forum_comme_author__e6f558_idx" - ), + index=models.Index(fields=["author", "course_id"], name="forum_comme_author__e6f558_idx"), ), migrations.AddIndex( model_name="commentthread", @@ -571,9 +547,7 @@ class Migration(migrations.Migration): ), migrations.AddIndex( model_name="comment", - index=models.Index( - fields=["author", "course_id"], name="forum_comme_author__257002_idx" - ), + index=models.Index(fields=["author", "course_id"], name="forum_comme_author__257002_idx"), ), migrations.AddIndex( model_name="comment", diff --git a/forum/migrations/0002_alter_readstate_course_id_and_more.py b/forum/migrations/0002_alter_readstate_course_id_and_more.py index 5375929f..da2823a0 100644 --- a/forum/migrations/0002_alter_readstate_course_id_and_more.py +++ b/forum/migrations/0002_alter_readstate_course_id_and_more.py @@ -1,11 +1,10 @@ # Generated by Django 4.2.16 on 2024-10-17 07:15 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("contenttypes", "0002_remove_content_type_name"), ("forum", "0001_initial"), diff --git a/forum/migrations/0003_alter_commentthread_title.py b/forum/migrations/0003_alter_commentthread_title.py index d940ad6f..54534040 100644 --- a/forum/migrations/0003_alter_commentthread_title.py +++ b/forum/migrations/0003_alter_commentthread_title.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("forum", "0002_alter_readstate_course_id_and_more"), ] diff --git a/forum/migrations/0004_add_author_username_fields.py b/forum/migrations/0004_add_author_username_fields.py index 2036f0c3..a7d7f0d6 100644 --- a/forum/migrations/0004_add_author_username_fields.py +++ b/forum/migrations/0004_add_author_username_fields.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("forum", "0003_alter_commentthread_title"), ] diff --git a/forum/migrations/0005_alter_commentthread_pinned.py b/forum/migrations/0005_alter_commentthread_pinned.py index c95ebcc6..782bf81f 100644 --- a/forum/migrations/0005_alter_commentthread_pinned.py +++ b/forum/migrations/0005_alter_commentthread_pinned.py @@ -11,7 +11,6 @@ def backfill_pinned_false(apps: Any, schema_editor: Any) -> None: class Migration(migrations.Migration): - dependencies = [ ("forum", "0004_add_author_username_fields"), ] diff --git a/forum/mongo.py b/forum/mongo.py index e2ca6f0c..3a2f2a27 100644 --- a/forum/mongo.py +++ b/forum/mongo.py @@ -1,7 +1,7 @@ """Mongo module for forum app.""" import logging -from typing import Any, Optional +from typing import Any from django.conf import settings from pymongo import MongoClient @@ -13,8 +13,8 @@ def get_database( - database: Optional[str] = None, - client_params: Optional[dict[Any, Any]] = None, + database: str | None = None, + client_params: dict[Any, Any] | None = None, ) -> Database: """ Connect to MongoDB. diff --git a/forum/pagination.py b/forum/pagination.py index 46dc98c8..e773e219 100644 --- a/forum/pagination.py +++ b/forum/pagination.py @@ -28,9 +28,7 @@ def get_page_size(self, request: Request) -> int: return self.max_page_size return min(int(page_size), self.max_page_size) - def paginate_queryset( - self, queryset: Any, request: Request, view: Any = None - ) -> Any: + def paginate_queryset(self, queryset: Any, request: Request, view: Any = None) -> Any: """ Paginate the queryset. diff --git a/forum/search/__init__.py b/forum/search/__init__.py index 83aa650d..4e9719c6 100644 --- a/forum/search/__init__.py +++ b/forum/search/__init__.py @@ -37,9 +37,7 @@ def _get_search_backend() -> base.BaseSearchBackend: By default, use the Elasticsearch search backend. """ - search_backend_module_name = getattr( - settings, "FORUM_SEARCH_BACKEND", "forum.search.es.ElasticsearchBackend" - ) + search_backend_module_name = getattr(settings, "FORUM_SEARCH_BACKEND", "forum.search.es.ElasticsearchBackend") module_name, class_name = search_backend_module_name.rsplit(".", 1) Backend = getattr(importlib.import_module(module_name), class_name) return Backend diff --git a/forum/search/base.py b/forum/search/base.py index e65f5614..7ebf6be1 100644 --- a/forum/search/base.py +++ b/forum/search/base.py @@ -14,14 +14,10 @@ class BaseDocumentSearchBackend: Abstract base class for document management. """ - def index_document( - self, index_name: str, doc_id: str | int, document: dict[str, t.Any] - ) -> None: + def index_document(self, index_name: str, doc_id: str | int, document: dict[str, t.Any]) -> None: raise NotImplementedError - def update_document( - self, index_name: str, doc_id: str | int, update_data: dict[str, t.Any] - ) -> None: + def update_document(self, index_name: str, doc_id: str | int, update_data: dict[str, t.Any]) -> None: raise NotImplementedError def delete_document(self, index_name: str, doc_id: str | int) -> None: @@ -36,9 +32,7 @@ class BaseIndexSearchBackend: def initialize_indices(self, force_new_index: bool = False) -> None: raise NotImplementedError - def rebuild_indices( - self, batch_size: int = 500, extra_catchup_minutes: int = 5 - ) -> None: + def rebuild_indices(self, batch_size: int = 500, extra_catchup_minutes: int = 5) -> None: raise NotImplementedError def validate_indices(self) -> None: @@ -66,16 +60,16 @@ def get_thread_ids( context: str, group_ids: list[int], search_text: str, - sort_criteria: t.Optional[list[dict[str, str]]] = None, - commentable_ids: t.Optional[list[str]] = None, - course_id: t.Optional[str] = None, + sort_criteria: list[dict[str, str]] | None = None, + commentable_ids: list[str] | None = None, + course_id: str | None = None, ) -> list[str]: """ Retrieve thread IDs based on search criteria. """ raise NotImplementedError - def get_suggested_text(self, search_text: str) -> t.Optional[str]: + def get_suggested_text(self, search_text: str) -> str | None: """ Retrieve text suggestions for a given search query. @@ -89,9 +83,9 @@ def get_thread_ids_with_corrected_text( context: str, group_ids: list[int], search_text: str, - sort_criteria: t.Optional[list[dict[str, str]]] = None, - commentable_ids: t.Optional[list[str]] = None, - course_id: t.Optional[str] = None, + sort_criteria: list[dict[str, str]] | None = None, + commentable_ids: list[str] | None = None, + course_id: str | None = None, ) -> list[str]: """ The function is just used of mimicking the behaviour of the test cases. @@ -113,6 +107,6 @@ class BaseSearchBackend: Abstract base search backend that exposes all search backend features. """ - DOCUMENT_SEARCH_CLASS: t.Type[BaseDocumentSearchBackend] - INDEX_SEARCH_CLASS: t.Type[BaseIndexSearchBackend] - THREAD_SEARCH_CLASS: t.Type[BaseThreadSearchBackend] + DOCUMENT_SEARCH_CLASS: type[BaseDocumentSearchBackend] + INDEX_SEARCH_CLASS: type[BaseIndexSearchBackend] + THREAD_SEARCH_CLASS: type[BaseThreadSearchBackend] diff --git a/forum/search/es.py b/forum/search/es.py index 30654059..d64ec549 100644 --- a/forum/search/es.py +++ b/forum/search/es.py @@ -4,8 +4,9 @@ import logging import re +from collections.abc import Iterator from datetime import datetime, timedelta -from typing import Any, Iterator, Optional +from typing import Any from django.conf import settings from elasticsearch import Elasticsearch, exceptions, helpers @@ -26,7 +27,7 @@ class ElasticsearchClientMixin: Provide a Elasticsearch client API based on a singleton. """ - ELASTIC_SEARCH_INSTANCE: Optional[Elasticsearch] = None + ELASTIC_SEARCH_INSTANCE: Elasticsearch | None = None @property def client(self) -> Elasticsearch: @@ -34,9 +35,7 @@ def client(self) -> Elasticsearch: Elasticsearch client singleton. """ if self.ELASTIC_SEARCH_INSTANCE is None: - self.ELASTIC_SEARCH_INSTANCE = Elasticsearch( - settings.FORUM_ELASTIC_SEARCH_CONFIG - ) + self.ELASTIC_SEARCH_INSTANCE = Elasticsearch(settings.FORUM_ELASTIC_SEARCH_CONFIG) return self.ELASTIC_SEARCH_INSTANCE @@ -70,16 +69,12 @@ def get_mysql_model_from_index_name(self, index_name: str) -> type[Content]: raise Exception("Invalid model name") -class ElasticsearchDocumentBackend( - base.BaseDocumentSearchBackend, ElasticsearchClientMixin -): +class ElasticsearchDocumentBackend(base.BaseDocumentSearchBackend, ElasticsearchClientMixin): """ Elasticsearch-based backend for document management. """ - def update_document( - self, index_name: str, doc_id: str | int, update_data: dict[str, Any] - ) -> None: + def update_document(self, index_name: str, doc_id: str | int, update_data: dict[str, Any]) -> None: """ Update a single document in the specified index. @@ -112,9 +107,7 @@ def delete_document(self, index_name: str, doc_id: str | int) -> None: except exceptions.RequestError as e: log.error(f"Error deleting document {doc_id} from index {index_name}: {e}") - def index_document( - self, index_name: str, doc_id: str | int, document: dict[str, Any] - ) -> None: + def index_document(self, index_name: str, doc_id: str | int, document: dict[str, Any]) -> None: """ Index a single document in the specified Elasticsearch index. @@ -130,9 +123,7 @@ def index_document( log.error(f"Error indexing document {doc_id} in {index_name}: {e}") -class ElasticsearchIndexBackend( - base.BaseIndexSearchBackend, ElasticsearchModelMixin, ElasticsearchClientMixin -): +class ElasticsearchIndexBackend(base.BaseIndexSearchBackend, ElasticsearchModelMixin, ElasticsearchClientMixin): """ Search forum threads. """ @@ -186,9 +177,7 @@ class ElasticsearchIndexBackend( }, } - def rebuild_indices( - self, batch_size: int = 500, extra_catchup_minutes: int = 5 - ) -> None: + def rebuild_indices(self, batch_size: int = 500, extra_catchup_minutes: int = 5) -> None: """ Rebuild the indices by creating new indices, importing data, and managing aliases. @@ -203,15 +192,11 @@ def rebuild_indices( for index_name in index_names: current_batch = 1 mysql_model = self.get_mysql_model_from_index_name(index_name) - for response in self._import_to_es_from_mysql( - mysql_model, index_name, batch_size - ): + for response in self._import_to_es_from_mysql(mysql_model, index_name, batch_size): self.batch_import_post_process(response, current_batch) current_batch += 1 - adjusted_start_time = initial_start_time - timedelta( - minutes=extra_catchup_minutes - ) + adjusted_start_time = initial_start_time - timedelta(minutes=extra_catchup_minutes) self.catchup_indices(index_names, adjusted_start_time, batch_size) # Update aliases to point to new indices @@ -223,9 +208,7 @@ def rebuild_indices( log.info("Rebuild indices complete.") - def catchup_indices( - self, index_names: list[str], start_time: datetime, batch_size: int = 100 - ) -> None: + def catchup_indices(self, index_names: list[str], start_time: datetime, batch_size: int = 100) -> None: """ Catch up the indices by importing documents updated after the specified start time. @@ -238,9 +221,7 @@ def catchup_indices( current_batch = 1 mysql_model = self.get_mysql_model_from_index_name(index_name) mysql_query: dict[str, Any] = {"updated_at__gte": start_time} - for response in self._import_to_es_from_mysql( - mysql_model, index_name, batch_size, mysql_query - ): + for response in self._import_to_es_from_mysql(mysql_model, index_name, batch_size, mysql_query): self.batch_import_post_process(response, current_batch) current_batch += 1 log.info(f"Catch up from {start_time} complete.") @@ -286,11 +267,7 @@ def delete_unused_indices(self) -> int: int: The number of indices deleted. """ # Fetch all indices related to models - all_indices = [ - index - for pattern in self.index_names - for index in self.client.indices.get(f"{pattern}*") - ] + all_indices = [index for pattern in self.index_names for index in self.client.indices.get(f"{pattern}*")] # Determine the latest indices latest_indices: dict[str, Any] = {} @@ -299,25 +276,18 @@ def delete_unused_indices(self) -> int: match = re.search(r"\d{14}", index_name) if match: timestamp = datetime.strptime(match.group(), "%Y%m%d%H%M%S") - if ( - base_name not in latest_indices - or timestamp > latest_indices[base_name][1] - ): + if base_name not in latest_indices or timestamp > latest_indices[base_name][1]: latest_indices[base_name] = (index_name, timestamp) # Delete all indices except the latest ones - indices_to_delete = set(all_indices) - { - name for name, _ in latest_indices.values() - } + indices_to_delete = set(all_indices) - {name for name, _ in latest_indices.values()} if indices_to_delete: self.client.indices.delete(index=",".join(indices_to_delete)) log.info(f"Deleted unused indices: {indices_to_delete}") return len(indices_to_delete) - def batch_import_post_process( - self, response: tuple[int, Any], batch_number: int - ) -> None: + def batch_import_post_process(self, response: tuple[int, Any], batch_number: int) -> None: """ Process the response from a batch import operation. @@ -329,13 +299,9 @@ def batch_import_post_process( for item in errors: if "error" in item["index"]: log.error(f"Error indexing. Response was: {response}") - log.info( - f"Imported {success_count} documents to the batch {batch_number} into the index" - ) + log.info(f"Imported {success_count} documents to the batch {batch_number} into the index") - def move_alias( - self, alias_name: str, index_name: str, force_delete: bool = False - ) -> None: + def move_alias(self, alias_name: str, index_name: str, force_delete: bool = False) -> None: """ Move an alias to point to a new index, optionally deleting an existing index with the same name. @@ -348,9 +314,7 @@ def move_alias( ValueError: If the alias name matches the index name or if the index doesn't exist. """ if alias_name == index_name: - raise ValueError( - f"Can't point alias [{alias_name}] to an index of the same name." - ) + raise ValueError(f"Can't point alias [{alias_name}] to an index of the same name.") if not self.exists_index(index_name): raise ValueError(f"Can't point alias to non-existent index [{index_name}].") @@ -535,14 +499,12 @@ def get_base_index_name(self, index_name: str) -> str: return re.sub(r"_\d{14}$", "", index_name) -class ElasticsearchThreadSearchBackend( - base.BaseThreadSearchBackend, ElasticsearchClientMixin, ElasticsearchModelMixin -): +class ElasticsearchThreadSearchBackend(base.BaseThreadSearchBackend, ElasticsearchClientMixin, ElasticsearchModelMixin): """ Base class to perform thread search. """ - def get_suggested_text(self, search_text: str) -> Optional[str]: + def get_suggested_text(self, search_text: str) -> str | None: """ Retrieve text suggestions for a given search query. @@ -559,24 +521,16 @@ def get_suggested_text(self, search_text: str) -> Optional[str]: for field in suggestion_fields } } - response: dict[str, Any] = self.client.search( - index=self.index_names, body=suggest_body - ) - return self._extract_suggestion( - response, [f"{field}_suggestions" for field in suggestion_fields] - ) + response: dict[str, Any] = self.client.search(index=self.index_names, body=suggest_body) + return self._extract_suggestion(response, [f"{field}_suggestions" for field in suggestion_fields]) @staticmethod - def _extract_suggestion( - response: dict[str, Any], suggestion_types: list[str] - ) -> Optional[str]: + def _extract_suggestion(response: dict[str, Any], suggestion_types: list[str]) -> str | None: """ Extract suggestions from the Elasticsearch response. """ for suggestion_type in suggestion_types: - suggestions: list[dict[str, Any]] = response.get("suggest", {}).get( - suggestion_type, [] - ) + suggestions: list[dict[str, Any]] = response.get("suggest", {}).get(suggestion_type, []) options = suggestions and suggestions[0].get("options", []) if options: return options[0]["text"] @@ -585,8 +539,8 @@ def _extract_suggestion( def build_must_clause( self, search_text: str, - commentable_ids: Optional[list[str]] = None, - course_id: Optional[str] = None, + commentable_ids: list[str] | None = None, + course_id: str | None = None, ) -> list[dict[str, Any]]: """ Build the 'must' clause for thread-specific Elasticsearch queries based on input parameters. @@ -616,7 +570,7 @@ def build_must_clause( def build_filter_clause( self, context: str, - group_ids: Optional[list[int]] = None, + group_ids: list[int] | None = None, ) -> list[dict[str, Any]]: """ Build the 'filter' clause for thread-specific Elasticsearch queries based on context and group parameters. @@ -653,25 +607,19 @@ def get_thread_ids( context: str, group_ids: list[int], search_text: str, - sort_criteria: Optional[list[dict[str, str]]] = None, - commentable_ids: Optional[list[str]] = None, - course_id: Optional[str] = None, + sort_criteria: list[dict[str, str]] | None = None, + commentable_ids: list[str] | None = None, + course_id: str | None = None, ) -> list[str]: """ Retrieve thread IDs based on search criteria. """ - must_clause: list[dict[str, Any]] = self.build_must_clause( - search_text, commentable_ids, course_id - ) - filter_clause: list[dict[str, Any]] = self.build_filter_clause( - context, group_ids - ) + must_clause: list[dict[str, Any]] = self.build_must_clause(search_text, commentable_ids, course_id) + filter_clause: list[dict[str, Any]] = self.build_filter_clause(context, group_ids) search_body = { "size": FORUM_MAX_DEEP_SEARCH_COMMENT_COUNT, "sort": sort_criteria or [{"updated_at": "desc"}], - "query": { - "bool": {"must": must_clause or [], "should": filter_clause or []} - }, + "query": {"bool": {"must": must_clause or [], "should": filter_clause or []}}, } response = self.client.search(index=self.index_names, body=search_body) if response: diff --git a/forum/search/meilisearch.py b/forum/search/meilisearch.py index aea8fc75..8609fc37 100644 --- a/forum/search/meilisearch.py +++ b/forum/search/meilisearch.py @@ -59,16 +59,12 @@ def create_document(document: dict[str, t.Any], doc_id: str) -> dict[str, t.Any] return processed -class MeilisearchDocumentBackend( - base.BaseDocumentSearchBackend, MeilisearchClientMixin -): +class MeilisearchDocumentBackend(base.BaseDocumentSearchBackend, MeilisearchClientMixin): """ Simple document management. """ - def index_document( - self, index_name: str, doc_id: str | int, document: dict[str, t.Any] - ) -> None: + def index_document(self, index_name: str, doc_id: str | int, document: dict[str, t.Any]) -> None: """ Insert a single document in the Meilisearch index. """ @@ -76,9 +72,7 @@ def index_document( processed = create_document(document, str(doc_id)) meilisearch_index.add_documents([processed]) - def update_document( - self, index_name: str, doc_id: str | int, update_data: dict[str, t.Any] - ) -> None: + def update_document(self, index_name: str, doc_id: str | int, update_data: dict[str, t.Any]) -> None: """ Updating is the same as inserting in meilisearch """ @@ -100,9 +94,7 @@ class MeilisearchIndexBackend(base.BaseIndexSearchBackend, MeilisearchClientMixi def initialize_indices(self, force_new_index: bool = False) -> None: filterable_fields = [m.PRIMARY_KEY_FIELD_NAME] + FILTERABLE_FIELDS - index_filterables = { - Model.index_name: filterable_fields for Model in MODEL_INDICES - } + index_filterables = {Model.index_name: filterable_fields for Model in MODEL_INDICES} if force_new_index: for index_name in index_filterables: meilisearch_index_name = m.get_meilisearch_index_name(index_name) @@ -110,9 +102,7 @@ def initialize_indices(self, force_new_index: bool = False) -> None: m.wait_for_task_to_succeed(self.meilisearch_client, task_info) m.create_indexes(index_filterables=index_filterables) - def rebuild_indices( - self, batch_size: int = 500, extra_catchup_minutes: int = 5 - ) -> None: + def rebuild_indices(self, batch_size: int = 500, extra_catchup_minutes: int = 5) -> None: """ Parse model instances and insert them in Meilisearch. Only MySQL-backed instances are supported. @@ -125,10 +115,7 @@ def rebuild_indices( paginator = Paginator(Model.objects.all(), per_page=batch_size) for page_number in paginator.page_range: page = paginator.get_page(page_number) - documents = [ - create_document(obj.doc_to_hash(), str(obj.id)) - for obj in page.object_list - ] + documents = [create_document(obj.doc_to_hash(), str(obj.id)) for obj in page.object_list] if documents: meilisearch_index.add_documents(documents) @@ -143,12 +130,8 @@ def refresh_indices(self) -> None: """ In Meilisearch, this command consists of waiting for pending tasks. """ - for enqueued_task in self.meilisearch_client.get_tasks( - {"statuses": ["enqueued", "processing"]} - ).results: - task = self.meilisearch_client.wait_for_task( - enqueued_task.uid, timeout_in_ms=5000 - ) + for enqueued_task in self.meilisearch_client.get_tasks({"statuses": ["enqueued", "processing"]}).results: + task = self.meilisearch_client.wait_for_task(enqueued_task.uid, timeout_in_ms=5000) if task.status != "succeeded": raise RuntimeError(f"Failed meilisearch task: {task}") @@ -160,9 +143,7 @@ def validate_indices(self) -> None: self.initialize_indices(force_new_index=False) -class MeilisearchThreadSearchBackend( - base.BaseThreadSearchBackend, MeilisearchClientMixin -): +class MeilisearchThreadSearchBackend(base.BaseThreadSearchBackend, MeilisearchClientMixin): """ Thread search backend. @@ -178,9 +159,9 @@ def get_thread_ids( group_ids: list[int], search_text: str, # This parameter is unsupported, but as far as we know it's not used anywhere. - sort_criteria: t.Optional[list[dict[str, str]]] = None, - commentable_ids: t.Optional[list[str]] = None, - course_id: t.Optional[str] = None, + sort_criteria: list[dict[str, str]] | None = None, + commentable_ids: list[str] | None = None, + course_id: str | None = None, ) -> list[str]: """ Retrieve thread IDs based on search criteria. @@ -214,7 +195,7 @@ def get_thread_ids( # Don't make the slightest attempt to sort results return list(thread_ids) - def get_suggested_text(self, search_text: str) -> t.Optional[str]: + def get_suggested_text(self, search_text: str) -> str | None: """ Meilisearch does not support query suggestion https://github.com/orgs/meilisearch/discussions/740 diff --git a/forum/search/typesense.py b/forum/search/typesense.py index 6a106f5f..52551c34 100644 --- a/forum/search/typesense.py +++ b/forum/search/typesense.py @@ -2,16 +2,15 @@ Typesense backend for searching comments and threads. """ -from typing import Any, Optional, cast +from typing import Any, cast from bs4 import BeautifulSoup from django.conf import settings from django.core.paginator import Paginator - from typesense import Client +from typesense.exceptions import ObjectNotFound from typesense.types.collection import CollectionCreateSchema from typesense.types.document import DocumentSchema, SearchParameters -from typesense.exceptions import ObjectNotFound from forum.backends.mysql.models import Comment, CommentThread from forum.constants import FORUM_MAX_DEEP_SEARCH_COMMENT_COUNT @@ -136,11 +135,7 @@ def document_from_thread(doc_id: str | int, data: dict[str, Any]) -> DocumentSch "context": str(data.get("context", "")), "text": "{}\n{}".format( str(data.get("title", "")), - ( - BeautifulSoup(data["body"], features="html.parser").get_text() - if data.get("body") - else "" - ), + (BeautifulSoup(data["body"], features="html.parser").get_text() if data.get("body") else ""), ), } @@ -156,11 +151,7 @@ def document_from_comment(doc_id: str | int, data: dict[str, Any]) -> DocumentSc "course_id": str(data.get("course_id", "")), "commentable_id": "", "context": str(data.get("context", "")), - "text": ( - BeautifulSoup(data["body"], features="html.parser").get_text() - if data.get("body") - else "" - ), + "text": (BeautifulSoup(data["body"], features="html.parser").get_text() if data.get("body") else ""), } @@ -199,9 +190,7 @@ class TypesenseDocumentBackend(BaseDocumentSearchBackend): Document backend implementation for Typesense. """ - def index_document( - self, index_name: str, doc_id: str | int, document: dict[str, Any] - ) -> None: + def index_document(self, index_name: str, doc_id: str | int, document: dict[str, Any]) -> None: """ Index a document in Typesense. """ @@ -216,9 +205,7 @@ def index_document( client.collections[collection_name()].documents.upsert(typesense_document) - def update_document( - self, index_name: str, doc_id: str | int, update_data: dict[str, Any] - ) -> None: + def update_document(self, index_name: str, doc_id: str | int, update_data: dict[str, Any]) -> None: """ Same operation as index_document, because upsert is used. """ @@ -268,9 +255,7 @@ def initialize_indices(self, force_new_index: bool = False) -> None: if force_new_index or not exists: client.collections.create(collection_schema()) - def rebuild_indices( - self, batch_size: int = 500, extra_catchup_minutes: int = 5 - ) -> None: + def rebuild_indices(self, batch_size: int = 500, extra_catchup_minutes: int = 5) -> None: """ Reindex everything in Typesense @@ -287,23 +272,14 @@ def rebuild_indices( (CommentThread, document_from_thread), (Comment, document_from_comment), ]: - paginator = Paginator( - model.objects.order_by("pk").all(), per_page=batch_size - ) + paginator = Paginator(model.objects.order_by("pk").all(), per_page=batch_size) for page_number in paginator.page_range: page = paginator.get_page(page_number) - documents = [ - document_builder(obj.pk, obj.doc_to_hash()) - for obj in page.object_list - ] + documents = [document_builder(obj.pk, obj.doc_to_hash()) for obj in page.object_list] if documents: - response = client.collections[collection_name()].documents.import_( - documents, {"action": "upsert"} - ) + response = client.collections[collection_name()].documents.import_(documents, {"action": "upsert"}) if not all(result["success"] for result in response): - raise ValueError( - f"Errors while importing documents to Typesense collection: {response}" - ) + raise ValueError(f"Errors while importing documents to Typesense collection: {response}") def validate_indices(self) -> None: """ @@ -317,18 +293,12 @@ def validate_indices(self) -> None: """ client = get_typesense_client() # cast to a wider type, because we want to use it in a more flexible way than TypedDict normally allows. - actual_schema = cast( - dict[str, Any], client.collections[collection_name()].retrieve() - ) + actual_schema = cast(dict[str, Any], client.collections[collection_name()].retrieve()) expected_schema = expected_full_collection_schema() errors: list[str] = [] - expected_field_names = set( - map(lambda field: field["name"], expected_schema["fields"]) - ) - actual_field_names = set( - map(lambda field: field["name"], actual_schema["fields"]) - ) + expected_field_names = {field["name"] for field in expected_schema["fields"]} + actual_field_names = {field["name"] for field in actual_schema["fields"]} if missing_fields := expected_field_names - actual_field_names: errors.append( @@ -345,6 +315,7 @@ def validate_indices(self) -> None: for expected_field, actual_field in zip( sorted(expected_schema["fields"], key=lambda field: field["name"]), sorted(actual_schema["fields"], key=lambda field: field["name"]), + strict=False, ): for key, expected_value in expected_field.items(): if expected_value != actual_field[key]: @@ -398,9 +369,9 @@ def get_thread_ids( group_ids: list[int], search_text: str, # This parameter is unsupported, but as far as we know it's not used anywhere. - sort_criteria: Optional[list[dict[str, str]]] = None, - commentable_ids: Optional[list[str]] = None, - course_id: Optional[str] = None, + sort_criteria: list[dict[str, str]] | None = None, + commentable_ids: list[str] | None = None, + course_id: str | None = None, ) -> list[str]: """ Retrieve thread IDs based on search criteria. @@ -416,11 +387,12 @@ def get_thread_ids( results = client.collections[collection_name()].documents.search(params) thread_ids: set[str] = { - hit["document"]["thread_id"] for hit in results.get("hits", []) # type: ignore + hit["document"]["thread_id"] # type: ignore[misc] + for hit in results.get("hits", []) } return list(thread_ids) - def get_suggested_text(self, search_text: str) -> Optional[str]: + def get_suggested_text(self, search_text: str) -> str | None: """ Retrieve text suggestions for a given search query. diff --git a/forum/serializers/contents.py b/forum/serializers/contents.py index a8c5c319..49d72c8c 100644 --- a/forum/serializers/contents.py +++ b/forum/serializers/contents.py @@ -67,15 +67,11 @@ class ContentSerializer(serializers.Serializer[dict[str, Any]]): at_position_list = serializers.ListField(default=[]) user_id = serializers.CharField(source="author_id") username = serializers.CharField(source="author_username", allow_null=True) - author = serializers.CharField( - source="author_username", allow_null=True, required=False - ) + author = serializers.CharField(source="author_username", allow_null=True, required=False) commentable_id = serializers.CharField(default="course") votes = VoteSummarySerializer() abuse_flaggers = serializers.ListField(child=serializers.CharField(), default=[]) - historical_abuse_flaggers = serializers.ListField( - child=serializers.CharField(), default=[] - ) + historical_abuse_flaggers = serializers.ListField(child=serializers.CharField(), default=[]) edit_history = EditHistorySerializer(default=[], many=True) closed = serializers.BooleanField(default=False) type = serializers.CharField() diff --git a/forum/serializers/thread.py b/forum/serializers/thread.py index c2f8b85f..8a185250 100644 --- a/forum/serializers/thread.py +++ b/forum/serializers/thread.py @@ -2,7 +2,7 @@ Serializer for the thread data. """ -from typing import Any, Optional +from typing import Any from rest_framework import serializers from rest_framework.serializers import ValidationError @@ -86,9 +86,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.count_flagged = self.context_data.pop("count_flagged", False) self.include_endorsed = self.context_data.pop("include_endorsed", False) self.include_read_state = self.context_data.pop("include_read_state", False) - self.merge_question_type_responses = self.context_data.pop( - "merge_question_type_responses", False - ) + self.merge_question_type_responses = self.context_data.pop("merge_question_type_responses", False) # Customize fields based on context if not self.with_responses: @@ -109,7 +107,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - def get_read(self, obj: dict[str, Any]) -> Optional[bool]: + def get_read(self, obj: dict[str, Any]) -> bool | None: """ Retrieve the read state of the thread. @@ -125,13 +123,13 @@ def get_read(self, obj: dict[str, Any]) -> Optional[bool]: user_id = self.context_data.get("user_id", None) course_id = obj["course_id"] thread_key = obj["_id"] - is_read, _ = self.backend.get_read_states( - [obj["_id"]], user_id, course_id - ).get(thread_key, (False, obj["comment_count"])) + is_read, _ = self.backend.get_read_states([obj["_id"]], user_id, course_id).get( + thread_key, (False, obj["comment_count"]) + ) return is_read return None - def get_unread_comments_count(self, obj: dict[str, Any]) -> Optional[int]: + def get_unread_comments_count(self, obj: dict[str, Any]) -> int | None: """ Retrieve the count of unread comments for the thread. @@ -147,13 +145,13 @@ def get_unread_comments_count(self, obj: dict[str, Any]) -> Optional[int]: user_id = self.context_data.get("user_id", None) course_id = obj["course_id"] thread_key = obj["_id"] - _, unread_count = self.backend.get_read_states( - [obj["_id"]], user_id, course_id - ).get(thread_key, (False, obj["comment_count"])) + _, unread_count = self.backend.get_read_states([obj["_id"]], user_id, course_id).get( + thread_key, (False, obj["comment_count"]) + ) return unread_count return None - def get_endorsed(self, obj: dict[str, Any]) -> Optional[bool]: + def get_endorsed(self, obj: dict[str, Any]) -> bool | None: """ Determine if the thread is endorsed. @@ -187,7 +185,7 @@ def get_abuse_flagged_count(self, obj: dict[str, Any]) -> int: return self.backend.get_abuse_flagged_count([thread_key]).get(thread_key, 0) return 0 - def get_children(self, obj: dict[str, Any]) -> Optional[Any]: + def get_children(self, obj: dict[str, Any]) -> Any | None: """ Retrieve the children (responses) for the thread if applicable. @@ -260,10 +258,7 @@ def to_representation(self, instance: dict[str, Any]) -> dict[str, Any]: data.pop("close_reason_code", None) if ( self.with_responses - and ( - not ("recursive" in self.context_data) - or self.context_data.get("recursive") is True - ) + and ("recursive" not in self.context_data or self.context_data.get("recursive") is True) and data.get("thread_type") == "question" and not self.merge_question_type_responses ): @@ -288,7 +283,7 @@ def update(self, instance: Any, validated_data: dict[str, Any]) -> Any: """Raise NotImplementedError""" raise NotImplementedError - def get_closed_by(self, obj: dict[str, Any]) -> Optional[str]: + def get_closed_by(self, obj: dict[str, Any]) -> str | None: """Retrieve the username of the person who closed the object.""" if closed_by_id := obj.get("closed_by_id"): return self.backend.get_username_from_id(closed_by_id) diff --git a/forum/serializers/users.py b/forum/serializers/users.py index 1637087e..1dde8597 100644 --- a/forum/serializers/users.py +++ b/forum/serializers/users.py @@ -12,15 +12,9 @@ class UserSerializer(serializers.Serializer[Any]): username = serializers.CharField() email = serializers.CharField(allow_null=True) external_id = serializers.CharField() - subscribed_thread_ids = serializers.ListField( - child=serializers.CharField(), default=[] - ) - subscribed_commentable_ids = serializers.ListField( - child=serializers.CharField(), default=[] - ) - subscribed_user_ids = serializers.ListField( - child=serializers.CharField(), default=[] - ) + subscribed_thread_ids = serializers.ListField(child=serializers.CharField(), default=[]) + subscribed_commentable_ids = serializers.ListField(child=serializers.CharField(), default=[]) + subscribed_user_ids = serializers.ListField(child=serializers.CharField(), default=[]) follower_ids = serializers.ListField(child=serializers.CharField(), default=[]) upvoted_ids = serializers.ListField(child=serializers.CharField(), default=[]) downvoted_ids = serializers.ListField(child=serializers.CharField(), default=[]) diff --git a/forum/settings/common.py b/forum/settings/common.py index 772fc9e7..8f969080 100644 --- a/forum/settings/common.py +++ b/forum/settings/common.py @@ -32,21 +32,15 @@ def plugin_settings(settings: Any) -> None: # Unfortunately we can't copy settings from edx-platform because tutor patches have # not been applied yet - settings.FORUM_MONGODB_DATABASE = getattr( - settings, "FORUM_MONGODB_DATABASE", "cs_comments_service" - ) - settings.FORUM_MONGODB_CLIENT_PARAMETERS = getattr( - settings, "FORUM_MONGODB_CLIENT_PARAMETERS", {"host": "mongodb"} - ) + settings.FORUM_MONGODB_DATABASE = getattr(settings, "FORUM_MONGODB_DATABASE", "cs_comments_service") + settings.FORUM_MONGODB_CLIENT_PARAMETERS = getattr(settings, "FORUM_MONGODB_CLIENT_PARAMETERS", {"host": "mongodb"}) # Enable forum service if "ENABLE_DISCUSSION_SERVICE" not in settings.FEATURES: settings.FEATURES["ENABLE_DISCUSSION_SERVICE"] = True # URL prefix must match the regex in the url_config of the plugin app - settings.COMMENTS_SERVICE_URL = getattr( - settings, "COMMENTS_SERVICE_URL", "http://localhost:8000/forum" - ) + settings.COMMENTS_SERVICE_URL = getattr(settings, "COMMENTS_SERVICE_URL", "http://localhost:8000/forum") # Timezone-awareness is required for mysql fields settings.USE_TZ = getattr(settings, "USE_TZ", True) diff --git a/forum/utils.py b/forum/utils.py index 15778672..ce11b761 100644 --- a/forum/utils.py +++ b/forum/utils.py @@ -1,8 +1,9 @@ """Forum Utils.""" import logging +from collections.abc import Sequence from datetime import datetime -from typing import Any, Sequence +from typing import Any import requests from django.conf import settings @@ -27,9 +28,7 @@ def handle_proxy_requests(request: HttpRequest, suffix: str, method: str) -> Res Returns: Response: The response from the proxied service. """ - comments_service_url = getattr( - settings, "CS_COMMENTS_SERVICE_URL", "http://forum:4567" - ) + comments_service_url = getattr(settings, "CS_COMMENTS_SERVICE_URL", "http://forum:4567") url = f"{comments_service_url}/api/v1/{suffix}" request_headers = { "X-Edx-Api-Key": request.headers.get("X-Edx-Api-Key", ""), @@ -74,9 +73,7 @@ def str_to_bool(value: str | bool) -> bool: return value.lower() in ("true", "1") -def get_int_value_from_collection( - collection: dict[str, Any], key: str, default_value: int -) -> int: +def get_int_value_from_collection(collection: dict[str, Any], key: str, default_value: int) -> int: """ Get int value from the collection. """ diff --git a/forum/views/comments.py b/forum/views/comments.py index ed90507c..4f221152 100644 --- a/forum/views/comments.py +++ b/forum/views/comments.py @@ -8,8 +8,8 @@ from rest_framework.views import APIView from forum.api import ( - create_parent_comment, create_child_comment, + create_parent_comment, delete_comment, get_parent_comment, update_comment, diff --git a/forum/views/flags.py b/forum/views/flags.py index 9f1df8bc..c2dd8a55 100644 --- a/forum/views/flags.py +++ b/forum/views/flags.py @@ -35,9 +35,7 @@ def put(self, request: Request, comment_id: str, action: str) -> Response: update_all = str_to_bool(request_data.get("all", False)) user_id = request_data.get("user_id") try: - serializer_data = update_comment_flag( - comment_id, action, user_id, update_all - ) + serializer_data = update_comment_flag(comment_id, action, user_id, update_all) return Response(serializer_data, status=status.HTTP_200_OK) except ForumV2RequestError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) diff --git a/forum/views/pins.py b/forum/views/pins.py index 4260ab98..c32ab1f2 100644 --- a/forum/views/pins.py +++ b/forum/views/pins.py @@ -35,9 +35,7 @@ def put(self, request: Request, thread_id: str) -> Response: A response with the updated thread data. """ try: - thread_data: dict[str, Any] = pin_thread( - request.data.get("user_id", ""), thread_id - ) + thread_data: dict[str, Any] = pin_thread(request.data.get("user_id", ""), thread_id) except ForumV2RequestError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @@ -64,9 +62,7 @@ def put(self, request: Request, thread_id: str) -> Response: A response with the updated thread data. """ try: - thread_data: dict[str, Any] = unpin_thread( - request.data.get("user_id", ""), thread_id - ) + thread_data: dict[str, Any] = unpin_thread(request.data.get("user_id", ""), thread_id) except ForumV2RequestError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) diff --git a/forum/views/users.py b/forum/views/users.py index c148d58d..0c75ecbd 100644 --- a/forum/views/users.py +++ b/forum/views/users.py @@ -41,9 +41,7 @@ def get(self, request: Request, user_id: str) -> Response: complete = str_to_bool(params.get("complete", False)) try: - user_data: dict[str, Any] = get_user( - user_id, group_ids, course_id, complete - ) + user_data: dict[str, Any] = get_user(user_id, group_ids, course_id, complete) except ForumV2RequestError as e: return Response({"error": str(e)}, status=status.HTTP_404_NOT_FOUND) @@ -212,9 +210,7 @@ def get(self, request: Request, user_id: str) -> Response: """Return thread_count and comment_count for user in course.""" course_id = request.query_params.get("course_id") if not course_id: - return Response( - {"error": "course_id is required"}, status=status.HTTP_400_BAD_REQUEST - ) + return Response({"error": "course_id is required"}, status=status.HTTP_400_BAD_REQUEST) try: data = get_user_post_counts(user_id, course_id) except ForumV2RequestError as e: @@ -225,9 +221,7 @@ def delete(self, request: Request, user_id: str) -> Response: """Delete all posts by user in course. Returns counts before deletion.""" course_id = request.query_params.get("course_id") if not course_id: - return Response( - {"error": "course_id is required"}, status=status.HTTP_400_BAD_REQUEST - ) + return Response({"error": "course_id is required"}, status=status.HTTP_400_BAD_REQUEST) try: data = delete_user_posts(user_id, course_id) except ForumV2RequestError as e: diff --git a/forum/views/votes.py b/forum/views/votes.py index 2153c5cd..e29a395d 100644 --- a/forum/views/votes.py +++ b/forum/views/votes.py @@ -53,9 +53,7 @@ def put(self, request: Request, thread_id: str) -> Response: Response: The HTTP response with the result of the vote operation. """ try: - thread_response = update_thread_votes( - thread_id, request.data["user_id"], request.data["value"] - ) + thread_response = update_thread_votes(thread_id, request.data["user_id"], request.data["value"]) except (ForumV2RequestError, KeyError) as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @@ -118,9 +116,7 @@ def put(self, request: Request, comment_id: str) -> Response: Response: The HTTP response with the result of the vote operation. """ try: - comment_response = update_comment_votes( - comment_id, request.data["user_id"], request.data["value"] - ) + comment_response = update_comment_votes(comment_id, request.data["user_id"], request.data["value"]) except (ForumV2RequestError, KeyError) as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 3403872d..00000000 --- a/mypy.ini +++ /dev/null @@ -1,23 +0,0 @@ -[mypy] -python_version = 3.12 - -disallow_untyped_defs = True -strict_optional = True -check_untyped_defs = True -disallow_any_generics = True -disallow_untyped_calls = True -disallow_untyped_decorators = True -ignore_errors = False -ignore_missing_imports = False -implicit_reexport = False -strict_equality = True -no_implicit_optional = True -warn_unused_ignores = True -warn_redundant_casts = True -warn_unused_configs = True -warn_unreachable = True -warn_no_return = True - -[mypy-search.*] -; Let's ignore edx-search missing types until they are added to the repo. -ignore_missing_imports = True diff --git a/pylintrc b/pylintrc deleted file mode 100644 index fa446352..00000000 --- a/pylintrc +++ /dev/null @@ -1,393 +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.6 -# ------------------------------ -[MASTER] -ignore = migrations -persistent = yes -load-plugins = edx_lint.pylint,pylint_django,pylint_celery - -[MESSAGES CONTROL] -enable = - blacklisted-name, - - 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, - 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, - line-too-long, - locally-disabled, - no-else-return, - pointless-string-statement, - 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, - invalid-name, - django-not-configured, - consider-using-with, - bad-option-value, - missing-timeout, - useless-suppression, - too-many-positional-arguments, - -[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,OneToOneField,ForeignKey,DateTimeField -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 - -# 22aad72373290794ac5bd89746f80d5c1bd95e25 diff --git a/pylintrc_tweaks b/pylintrc_tweaks deleted file mode 100644 index 7b6eb35c..00000000 --- a/pylintrc_tweaks +++ /dev/null @@ -1,11 +0,0 @@ -# pylintrc tweaks for use with edx_lint. -[MASTER] -ignore = migrations -load-plugins = edx_lint.pylint,pylint_django,pylint_celery - -[MESSAGES CONTROL] -disable+= - invalid-name, - django-not-configured, - consider-using-with, - bad-option-value, diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..ffceeb3f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,244 @@ +[build-system] +requires = ["setuptools>=61.0", "setuptools-scm>=8.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "openedx-forum" +dynamic = ["version"] +description = "Open edX forum application" +readme = "README.rst" +requires-python = ">=3.12" +license = "AGPL-3.0-only" +license-files = ["LICENSE.txt"] +authors = [ + {name = "Open edX Project", email = "oscm@openedx.org"}, +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Framework :: Django", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.2", + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", +] +keywords = [ + "Python", + "edx", + "forum", + "openedx", +] +dependencies = [ + "Django>=4.2", + "beautifulsoup4", + "djangorestframework", + "openedx-atlas", + "requests", + "pymongo", + "elasticsearch", + "edx-search", + "typesense", + "mysqlclient", +] + +[project.urls] +Repository = "https://github.com/openedx/forum" + +[project.entry-points."lms.djangoapp"] +forum = "forum.apps:ForumConfig" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +include = ["forum", "forum.*"] +exclude = ["*tests"] + +[tool.setuptools.package-data] +"*" = [ + "conf/**/*", + "static/**/*", + "templates/**/*", + "translations/**/*", +] + +# Setuptools SCM configuration +# https://setuptools-scm.readthedocs.io/ +[tool.setuptools_scm] +version_scheme = "only-version" +local_scheme = "no-local-version" + +# uv configuration +# https://docs.astral.sh/uv/reference/settings/ +[tool.uv] +package = true +conflicts = [ + [ + {group = "test"}, + {group = "django42"}, + ], +] + +# constraint-dependencies is machine-managed by edx-lint — do not edit directly. +# To regenerate, run: uv run --with edx-lint edx_lint write_uv_constraints pyproject.toml +constraint-dependencies = [ + "Django<6.0", + "elasticsearch<7.14.0", +] + +[dependency-groups] +# Base test packages, no Django version pinned +test-base = [ + "coverage", + "pytest-cov", + "pytest-django", + "pytest>=7.0", + "code-annotations", + "mongomock", + "Faker", + "build", + "twine", + "edx-lint", +] +# Default test group: current supported Django version +# Update Django pin here when bumping the default; add a legacy group for older versions +test = [ + {include-group = "test-base"}, + "Django>=5.2,<6.0", +] +# Legacy Django 4.2 support +django42 = [ + {include-group = "test-base"}, + "Django>=4.2,<5.0", +] +quality = [ + {include-group = "test"}, + "ruff", + "mypy", + "djangorestframework-stubs", + "types-beautifulsoup4", + "types-requests", +] +doc = [ + {include-group = "test"}, + "doc8", + "sphinx-book-theme", + "Sphinx", +] +ci = [ + "tox", + "tox-uv", +] +dev = [ + {include-group = "quality"}, + {include-group = "ci"}, + "diff-cover", + "edx-i18n-tools", +] + +[tool.edx_lint] +uv_constraints = [] + +# 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) + "DJ001", # null=True on string-based fields (pre-existing, requires migrations to fix) + "DJ008", # Model missing __str__ (pre-existing, not enforced before) + "DJ012", # Model method order (pre-existing, not enforced before) +] + +[tool.ruff.lint.per-file-ignores] +"forum/models.py" = ["F401", "F403"] # intentional star-import re-export +"manage.py" = ["F401"] # intentional django import for availability check + +[tool.ruff.lint.isort] +known-third-party = ["django", "rest_framework", "pymongo", "elasticsearch", "typesense", "bs4"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + +# Pytest configuration +# https://docs.pytest.org/en/stable/reference/customize.html +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "forum.settings.test" +addopts = "--cov forum --cov tests --cov-report term-missing --cov-report xml" +norecursedirs = [".*", "docs", "requirements", "site-packages", "e2e"] + +# Coverage configuration +# https://coverage.readthedocs.io/en/latest/config.html +[tool.coverage.run] +branch = true +source = ["forum"] +omit = [ + "forum/settings/test.py", + "*/migrations/*", + "*admin.py", + "*/static/*", + "*/templates/*", + "tests/e2e/*", +] + +[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] +python_version = "3.12" + +disallow_untyped_defs = true +strict_optional = true +check_untyped_defs = true +disallow_any_generics = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +ignore_errors = false +ignore_missing_imports = false +implicit_reexport = false +strict_equality = true +no_implicit_optional = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unreachable = true +warn_no_return = true + +[[tool.mypy.overrides]] +module = "search.*" +ignore_missing_imports = true + +# Semantic Release configuration +# https://python-semantic-release.readthedocs.io/ +[tool.semantic_release] +# Use SETUPTOOLS_SCM_PRETEND_VERSION to set version at build time since the +# build step happens before the repo gets tagged. +build_command = "pip install build && SETUPTOOLS_SCM_PRETEND_VERSION=$NEW_VERSION python -m build" diff --git a/requirements/base.in b/requirements/base.in deleted file mode 100644 index 311e9e10..00000000 --- a/requirements/base.in +++ /dev/null @@ -1,15 +0,0 @@ -# Core requirements for using this application --c constraints.txt - -Django # Web application framework - - -beautifulsoup4 -djangorestframework -openedx-atlas -requests -pymongo -elasticsearch -edx-search # meilisearch backend -typesense -mysqlclient diff --git a/requirements/base.txt b/requirements/base.txt deleted file mode 100644 index 7de3feff..00000000 --- a/requirements/base.txt +++ /dev/null @@ -1,203 +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 -annotated-types==0.7.0 - # via pydantic -anyio==4.12.1 - # via httpx -asgiref==3.11.1 - # via django -attrs==25.4.0 - # via openedx-events -beautifulsoup4==4.14.3 - # via -r requirements/base.in -billiard==4.2.4 - # via celery -camel-converter[pydantic]==5.1.0 - # via meilisearch -celery==5.6.2 - # via event-tracking -certifi==2026.2.25 - # via - # elasticsearch - # httpcore - # httpx - # requests -cffi==2.0.0 - # via pynacl -charset-normalizer==3.4.5 - # via requests -click==8.3.1 - # via - # celery - # click-didyoumean - # click-plugins - # click-repl - # code-annotations - # edx-django-utils -click-didyoumean==0.3.1 - # via celery -click-plugins==1.1.1.2 - # via celery -click-repl==0.3.0 - # via celery -code-annotations==2.3.2 - # via edx-toggles -django==5.2.12 - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/base.in - # django-crum - # django-waffle - # djangorestframework - # edx-django-utils - # edx-search - # edx-toggles - # event-tracking - # openedx-events -django-crum==0.7.9 - # via - # edx-django-utils - # edx-toggles -django-waffle==5.0.0 - # via - # edx-django-utils - # edx-toggles -djangorestframework==3.16.1 - # via -r requirements/base.in -dnspython==2.8.0 - # via pymongo -edx-ccx-keys==2.0.2 - # via openedx-events -edx-django-utils==8.0.1 - # via - # edx-toggles - # event-tracking - # openedx-events -edx-opaque-keys[django]==3.1.0 - # via - # edx-ccx-keys - # openedx-events -edx-search==4.4.0 - # via -r requirements/base.in -edx-toggles==5.4.1 - # via - # edx-search - # event-tracking -elasticsearch==7.13.4 - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/base.in - # edx-search -event-tracking==3.3.0 - # via edx-search -fastavro==1.12.1 - # via openedx-events -h11==0.16.0 - # via httpcore -httpcore==1.0.9 - # via httpx -httpx==0.28.1 - # via typesense -idna==3.11 - # via - # anyio - # httpx - # requests -jinja2==3.1.6 - # via code-annotations -kombu==5.6.2 - # via celery -markupsafe==3.0.3 - # via jinja2 -meilisearch==0.40.0 - # via edx-search -mysqlclient==2.2.8 - # via -r requirements/base.in -openedx-atlas==0.7.0 - # via -r requirements/base.in -openedx-events==10.5.0 - # via event-tracking -packaging==26.0 - # via kombu -prompt-toolkit==3.0.52 - # via click-repl -psutil==7.2.2 - # via edx-django-utils -pycparser==3.0 - # via cffi -pydantic==2.12.5 - # via camel-converter -pydantic-core==2.41.5 - # via pydantic -pymongo==4.16.0 - # via - # -r requirements/base.in - # edx-opaque-keys - # event-tracking -pynacl==1.6.2 - # via edx-django-utils -python-dateutil==2.9.0.post0 - # via celery -python-slugify==8.0.4 - # via code-annotations -pytz==2026.1.post1 - # via event-tracking -pyyaml==6.0.3 - # via code-annotations -requests==2.32.5 - # via - # -r requirements/base.in - # meilisearch -six==1.17.0 - # via - # edx-ccx-keys - # event-tracking - # python-dateutil -soupsieve==2.8.3 - # via beautifulsoup4 -sqlparse==0.5.5 - # via django -stevedore==5.7.0 - # via - # code-annotations - # edx-django-utils - # edx-opaque-keys -text-unidecode==1.3 - # via python-slugify -typesense==2.0.0 - # via -r requirements/base.in -typing-extensions==4.15.0 - # via - # anyio - # beautifulsoup4 - # edx-opaque-keys - # pydantic - # pydantic-core - # typesense - # typing-inspection -typing-inspection==0.4.2 - # via pydantic -tzdata==2025.3 - # via kombu -tzlocal==5.3.1 - # via celery -urllib3==1.26.20 - # via - # elasticsearch - # requests -vine==5.1.0 - # via - # amqp - # celery - # kombu -wcwidth==0.6.0 - # via prompt-toolkit - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/requirements/ci.in b/requirements/ci.in deleted file mode 100644 index ac57c6a8..00000000 --- a/requirements/ci.in +++ /dev/null @@ -1,7 +0,0 @@ -# Requirements for running tests in CI - --c constraints.txt - --r quality.txt # Core and quality check dependencies - -black # code formatter diff --git a/requirements/ci.txt b/requirements/ci.txt deleted file mode 100644 index 26e82e98..00000000 --- a/requirements/ci.txt +++ /dev/null @@ -1,593 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# make upgrade -# -amqp==5.3.1 - # via - # -r requirements/quality.txt - # kombu -annotated-types==0.7.0 - # via - # -r requirements/quality.txt - # pydantic -anyio==4.12.1 - # via - # -r requirements/quality.txt - # httpx -asgiref==3.11.1 - # via - # -r requirements/quality.txt - # django -astroid==4.0.4 - # via - # -r requirements/quality.txt - # pylint - # pylint-celery -attrs==25.4.0 - # via - # -r requirements/quality.txt - # openedx-events -beautifulsoup4==4.14.3 - # via -r requirements/quality.txt -billiard==4.2.4 - # via - # -r requirements/quality.txt - # celery -black==26.3.0 - # via -r requirements/ci.in -build==1.4.0 - # via -r requirements/quality.txt -cachetools==7.0.3 - # via - # -r requirements/quality.txt - # tox -camel-converter[pydantic]==5.1.0 - # via - # -r requirements/quality.txt - # meilisearch -celery==5.6.2 - # via - # -r requirements/quality.txt - # event-tracking -certifi==2026.2.25 - # via - # -r requirements/quality.txt - # elasticsearch - # httpcore - # httpx - # requests -cffi==2.0.0 - # via - # -r requirements/quality.txt - # cryptography - # pynacl -charset-normalizer==3.4.5 - # via - # -r requirements/quality.txt - # requests -click==8.3.1 - # via - # -r requirements/quality.txt - # black - # celery - # click-didyoumean - # click-log - # click-plugins - # click-repl - # code-annotations - # edx-django-utils - # edx-lint -click-didyoumean==0.3.1 - # via - # -r requirements/quality.txt - # celery -click-log==0.4.0 - # via - # -r requirements/quality.txt - # edx-lint -click-plugins==1.1.1.2 - # via - # -r requirements/quality.txt - # celery -click-repl==0.3.0 - # via - # -r requirements/quality.txt - # celery -code-annotations==2.3.2 - # via - # -r requirements/quality.txt - # edx-lint - # edx-toggles -colorama==0.4.6 - # via - # -r requirements/quality.txt - # tox -coverage[toml]==7.13.4 - # via - # -r requirements/quality.txt - # pytest-cov -cryptography==46.0.5 - # via - # -r requirements/quality.txt - # secretstorage -dill==0.4.1 - # via - # -r requirements/quality.txt - # pylint -distlib==0.4.0 - # via - # -r requirements/quality.txt - # virtualenv -django==5.2.12 - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/quality.txt - # django-crum - # django-stubs - # django-stubs-ext - # django-waffle - # djangorestframework - # edx-django-utils - # edx-search - # edx-toggles - # event-tracking - # openedx-events -django-crum==0.7.9 - # via - # -r requirements/quality.txt - # edx-django-utils - # edx-toggles -django-stubs==5.2.9 - # via - # -r requirements/quality.txt - # djangorestframework-stubs -django-stubs-ext==5.2.9 - # via - # -r requirements/quality.txt - # django-stubs -django-waffle==5.0.0 - # via - # -r requirements/quality.txt - # edx-django-utils - # edx-toggles -djangorestframework==3.16.1 - # via -r requirements/quality.txt -djangorestframework-stubs==3.16.8 - # via -r requirements/quality.txt -dnspython==2.8.0 - # via - # -r requirements/quality.txt - # pymongo -docutils==0.22.4 - # via - # -r requirements/quality.txt - # readme-renderer -edx-ccx-keys==2.0.2 - # via - # -r requirements/quality.txt - # openedx-events -edx-django-utils==8.0.1 - # via - # -r requirements/quality.txt - # edx-toggles - # event-tracking - # openedx-events -edx-lint==5.6.0 - # via -r requirements/quality.txt -edx-opaque-keys[django]==3.1.0 - # via - # -r requirements/quality.txt - # edx-ccx-keys - # openedx-events -edx-search==4.4.0 - # via -r requirements/quality.txt -edx-toggles==5.4.1 - # via - # -r requirements/quality.txt - # edx-search - # event-tracking -elasticsearch==7.13.4 - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/quality.txt - # edx-search -event-tracking==3.3.0 - # via - # -r requirements/quality.txt - # edx-search -faker==40.8.0 - # via -r requirements/quality.txt -fastavro==1.12.1 - # via - # -r requirements/quality.txt - # openedx-events -filelock==3.25.0 - # via - # -r requirements/quality.txt - # python-discovery - # tox - # virtualenv -h11==0.16.0 - # via - # -r requirements/quality.txt - # httpcore -httpcore==1.0.9 - # via - # -r requirements/quality.txt - # httpx -httpx==0.28.1 - # via - # -r requirements/quality.txt - # typesense -id==1.5.0 - # via - # -r requirements/quality.txt - # twine -idna==3.11 - # via - # -r requirements/quality.txt - # anyio - # httpx - # requests -iniconfig==2.3.0 - # via - # -r requirements/quality.txt - # pytest -isort==8.0.1 - # via - # -r requirements/quality.txt - # pylint -jaraco-classes==3.4.0 - # via - # -r requirements/quality.txt - # keyring -jaraco-context==6.1.0 - # via - # -r requirements/quality.txt - # keyring -jaraco-functools==4.4.0 - # via - # -r requirements/quality.txt - # keyring -jeepney==0.9.0 - # via - # -r requirements/quality.txt - # keyring - # secretstorage -jinja2==3.1.6 - # via - # -r requirements/quality.txt - # code-annotations -keyring==25.7.0 - # via - # -r requirements/quality.txt - # twine -kombu==5.6.2 - # via - # -r requirements/quality.txt - # celery -librt==0.8.1 - # via - # -r requirements/quality.txt - # mypy -markdown-it-py==4.0.0 - # via - # -r requirements/quality.txt - # rich -markupsafe==3.0.3 - # via - # -r requirements/quality.txt - # jinja2 -mccabe==0.7.0 - # via - # -r requirements/quality.txt - # pylint -mdurl==0.1.2 - # via - # -r requirements/quality.txt - # markdown-it-py -meilisearch==0.40.0 - # via - # -r requirements/quality.txt - # edx-search -mongomock==4.3.0 - # via -r requirements/quality.txt -more-itertools==10.8.0 - # via - # -r requirements/quality.txt - # jaraco-classes - # jaraco-functools -mypy==1.19.1 - # via -r requirements/quality.txt -mypy-extensions==1.1.0 - # via - # -r requirements/quality.txt - # black - # mypy -mysqlclient==2.2.8 - # via -r requirements/quality.txt -nh3==0.3.3 - # via - # -r requirements/quality.txt - # readme-renderer -openedx-atlas==0.7.0 - # via -r requirements/quality.txt -openedx-events==10.5.0 - # via - # -r requirements/quality.txt - # event-tracking -packaging==26.0 - # via - # -r requirements/quality.txt - # black - # build - # kombu - # mongomock - # pyproject-api - # pytest - # tox - # twine -pathspec==1.0.4 - # via - # -r requirements/quality.txt - # black - # mypy -platformdirs==4.9.4 - # via - # -r requirements/quality.txt - # black - # pylint - # python-discovery - # tox - # virtualenv -pluggy==1.6.0 - # via - # -r requirements/quality.txt - # pytest - # pytest-cov - # tox -prompt-toolkit==3.0.52 - # via - # -r requirements/quality.txt - # click-repl -psutil==7.2.2 - # via - # -r requirements/quality.txt - # edx-django-utils -pycodestyle==2.14.0 - # via -r requirements/quality.txt -pycparser==3.0 - # via - # -r requirements/quality.txt - # cffi -pydantic==2.12.5 - # via - # -r requirements/quality.txt - # camel-converter -pydantic-core==2.41.5 - # via - # -r requirements/quality.txt - # pydantic -pydocstyle==6.3.0 - # via -r requirements/quality.txt -pygments==2.19.2 - # via - # -r requirements/quality.txt - # pytest - # readme-renderer - # rich -pylint==4.0.5 - # via - # -r requirements/quality.txt - # edx-lint - # pylint-celery - # pylint-django - # pylint-plugin-utils -pylint-celery==0.3 - # via - # -r requirements/quality.txt - # edx-lint -pylint-django==2.7.0 - # via - # -r requirements/quality.txt - # edx-lint -pylint-plugin-utils==0.9.0 - # via - # -r requirements/quality.txt - # pylint-celery - # pylint-django -pymongo==4.16.0 - # via - # -r requirements/quality.txt - # edx-opaque-keys - # event-tracking -pynacl==1.6.2 - # via - # -r requirements/quality.txt - # edx-django-utils -pyproject-api==1.10.0 - # via - # -r requirements/quality.txt - # tox -pyproject-hooks==1.2.0 - # via - # -r requirements/quality.txt - # build -pytest==9.0.2 - # via - # -r requirements/quality.txt - # pytest-cov - # pytest-django -pytest-cov==7.0.0 - # via -r requirements/quality.txt -pytest-django==4.12.0 - # via -r requirements/quality.txt -python-dateutil==2.9.0.post0 - # via - # -r requirements/quality.txt - # celery -python-discovery==1.1.0 - # via - # -r requirements/quality.txt - # virtualenv -python-slugify==8.0.4 - # via - # -r requirements/quality.txt - # code-annotations -pytokens==0.4.1 - # via black -pytz==2026.1.post1 - # via - # -r requirements/quality.txt - # event-tracking - # mongomock -pyyaml==6.0.3 - # via - # -r requirements/quality.txt - # code-annotations -readme-renderer==44.0 - # via - # -r requirements/quality.txt - # twine -requests==2.32.5 - # via - # -r requirements/quality.txt - # id - # meilisearch - # requests-toolbelt - # twine -requests-toolbelt==1.0.0 - # via - # -r requirements/quality.txt - # twine -rfc3986==2.0.0 - # via - # -r requirements/quality.txt - # twine -rich==14.3.3 - # via - # -r requirements/quality.txt - # twine -secretstorage==3.5.0 - # via - # -r requirements/quality.txt - # keyring -sentinels==1.1.1 - # via - # -r requirements/quality.txt - # mongomock -six==1.17.0 - # via - # -r requirements/quality.txt - # edx-ccx-keys - # edx-lint - # event-tracking - # python-dateutil -snowballstemmer==3.0.1 - # via - # -r requirements/quality.txt - # pydocstyle -soupsieve==2.8.3 - # via - # -r requirements/quality.txt - # beautifulsoup4 -sqlparse==0.5.5 - # via - # -r requirements/quality.txt - # django -stevedore==5.7.0 - # via - # -r requirements/quality.txt - # code-annotations - # edx-django-utils - # edx-opaque-keys -text-unidecode==1.3 - # via - # -r requirements/quality.txt - # python-slugify -tomli-w==1.2.0 - # via - # -r requirements/quality.txt - # tox -tomlkit==0.14.0 - # via - # -r requirements/quality.txt - # pylint -tox==4.49.0 - # via -r requirements/quality.txt -twine==6.2.0 - # via -r requirements/quality.txt -types-beautifulsoup4==4.12.0.20250516 - # via -r requirements/quality.txt -types-html5lib==1.1.11.20251117 - # via - # -r requirements/quality.txt - # types-beautifulsoup4 -types-pyyaml==6.0.12.20250915 - # via - # -r requirements/quality.txt - # django-stubs - # djangorestframework-stubs -types-requests==2.31.0.6 - # via -r requirements/quality.txt -types-urllib3==1.26.25.14 - # via - # -r requirements/quality.txt - # types-requests -types-webencodings==0.5.0.20251108 - # via - # -r requirements/quality.txt - # types-html5lib -typesense==2.0.0 - # via -r requirements/quality.txt -typing-extensions==4.15.0 - # via - # -r requirements/quality.txt - # anyio - # beautifulsoup4 - # django-stubs - # django-stubs-ext - # djangorestframework-stubs - # edx-opaque-keys - # mypy - # pydantic - # pydantic-core - # typesense - # typing-inspection -typing-inspection==0.4.2 - # via - # -r requirements/quality.txt - # pydantic -tzdata==2025.3 - # via - # -r requirements/quality.txt - # kombu -tzlocal==5.3.1 - # via - # -r requirements/quality.txt - # celery -urllib3==1.26.20 - # via - # -r requirements/quality.txt - # elasticsearch - # requests - # twine -vine==5.1.0 - # via - # -r requirements/quality.txt - # amqp - # celery - # kombu -virtualenv==21.1.0 - # via - # -r requirements/quality.txt - # tox -wcwidth==0.6.0 - # via - # -r requirements/quality.txt - # prompt-toolkit - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/requirements/constraints.txt b/requirements/constraints.txt deleted file mode 100644 index e1a30b9f..00000000 --- a/requirements/constraints.txt +++ /dev/null @@ -1,14 +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 edx repos --c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - -backports.zoneinfo ; python_version < "3.9" diff --git a/requirements/dev.in b/requirements/dev.in deleted file mode 100644 index eb8c92c5..00000000 --- a/requirements/dev.in +++ /dev/null @@ -1,9 +0,0 @@ -# Additional requirements for development of this application --c constraints.txt - --r pip-tools.txt # pip-tools and its dependencies, for managing requirements files --r quality.txt # Core and quality check dependencies --r ci.txt # dependencies for setting up testing in CI - -diff-cover # Changeset diff test coverage -edx-i18n-tools # For i18n_tool dummy diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index 901311a1..00000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,785 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# make upgrade -# -amqp==5.3.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # kombu -annotated-types==0.7.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pydantic -anyio==4.12.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # httpx -asgiref==3.11.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # django -astroid==4.0.4 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pylint - # pylint-celery -attrs==25.4.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # openedx-events -beautifulsoup4==4.14.3 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -billiard==4.2.4 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # celery -black==26.3.0 - # via -r requirements/ci.txt -build==1.4.0 - # via - # -r requirements/ci.txt - # -r requirements/pip-tools.txt - # -r requirements/quality.txt - # pip-tools -cachetools==7.0.3 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # tox -camel-converter[pydantic]==5.1.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # meilisearch -celery==5.6.2 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # event-tracking -certifi==2026.2.25 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # elasticsearch - # httpcore - # httpx - # requests -cffi==2.0.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # cryptography - # pynacl -chardet==7.0.1 - # via diff-cover -charset-normalizer==3.4.5 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # requests -click==8.3.1 - # via - # -r requirements/ci.txt - # -r requirements/pip-tools.txt - # -r requirements/quality.txt - # black - # celery - # click-didyoumean - # click-log - # click-plugins - # click-repl - # code-annotations - # edx-django-utils - # edx-lint - # pip-tools -click-didyoumean==0.3.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # celery -click-log==0.4.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-lint -click-plugins==1.1.1.2 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # celery -click-repl==0.3.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # celery -code-annotations==2.3.2 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-lint - # edx-toggles -colorama==0.4.6 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # tox -coverage[toml]==7.13.4 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pytest-cov -cryptography==46.0.5 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # secretstorage -diff-cover==10.2.0 - # via -r requirements/dev.in -dill==0.4.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pylint -distlib==0.4.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # virtualenv -django==5.2.12 - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/ci.txt - # -r requirements/quality.txt - # django-crum - # django-stubs - # django-stubs-ext - # django-waffle - # djangorestframework - # edx-django-utils - # edx-i18n-tools - # edx-search - # edx-toggles - # event-tracking - # openedx-events -django-crum==0.7.9 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-django-utils - # edx-toggles -django-stubs==5.2.9 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # djangorestframework-stubs -django-stubs-ext==5.2.9 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # django-stubs -django-waffle==5.0.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-django-utils - # edx-toggles -djangorestframework==3.16.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -djangorestframework-stubs==3.16.8 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -dnspython==2.8.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pymongo -docutils==0.22.4 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # readme-renderer -edx-ccx-keys==2.0.2 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # openedx-events -edx-django-utils==8.0.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-toggles - # event-tracking - # openedx-events -edx-i18n-tools==1.9.0 - # via -r requirements/dev.in -edx-lint==5.6.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -edx-opaque-keys[django]==3.1.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-ccx-keys - # openedx-events -edx-search==4.4.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -edx-toggles==5.4.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-search - # event-tracking -elasticsearch==7.13.4 - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-search -event-tracking==3.3.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-search -faker==40.8.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -fastavro==1.12.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # openedx-events -filelock==3.25.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # python-discovery - # tox - # virtualenv -h11==0.16.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # httpcore -httpcore==1.0.9 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # httpx -httpx==0.28.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # typesense -id==1.5.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # twine -idna==3.11 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # anyio - # httpx - # requests -iniconfig==2.3.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pytest -isort==8.0.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pylint -jaraco-classes==3.4.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # keyring -jaraco-context==6.1.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # keyring -jaraco-functools==4.4.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # keyring -jeepney==0.9.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # keyring - # secretstorage -jinja2==3.1.6 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # code-annotations - # diff-cover -keyring==25.7.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # twine -kombu==5.6.2 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # celery -librt==0.8.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # mypy -lxml[html-clean]==6.0.2 - # via - # edx-i18n-tools - # lxml-html-clean -lxml-html-clean==0.4.4 - # via lxml -markdown-it-py==4.0.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # rich -markupsafe==3.0.3 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # jinja2 -mccabe==0.7.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pylint -mdurl==0.1.2 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # markdown-it-py -meilisearch==0.40.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-search -mongomock==4.3.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -more-itertools==10.8.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # jaraco-classes - # jaraco-functools -mypy==1.19.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -mypy-extensions==1.1.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # black - # mypy -mysqlclient==2.2.8 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -nh3==0.3.3 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # readme-renderer -openedx-atlas==0.7.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -openedx-events==10.5.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # event-tracking -packaging==26.0 - # via - # -r requirements/ci.txt - # -r requirements/pip-tools.txt - # -r requirements/quality.txt - # black - # build - # kombu - # mongomock - # pyproject-api - # pytest - # tox - # twine - # wheel -path==16.16.0 - # via edx-i18n-tools -pathspec==1.0.4 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # black - # mypy -pip-tools==7.5.3 - # via -r requirements/pip-tools.txt -platformdirs==4.9.4 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # black - # pylint - # python-discovery - # tox - # virtualenv -pluggy==1.6.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # diff-cover - # pytest - # pytest-cov - # tox -polib==1.2.0 - # via edx-i18n-tools -prompt-toolkit==3.0.52 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # click-repl -psutil==7.2.2 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-django-utils -pycodestyle==2.14.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -pycparser==3.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # cffi -pydantic==2.12.5 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # camel-converter -pydantic-core==2.41.5 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pydantic -pydocstyle==6.3.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -pygments==2.19.2 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # diff-cover - # pytest - # readme-renderer - # rich -pylint==4.0.5 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-lint - # pylint-celery - # pylint-django - # pylint-plugin-utils -pylint-celery==0.3 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-lint -pylint-django==2.7.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-lint -pylint-plugin-utils==0.9.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pylint-celery - # pylint-django -pymongo==4.16.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-opaque-keys - # event-tracking -pynacl==1.6.2 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-django-utils -pyproject-api==1.10.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # tox -pyproject-hooks==1.2.0 - # via - # -r requirements/ci.txt - # -r requirements/pip-tools.txt - # -r requirements/quality.txt - # build - # pip-tools -pytest==9.0.2 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pytest-cov - # pytest-django -pytest-cov==7.0.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -pytest-django==4.12.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -python-dateutil==2.9.0.post0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # celery -python-discovery==1.1.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # virtualenv -python-slugify==8.0.4 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # code-annotations -pytokens==0.4.1 - # via - # -r requirements/ci.txt - # black -pytz==2026.1.post1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # event-tracking - # mongomock -pyyaml==6.0.3 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # code-annotations - # edx-i18n-tools -readme-renderer==44.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # twine -requests==2.32.5 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # id - # meilisearch - # requests-toolbelt - # twine -requests-toolbelt==1.0.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # twine -rfc3986==2.0.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # twine -rich==14.3.3 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # twine -secretstorage==3.5.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # keyring -sentinels==1.1.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # mongomock -six==1.17.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-ccx-keys - # edx-lint - # event-tracking - # python-dateutil -snowballstemmer==3.0.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pydocstyle -soupsieve==2.8.3 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # beautifulsoup4 -sqlparse==0.5.5 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # django -stevedore==5.7.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # code-annotations - # edx-django-utils - # edx-opaque-keys -text-unidecode==1.3 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # python-slugify -tomli-w==1.2.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # tox -tomlkit==0.14.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pylint -tox==4.49.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -twine==6.2.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -types-beautifulsoup4==4.12.0.20250516 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -types-html5lib==1.1.11.20251117 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # types-beautifulsoup4 -types-pyyaml==6.0.12.20250915 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # django-stubs - # djangorestframework-stubs -types-requests==2.31.0.6 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -types-urllib3==1.26.25.14 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # types-requests -types-webencodings==0.5.0.20251108 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # types-html5lib -typesense==2.0.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt -typing-extensions==4.15.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # anyio - # beautifulsoup4 - # django-stubs - # django-stubs-ext - # djangorestframework-stubs - # edx-opaque-keys - # mypy - # pydantic - # pydantic-core - # typesense - # typing-inspection -typing-inspection==0.4.2 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # pydantic -tzdata==2025.3 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # kombu -tzlocal==5.3.1 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # celery -urllib3==1.26.20 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # elasticsearch - # requests - # twine -vine==5.1.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # amqp - # celery - # kombu -virtualenv==21.1.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # tox -wcwidth==0.6.0 - # via - # -r requirements/ci.txt - # -r requirements/quality.txt - # prompt-toolkit -wheel==0.46.3 - # via - # -r requirements/pip-tools.txt - # pip-tools - -# The following packages are considered to be unsafe in a requirements file: -# pip -# setuptools diff --git a/requirements/doc.in b/requirements/doc.in deleted file mode 100644 index c63e2aec..00000000 --- a/requirements/doc.in +++ /dev/null @@ -1,8 +0,0 @@ -# Requirements for documentation validation --c constraints.txt - --r test.txt # Core and testing dependencies for this package - -doc8 # reStructuredText style checker -sphinx-book-theme # Common theme for all Open edX projects -Sphinx # Documentation builder diff --git a/requirements/doc.txt b/requirements/doc.txt deleted file mode 100644 index 022cd94c..00000000 --- a/requirements/doc.txt +++ /dev/null @@ -1,528 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# make upgrade -# -accessible-pygments==0.0.5 - # via pydata-sphinx-theme -alabaster==1.0.0 - # via sphinx -amqp==5.3.1 - # via - # -r requirements/test.txt - # kombu -annotated-types==0.7.0 - # via - # -r requirements/test.txt - # pydantic -anyio==4.12.1 - # via - # -r requirements/test.txt - # httpx -asgiref==3.11.1 - # via - # -r requirements/test.txt - # django -attrs==25.4.0 - # via - # -r requirements/test.txt - # openedx-events -babel==2.18.0 - # via - # pydata-sphinx-theme - # sphinx -beautifulsoup4==4.14.3 - # via - # -r requirements/test.txt - # pydata-sphinx-theme -billiard==4.2.4 - # via - # -r requirements/test.txt - # celery -build==1.4.0 - # via -r requirements/test.txt -cachetools==7.0.3 - # via - # -r requirements/test.txt - # tox -camel-converter[pydantic]==5.1.0 - # via - # -r requirements/test.txt - # meilisearch -celery==5.6.2 - # via - # -r requirements/test.txt - # event-tracking -certifi==2026.2.25 - # via - # -r requirements/test.txt - # elasticsearch - # httpcore - # httpx - # requests -cffi==2.0.0 - # via - # -r requirements/test.txt - # cryptography - # pynacl -charset-normalizer==3.4.5 - # via - # -r requirements/test.txt - # requests -click==8.3.1 - # via - # -r requirements/test.txt - # celery - # click-didyoumean - # click-plugins - # click-repl - # code-annotations - # edx-django-utils -click-didyoumean==0.3.1 - # via - # -r requirements/test.txt - # celery -click-plugins==1.1.1.2 - # via - # -r requirements/test.txt - # celery -click-repl==0.3.0 - # via - # -r requirements/test.txt - # celery -code-annotations==2.3.2 - # via - # -r requirements/test.txt - # edx-toggles -colorama==0.4.6 - # via - # -r requirements/test.txt - # tox -coverage[toml]==7.13.4 - # via - # -r requirements/test.txt - # pytest-cov -cryptography==46.0.5 - # via - # -r requirements/test.txt - # secretstorage -distlib==0.4.0 - # via - # -r requirements/test.txt - # virtualenv -django==5.2.12 - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/test.txt - # django-crum - # django-waffle - # djangorestframework - # edx-django-utils - # edx-search - # edx-toggles - # event-tracking - # openedx-events -django-crum==0.7.9 - # via - # -r requirements/test.txt - # edx-django-utils - # edx-toggles -django-waffle==5.0.0 - # via - # -r requirements/test.txt - # edx-django-utils - # edx-toggles -djangorestframework==3.16.1 - # via -r requirements/test.txt -dnspython==2.8.0 - # via - # -r requirements/test.txt - # pymongo -doc8==0.11.2 - # via -r requirements/doc.in -docutils==0.22.4 - # via - # -r requirements/test.txt - # doc8 - # pydata-sphinx-theme - # readme-renderer - # restructuredtext-lint - # sphinx -edx-ccx-keys==2.0.2 - # via - # -r requirements/test.txt - # openedx-events -edx-django-utils==8.0.1 - # via - # -r requirements/test.txt - # edx-toggles - # event-tracking - # openedx-events -edx-opaque-keys[django]==3.1.0 - # via - # -r requirements/test.txt - # edx-ccx-keys - # openedx-events -edx-search==4.4.0 - # via -r requirements/test.txt -edx-toggles==5.4.1 - # via - # -r requirements/test.txt - # edx-search - # event-tracking -elasticsearch==7.13.4 - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/test.txt - # edx-search -event-tracking==3.3.0 - # via - # -r requirements/test.txt - # edx-search -faker==40.8.0 - # via -r requirements/test.txt -fastavro==1.12.1 - # via - # -r requirements/test.txt - # openedx-events -filelock==3.25.0 - # via - # -r requirements/test.txt - # python-discovery - # tox - # virtualenv -h11==0.16.0 - # via - # -r requirements/test.txt - # httpcore -httpcore==1.0.9 - # via - # -r requirements/test.txt - # httpx -httpx==0.28.1 - # via - # -r requirements/test.txt - # typesense -id==1.5.0 - # via - # -r requirements/test.txt - # twine -idna==3.11 - # via - # -r requirements/test.txt - # anyio - # httpx - # requests -imagesize==2.0.0 - # via sphinx -iniconfig==2.3.0 - # via - # -r requirements/test.txt - # pytest -jaraco-classes==3.4.0 - # via - # -r requirements/test.txt - # keyring -jaraco-context==6.1.0 - # via - # -r requirements/test.txt - # keyring -jaraco-functools==4.4.0 - # via - # -r requirements/test.txt - # keyring -jeepney==0.9.0 - # via - # -r requirements/test.txt - # keyring - # secretstorage -jinja2==3.1.6 - # via - # -r requirements/test.txt - # code-annotations - # sphinx -keyring==25.7.0 - # via - # -r requirements/test.txt - # twine -kombu==5.6.2 - # via - # -r requirements/test.txt - # celery -markdown-it-py==4.0.0 - # via - # -r requirements/test.txt - # rich -markupsafe==3.0.3 - # via - # -r requirements/test.txt - # jinja2 -mdurl==0.1.2 - # via - # -r requirements/test.txt - # markdown-it-py -meilisearch==0.40.0 - # via - # -r requirements/test.txt - # edx-search -mongomock==4.3.0 - # via -r requirements/test.txt -more-itertools==10.8.0 - # via - # -r requirements/test.txt - # jaraco-classes - # jaraco-functools -mysqlclient==2.2.8 - # via -r requirements/test.txt -nh3==0.3.3 - # via - # -r requirements/test.txt - # readme-renderer -openedx-atlas==0.7.0 - # via -r requirements/test.txt -openedx-events==10.5.0 - # via - # -r requirements/test.txt - # event-tracking -packaging==26.0 - # via - # -r requirements/test.txt - # build - # kombu - # mongomock - # pydata-sphinx-theme - # pyproject-api - # pytest - # sphinx - # tox - # twine -platformdirs==4.9.4 - # via - # -r requirements/test.txt - # python-discovery - # tox - # virtualenv -pluggy==1.6.0 - # via - # -r requirements/test.txt - # pytest - # pytest-cov - # tox -prompt-toolkit==3.0.52 - # via - # -r requirements/test.txt - # click-repl -psutil==7.2.2 - # via - # -r requirements/test.txt - # edx-django-utils -pycparser==3.0 - # via - # -r requirements/test.txt - # cffi -pydantic==2.12.5 - # via - # -r requirements/test.txt - # camel-converter -pydantic-core==2.41.5 - # via - # -r requirements/test.txt - # pydantic -pydata-sphinx-theme==0.15.4 - # via sphinx-book-theme -pygments==2.19.2 - # via - # -r requirements/test.txt - # accessible-pygments - # doc8 - # pydata-sphinx-theme - # pytest - # readme-renderer - # rich - # sphinx -pymongo==4.16.0 - # via - # -r requirements/test.txt - # edx-opaque-keys - # event-tracking -pynacl==1.6.2 - # via - # -r requirements/test.txt - # edx-django-utils -pyproject-api==1.10.0 - # via - # -r requirements/test.txt - # tox -pyproject-hooks==1.2.0 - # via - # -r requirements/test.txt - # build -pytest==9.0.2 - # via - # -r requirements/test.txt - # pytest-cov - # pytest-django -pytest-cov==7.0.0 - # via -r requirements/test.txt -pytest-django==4.12.0 - # via -r requirements/test.txt -python-dateutil==2.9.0.post0 - # via - # -r requirements/test.txt - # celery -python-discovery==1.1.0 - # via - # -r requirements/test.txt - # virtualenv -python-slugify==8.0.4 - # via - # -r requirements/test.txt - # code-annotations -pytz==2026.1.post1 - # via - # -r requirements/test.txt - # event-tracking - # mongomock -pyyaml==6.0.3 - # via - # -r requirements/test.txt - # code-annotations -readme-renderer==44.0 - # via - # -r requirements/test.txt - # twine -requests==2.32.5 - # via - # -r requirements/test.txt - # id - # meilisearch - # requests-toolbelt - # sphinx - # twine -requests-toolbelt==1.0.0 - # via - # -r requirements/test.txt - # twine -restructuredtext-lint==2.0.2 - # via doc8 -rfc3986==2.0.0 - # via - # -r requirements/test.txt - # twine -rich==14.3.3 - # via - # -r requirements/test.txt - # twine -roman-numerals==4.1.0 - # via sphinx -secretstorage==3.5.0 - # via - # -r requirements/test.txt - # keyring -sentinels==1.1.1 - # via - # -r requirements/test.txt - # mongomock -six==1.17.0 - # via - # -r requirements/test.txt - # edx-ccx-keys - # event-tracking - # python-dateutil -snowballstemmer==3.0.1 - # via sphinx -soupsieve==2.8.3 - # via - # -r requirements/test.txt - # beautifulsoup4 -sphinx==9.1.0 - # via - # -r requirements/doc.in - # pydata-sphinx-theme - # sphinx-book-theme -sphinx-book-theme==1.1.4 - # 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-jsmath==1.0.1 - # via sphinx -sphinxcontrib-qthelp==2.0.0 - # via sphinx -sphinxcontrib-serializinghtml==2.0.0 - # via sphinx -sqlparse==0.5.5 - # via - # -r requirements/test.txt - # django -stevedore==5.7.0 - # via - # -r requirements/test.txt - # code-annotations - # doc8 - # edx-django-utils - # edx-opaque-keys -text-unidecode==1.3 - # via - # -r requirements/test.txt - # python-slugify -tomli-w==1.2.0 - # via - # -r requirements/test.txt - # tox -tox==4.49.0 - # via -r requirements/test.txt -twine==6.2.0 - # via -r requirements/test.txt -typesense==2.0.0 - # via -r requirements/test.txt -typing-extensions==4.15.0 - # via - # -r requirements/test.txt - # anyio - # beautifulsoup4 - # edx-opaque-keys - # pydantic - # pydantic-core - # pydata-sphinx-theme - # typesense - # typing-inspection -typing-inspection==0.4.2 - # via - # -r requirements/test.txt - # pydantic -tzdata==2025.3 - # via - # -r requirements/test.txt - # kombu -tzlocal==5.3.1 - # via - # -r requirements/test.txt - # celery -urllib3==1.26.20 - # via - # -r requirements/test.txt - # elasticsearch - # requests - # twine -vine==5.1.0 - # via - # -r requirements/test.txt - # amqp - # celery - # kombu -virtualenv==21.1.0 - # via - # -r requirements/test.txt - # tox -wcwidth==0.6.0 - # via - # -r requirements/test.txt - # prompt-toolkit - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/requirements/pip-tools.in b/requirements/pip-tools.in deleted file mode 100644 index 0295d2c8..00000000 --- a/requirements/pip-tools.in +++ /dev/null @@ -1,5 +0,0 @@ -# Just the dependencies to run pip-tools, mainly for the "upgrade" make target - --c constraints.txt - -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 d89516b8..00000000 --- a/requirements/pip-tools.txt +++ /dev/null @@ -1,26 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# make upgrade -# -build==1.4.0 - # via pip-tools -click==8.3.1 - # via pip-tools -packaging==26.0 - # 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.46.3 - # via pip-tools - -# The following packages are considered to be unsafe in a requirements file: -# pip -# setuptools diff --git a/requirements/pip.in b/requirements/pip.in deleted file mode 100644 index 716c6f28..00000000 --- a/requirements/pip.in +++ /dev/null @@ -1,6 +0,0 @@ -# Core dependencies for installing other packages --c constraints.txt - -pip -setuptools -wheel diff --git a/requirements/pip.txt b/requirements/pip.txt deleted file mode 100644 index 084d708e..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.0 - # via wheel -wheel==0.46.3 - # via -r requirements/pip.in - -# The following packages are considered to be unsafe in a requirements file: -pip==26.0.1 - # via -r requirements/pip.in -setuptools==82.0.0 - # 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/quality.in b/requirements/quality.in deleted file mode 100644 index a5574267..00000000 --- a/requirements/quality.in +++ /dev/null @@ -1,14 +0,0 @@ -# Requirements for code quality checks - --c constraints.txt - --r test.txt # Core and testing dependencies for this package - -edx-lint # edX pylint rules and plugins -isort # to standardize order of imports -pycodestyle # PEP 8 compliance validation -pydocstyle # PEP 257 compliance validation -mypy -djangorestframework-stubs -types-beautifulsoup4 -types-requests diff --git a/requirements/quality.txt b/requirements/quality.txt deleted file mode 100644 index 26d63e12..00000000 --- a/requirements/quality.txt +++ /dev/null @@ -1,550 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# make upgrade -# -amqp==5.3.1 - # via - # -r requirements/test.txt - # kombu -annotated-types==0.7.0 - # via - # -r requirements/test.txt - # pydantic -anyio==4.12.1 - # via - # -r requirements/test.txt - # httpx -asgiref==3.11.1 - # via - # -r requirements/test.txt - # django -astroid==4.0.4 - # via - # pylint - # pylint-celery -attrs==25.4.0 - # via - # -r requirements/test.txt - # openedx-events -beautifulsoup4==4.14.3 - # via -r requirements/test.txt -billiard==4.2.4 - # via - # -r requirements/test.txt - # celery -build==1.4.0 - # via -r requirements/test.txt -cachetools==7.0.3 - # via - # -r requirements/test.txt - # tox -camel-converter[pydantic]==5.1.0 - # via - # -r requirements/test.txt - # meilisearch -celery==5.6.2 - # via - # -r requirements/test.txt - # event-tracking -certifi==2026.2.25 - # via - # -r requirements/test.txt - # elasticsearch - # httpcore - # httpx - # requests -cffi==2.0.0 - # via - # -r requirements/test.txt - # cryptography - # pynacl -charset-normalizer==3.4.5 - # via - # -r requirements/test.txt - # requests -click==8.3.1 - # via - # -r requirements/test.txt - # celery - # click-didyoumean - # click-log - # click-plugins - # click-repl - # code-annotations - # edx-django-utils - # edx-lint -click-didyoumean==0.3.1 - # via - # -r requirements/test.txt - # celery -click-log==0.4.0 - # via edx-lint -click-plugins==1.1.1.2 - # via - # -r requirements/test.txt - # celery -click-repl==0.3.0 - # via - # -r requirements/test.txt - # celery -code-annotations==2.3.2 - # via - # -r requirements/test.txt - # edx-lint - # edx-toggles -colorama==0.4.6 - # via - # -r requirements/test.txt - # tox -coverage[toml]==7.13.4 - # via - # -r requirements/test.txt - # pytest-cov -cryptography==46.0.5 - # via - # -r requirements/test.txt - # secretstorage -dill==0.4.1 - # via pylint -distlib==0.4.0 - # via - # -r requirements/test.txt - # virtualenv -django==5.2.12 - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/test.txt - # django-crum - # django-stubs - # django-stubs-ext - # django-waffle - # djangorestframework - # edx-django-utils - # edx-search - # edx-toggles - # event-tracking - # openedx-events -django-crum==0.7.9 - # via - # -r requirements/test.txt - # edx-django-utils - # edx-toggles -django-stubs==5.2.9 - # via djangorestframework-stubs -django-stubs-ext==5.2.9 - # via django-stubs -django-waffle==5.0.0 - # via - # -r requirements/test.txt - # edx-django-utils - # edx-toggles -djangorestframework==3.16.1 - # via -r requirements/test.txt -djangorestframework-stubs==3.16.8 - # via -r requirements/quality.in -dnspython==2.8.0 - # via - # -r requirements/test.txt - # pymongo -docutils==0.22.4 - # via - # -r requirements/test.txt - # readme-renderer -edx-ccx-keys==2.0.2 - # via - # -r requirements/test.txt - # openedx-events -edx-django-utils==8.0.1 - # via - # -r requirements/test.txt - # edx-toggles - # event-tracking - # openedx-events -edx-lint==5.6.0 - # via -r requirements/quality.in -edx-opaque-keys[django]==3.1.0 - # via - # -r requirements/test.txt - # edx-ccx-keys - # openedx-events -edx-search==4.4.0 - # via -r requirements/test.txt -edx-toggles==5.4.1 - # via - # -r requirements/test.txt - # edx-search - # event-tracking -elasticsearch==7.13.4 - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/test.txt - # edx-search -event-tracking==3.3.0 - # via - # -r requirements/test.txt - # edx-search -faker==40.8.0 - # via -r requirements/test.txt -fastavro==1.12.1 - # via - # -r requirements/test.txt - # openedx-events -filelock==3.25.0 - # via - # -r requirements/test.txt - # python-discovery - # tox - # virtualenv -h11==0.16.0 - # via - # -r requirements/test.txt - # httpcore -httpcore==1.0.9 - # via - # -r requirements/test.txt - # httpx -httpx==0.28.1 - # via - # -r requirements/test.txt - # typesense -id==1.5.0 - # via - # -r requirements/test.txt - # twine -idna==3.11 - # via - # -r requirements/test.txt - # anyio - # httpx - # requests -iniconfig==2.3.0 - # via - # -r requirements/test.txt - # pytest -isort==8.0.1 - # via - # -r requirements/quality.in - # pylint -jaraco-classes==3.4.0 - # via - # -r requirements/test.txt - # keyring -jaraco-context==6.1.0 - # via - # -r requirements/test.txt - # keyring -jaraco-functools==4.4.0 - # via - # -r requirements/test.txt - # keyring -jeepney==0.9.0 - # via - # -r requirements/test.txt - # keyring - # secretstorage -jinja2==3.1.6 - # via - # -r requirements/test.txt - # code-annotations -keyring==25.7.0 - # via - # -r requirements/test.txt - # twine -kombu==5.6.2 - # via - # -r requirements/test.txt - # celery -librt==0.8.1 - # via mypy -markdown-it-py==4.0.0 - # via - # -r requirements/test.txt - # rich -markupsafe==3.0.3 - # via - # -r requirements/test.txt - # jinja2 -mccabe==0.7.0 - # via pylint -mdurl==0.1.2 - # via - # -r requirements/test.txt - # markdown-it-py -meilisearch==0.40.0 - # via - # -r requirements/test.txt - # edx-search -mongomock==4.3.0 - # via -r requirements/test.txt -more-itertools==10.8.0 - # via - # -r requirements/test.txt - # jaraco-classes - # jaraco-functools -mypy==1.19.1 - # via -r requirements/quality.in -mypy-extensions==1.1.0 - # via mypy -mysqlclient==2.2.8 - # via -r requirements/test.txt -nh3==0.3.3 - # via - # -r requirements/test.txt - # readme-renderer -openedx-atlas==0.7.0 - # via -r requirements/test.txt -openedx-events==10.5.0 - # via - # -r requirements/test.txt - # event-tracking -packaging==26.0 - # via - # -r requirements/test.txt - # build - # kombu - # mongomock - # pyproject-api - # pytest - # tox - # twine -pathspec==1.0.4 - # via mypy -platformdirs==4.9.4 - # via - # -r requirements/test.txt - # pylint - # python-discovery - # tox - # virtualenv -pluggy==1.6.0 - # via - # -r requirements/test.txt - # pytest - # pytest-cov - # tox -prompt-toolkit==3.0.52 - # via - # -r requirements/test.txt - # click-repl -psutil==7.2.2 - # via - # -r requirements/test.txt - # edx-django-utils -pycodestyle==2.14.0 - # via -r requirements/quality.in -pycparser==3.0 - # via - # -r requirements/test.txt - # cffi -pydantic==2.12.5 - # via - # -r requirements/test.txt - # camel-converter -pydantic-core==2.41.5 - # via - # -r requirements/test.txt - # pydantic -pydocstyle==6.3.0 - # via -r requirements/quality.in -pygments==2.19.2 - # via - # -r requirements/test.txt - # pytest - # readme-renderer - # rich -pylint==4.0.5 - # via - # edx-lint - # pylint-celery - # pylint-django - # pylint-plugin-utils -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 -pymongo==4.16.0 - # via - # -r requirements/test.txt - # edx-opaque-keys - # event-tracking -pynacl==1.6.2 - # via - # -r requirements/test.txt - # edx-django-utils -pyproject-api==1.10.0 - # via - # -r requirements/test.txt - # tox -pyproject-hooks==1.2.0 - # via - # -r requirements/test.txt - # build -pytest==9.0.2 - # via - # -r requirements/test.txt - # pytest-cov - # pytest-django -pytest-cov==7.0.0 - # via -r requirements/test.txt -pytest-django==4.12.0 - # via -r requirements/test.txt -python-dateutil==2.9.0.post0 - # via - # -r requirements/test.txt - # celery -python-discovery==1.1.0 - # via - # -r requirements/test.txt - # virtualenv -python-slugify==8.0.4 - # via - # -r requirements/test.txt - # code-annotations -pytz==2026.1.post1 - # via - # -r requirements/test.txt - # event-tracking - # mongomock -pyyaml==6.0.3 - # via - # -r requirements/test.txt - # code-annotations -readme-renderer==44.0 - # via - # -r requirements/test.txt - # twine -requests==2.32.5 - # via - # -r requirements/test.txt - # id - # meilisearch - # requests-toolbelt - # twine -requests-toolbelt==1.0.0 - # via - # -r requirements/test.txt - # twine -rfc3986==2.0.0 - # via - # -r requirements/test.txt - # twine -rich==14.3.3 - # via - # -r requirements/test.txt - # twine -secretstorage==3.5.0 - # via - # -r requirements/test.txt - # keyring -sentinels==1.1.1 - # via - # -r requirements/test.txt - # mongomock -six==1.17.0 - # via - # -r requirements/test.txt - # edx-ccx-keys - # edx-lint - # event-tracking - # python-dateutil -snowballstemmer==3.0.1 - # via pydocstyle -soupsieve==2.8.3 - # via - # -r requirements/test.txt - # beautifulsoup4 -sqlparse==0.5.5 - # via - # -r requirements/test.txt - # django -stevedore==5.7.0 - # via - # -r requirements/test.txt - # code-annotations - # edx-django-utils - # edx-opaque-keys -text-unidecode==1.3 - # via - # -r requirements/test.txt - # python-slugify -tomli-w==1.2.0 - # via - # -r requirements/test.txt - # tox -tomlkit==0.14.0 - # via pylint -tox==4.49.0 - # via -r requirements/test.txt -twine==6.2.0 - # via -r requirements/test.txt -types-beautifulsoup4==4.12.0.20250516 - # via -r requirements/quality.in -types-html5lib==1.1.11.20251117 - # via types-beautifulsoup4 -types-pyyaml==6.0.12.20250915 - # via - # django-stubs - # djangorestframework-stubs -types-requests==2.31.0.6 - # via -r requirements/quality.in -types-urllib3==1.26.25.14 - # via types-requests -types-webencodings==0.5.0.20251108 - # via types-html5lib -typesense==2.0.0 - # via -r requirements/test.txt -typing-extensions==4.15.0 - # via - # -r requirements/test.txt - # anyio - # beautifulsoup4 - # django-stubs - # django-stubs-ext - # djangorestframework-stubs - # edx-opaque-keys - # mypy - # pydantic - # pydantic-core - # typesense - # typing-inspection -typing-inspection==0.4.2 - # via - # -r requirements/test.txt - # pydantic -tzdata==2025.3 - # via - # -r requirements/test.txt - # kombu -tzlocal==5.3.1 - # via - # -r requirements/test.txt - # celery -urllib3==1.26.20 - # via - # -r requirements/test.txt - # elasticsearch - # requests - # twine -vine==5.1.0 - # via - # -r requirements/test.txt - # amqp - # celery - # kombu -virtualenv==21.1.0 - # via - # -r requirements/test.txt - # tox -wcwidth==0.6.0 - # via - # -r requirements/test.txt - # prompt-toolkit - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/requirements/test.in b/requirements/test.in deleted file mode 100644 index 6113b225..00000000 --- a/requirements/test.in +++ /dev/null @@ -1,13 +0,0 @@ -# Requirements for test runs. --c constraints.txt - --r base.txt # Core dependencies for this package - -pytest-cov # pytest extension for code coverage statistics -pytest-django # pytest extension for better Django support -code-annotations # provides commands used by the pii_check tox target. -tox -mongomock -Faker -twine # Validates README.rst for usage on PyPI -build # Needed to build the wheel for twine check diff --git a/requirements/test.txt b/requirements/test.txt deleted file mode 100644 index 35fd2d6f..00000000 --- a/requirements/test.txt +++ /dev/null @@ -1,414 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# make upgrade -# -amqp==5.3.1 - # via - # -r requirements/base.txt - # kombu -annotated-types==0.7.0 - # via - # -r requirements/base.txt - # pydantic -anyio==4.12.1 - # via - # -r requirements/base.txt - # httpx -asgiref==3.11.1 - # via - # -r requirements/base.txt - # django -attrs==25.4.0 - # via - # -r requirements/base.txt - # openedx-events -beautifulsoup4==4.14.3 - # via -r requirements/base.txt -billiard==4.2.4 - # via - # -r requirements/base.txt - # celery -build==1.4.0 - # via -r requirements/test.in -cachetools==7.0.3 - # via tox -camel-converter[pydantic]==5.1.0 - # via - # -r requirements/base.txt - # meilisearch -celery==5.6.2 - # via - # -r requirements/base.txt - # event-tracking -certifi==2026.2.25 - # via - # -r requirements/base.txt - # elasticsearch - # httpcore - # httpx - # requests -cffi==2.0.0 - # via - # -r requirements/base.txt - # cryptography - # pynacl -charset-normalizer==3.4.5 - # via - # -r requirements/base.txt - # requests -click==8.3.1 - # via - # -r requirements/base.txt - # celery - # click-didyoumean - # click-plugins - # click-repl - # code-annotations - # edx-django-utils -click-didyoumean==0.3.1 - # via - # -r requirements/base.txt - # celery -click-plugins==1.1.1.2 - # via - # -r requirements/base.txt - # celery -click-repl==0.3.0 - # via - # -r requirements/base.txt - # celery -code-annotations==2.3.2 - # via - # -r requirements/base.txt - # -r requirements/test.in - # edx-toggles -colorama==0.4.6 - # via tox -coverage[toml]==7.13.4 - # via pytest-cov -cryptography==46.0.5 - # via secretstorage -distlib==0.4.0 - # via virtualenv - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/base.txt - # django-crum - # django-waffle - # djangorestframework - # edx-django-utils - # edx-search - # edx-toggles - # event-tracking - # openedx-events -django-crum==0.7.9 - # via - # -r requirements/base.txt - # edx-django-utils - # edx-toggles -django-waffle==5.0.0 - # via - # -r requirements/base.txt - # edx-django-utils - # edx-toggles -djangorestframework==3.16.1 - # via -r requirements/base.txt -dnspython==2.8.0 - # via - # -r requirements/base.txt - # pymongo -docutils==0.22.4 - # via readme-renderer -edx-ccx-keys==2.0.2 - # via - # -r requirements/base.txt - # openedx-events -edx-django-utils==8.0.1 - # via - # -r requirements/base.txt - # edx-toggles - # event-tracking - # openedx-events -edx-opaque-keys[django]==3.1.0 - # via - # -r requirements/base.txt - # edx-ccx-keys - # openedx-events -edx-search==4.4.0 - # via -r requirements/base.txt -edx-toggles==5.4.1 - # via - # -r requirements/base.txt - # edx-search - # event-tracking -elasticsearch==7.13.4 - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/base.txt - # edx-search -event-tracking==3.3.0 - # via - # -r requirements/base.txt - # edx-search -faker==40.8.0 - # via -r requirements/test.in -fastavro==1.12.1 - # via - # -r requirements/base.txt - # openedx-events -filelock==3.25.0 - # via - # python-discovery - # tox - # virtualenv -h11==0.16.0 - # via - # -r requirements/base.txt - # httpcore -httpcore==1.0.9 - # via - # -r requirements/base.txt - # httpx -httpx==0.28.1 - # via - # -r requirements/base.txt - # typesense -id==1.5.0 - # via twine -idna==3.11 - # via - # -r requirements/base.txt - # anyio - # httpx - # requests -iniconfig==2.3.0 - # via pytest -jaraco-classes==3.4.0 - # via keyring -jaraco-context==6.1.0 - # via keyring -jaraco-functools==4.4.0 - # via keyring -jeepney==0.9.0 - # via - # keyring - # secretstorage -jinja2==3.1.6 - # via - # -r requirements/base.txt - # code-annotations -keyring==25.7.0 - # via twine -kombu==5.6.2 - # via - # -r requirements/base.txt - # celery -markdown-it-py==4.0.0 - # via rich -markupsafe==3.0.3 - # via - # -r requirements/base.txt - # jinja2 -mdurl==0.1.2 - # via markdown-it-py -meilisearch==0.40.0 - # via - # -r requirements/base.txt - # edx-search -mongomock==4.3.0 - # via -r requirements/test.in -more-itertools==10.8.0 - # via - # jaraco-classes - # jaraco-functools -mysqlclient==2.2.8 - # via -r requirements/base.txt -nh3==0.3.3 - # via readme-renderer -openedx-atlas==0.7.0 - # via -r requirements/base.txt -openedx-events==10.5.0 - # via - # -r requirements/base.txt - # event-tracking -packaging==26.0 - # via - # -r requirements/base.txt - # build - # kombu - # mongomock - # pyproject-api - # pytest - # tox - # twine -platformdirs==4.9.4 - # via - # python-discovery - # tox - # virtualenv -pluggy==1.6.0 - # via - # pytest - # pytest-cov - # tox -prompt-toolkit==3.0.52 - # via - # -r requirements/base.txt - # click-repl -psutil==7.2.2 - # via - # -r requirements/base.txt - # edx-django-utils -pycparser==3.0 - # via - # -r requirements/base.txt - # cffi -pydantic==2.12.5 - # via - # -r requirements/base.txt - # camel-converter -pydantic-core==2.41.5 - # via - # -r requirements/base.txt - # pydantic -pygments==2.19.2 - # via - # pytest - # readme-renderer - # rich -pymongo==4.16.0 - # via - # -r requirements/base.txt - # edx-opaque-keys - # event-tracking -pynacl==1.6.2 - # via - # -r requirements/base.txt - # edx-django-utils -pyproject-api==1.10.0 - # via tox -pyproject-hooks==1.2.0 - # via build -pytest==9.0.2 - # via - # pytest-cov - # pytest-django -pytest-cov==7.0.0 - # via -r requirements/test.in -pytest-django==4.12.0 - # via -r requirements/test.in -python-dateutil==2.9.0.post0 - # via - # -r requirements/base.txt - # celery -python-discovery==1.1.0 - # via virtualenv -python-slugify==8.0.4 - # via - # -r requirements/base.txt - # code-annotations -pytz==2026.1.post1 - # via - # -r requirements/base.txt - # event-tracking - # mongomock -pyyaml==6.0.3 - # via - # -r requirements/base.txt - # code-annotations -readme-renderer==44.0 - # via twine -requests==2.32.5 - # via - # -r requirements/base.txt - # id - # meilisearch - # requests-toolbelt - # twine -requests-toolbelt==1.0.0 - # via twine -rfc3986==2.0.0 - # via twine -rich==14.3.3 - # via twine -secretstorage==3.5.0 - # via keyring -sentinels==1.1.1 - # via mongomock -six==1.17.0 - # via - # -r requirements/base.txt - # edx-ccx-keys - # event-tracking - # python-dateutil -soupsieve==2.8.3 - # via - # -r requirements/base.txt - # beautifulsoup4 -sqlparse==0.5.5 - # via - # -r requirements/base.txt - # django -stevedore==5.7.0 - # via - # -r requirements/base.txt - # code-annotations - # edx-django-utils - # edx-opaque-keys -text-unidecode==1.3 - # via - # -r requirements/base.txt - # python-slugify -tomli-w==1.2.0 - # via tox -tox==4.49.0 - # via -r requirements/test.in -twine==6.2.0 - # via -r requirements/test.in -typesense==2.0.0 - # via -r requirements/base.txt -typing-extensions==4.15.0 - # via - # -r requirements/base.txt - # anyio - # beautifulsoup4 - # edx-opaque-keys - # pydantic - # pydantic-core - # typesense - # typing-inspection -typing-inspection==0.4.2 - # via - # -r requirements/base.txt - # pydantic -tzdata==2025.3 - # via - # -r requirements/base.txt - # kombu -tzlocal==5.3.1 - # via - # -r requirements/base.txt - # celery -urllib3==1.26.20 - # via - # -r requirements/base.txt - # elasticsearch - # requests - # twine -vine==5.1.0 - # via - # -r requirements/base.txt - # amqp - # celery - # kombu -virtualenv==21.1.0 - # via tox -wcwidth==0.6.0 - # via - # -r requirements/base.txt - # prompt-toolkit - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d7825996..00000000 --- a/setup.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[isort] -include_trailing_comma = True -indent = ' ' -line_length = 120 -multi_line_output = 3 -skip= - migrations - -[wheel] -universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100755 index db4f5f03..00000000 --- a/setup.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env python -""" -Package metadata for forum. -""" - -import os -import re -import sys - -from setuptools import find_packages, setup # pylint: disable=E0401 - - -def get_version(*file_paths): - """ - Extract the version string from the file. - - Input: - - file_paths: relative path fragments to file with - version string - """ - filename = os.path.join(os.path.dirname(__file__), *file_paths) - version_file = open(filename, encoding="utf8").read() - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) - if version_match: - return version_match.group(1) - raise RuntimeError("Unable to find version string.") - - -def load_requirements(*requirements_paths): - """ - Load all requirements from the specified requirements files. - - Requirements will include any constraints from files specified - with -c in the requirements files. - Returns a list of requirement strings. - """ - # e.g. {"django": "Django", "confluent-kafka": "confluent_kafka[avro]"} - by_canonical_name = {} - - def check_name_consistent(package): - """ - Raise exception if package is named different ways. - - This ensures that packages are named consistently so we can match - constraints to packages. It also ensures that if we require a package - with extras we don't constrain it without mentioning the extras (since - that too would interfere with matching constraints.) - """ - canonical = package.lower().replace("_", "-").split("[")[0] - seen_spelling = by_canonical_name.get(canonical) - if seen_spelling is None: - by_canonical_name[canonical] = package - elif seen_spelling != package: - raise Exception( - f'Encountered both "{seen_spelling}" and "{package}" in requirements ' - "and constraints files; please use just one or the other." - ) - - requirements = {} - constraint_files = set() - - # groups "pkg<=x.y.z,..." into ("pkg", "<=x.y.z,...") - re_package_name_base_chars = r"a-zA-Z0-9\-_." # chars allowed in base package name - # Two groups: name[maybe,extras], and optionally a constraint - requirement_line_regex = re.compile( - r"([%s]+(?:\[[%s,\s]+\])?)([<>=][^#\s]+)?" - % (re_package_name_base_chars, re_package_name_base_chars) - ) - - def add_version_constraint_or_raise( - current_line, current_requirements, add_if_not_present - ): - regex_match = requirement_line_regex.match(current_line) - if regex_match: - package = regex_match.group(1) - version_constraints = regex_match.group(2) - check_name_consistent(package) - existing_version_constraints = current_requirements.get(package, None) - # It's fine to add constraints to an unconstrained package, - # but raise an error if there are already constraints in place. - if ( - existing_version_constraints - and existing_version_constraints != version_constraints - ): - raise BaseException( - f"Multiple constraint definitions found for {package}:" - f' "{existing_version_constraints}" and "{version_constraints}".' - f"Combine constraints into one location with {package}" - f"{existing_version_constraints},{version_constraints}." - ) - if add_if_not_present or package in current_requirements: - current_requirements[package] = version_constraints - - # Read requirements from .in files and store the path to any - # constraint files that are pulled in. - for path in requirements_paths: - with open(path) as reqs: - for line in reqs: - if is_requirement(line): - add_version_constraint_or_raise(line, requirements, True) - if line and line.startswith("-c") and not line.startswith("-c http"): - constraint_files.add( - os.path.dirname(path) - + "/" - + line.split("#")[0].replace("-c", "").strip() - ) - - # process constraint files: add constraints to existing requirements - for constraint_file in constraint_files: - with open(constraint_file) as reader: - for line in reader: - if is_requirement(line): - add_version_constraint_or_raise(line, requirements, False) - - # process back into list of pkg><=constraints strings - constrained_requirements = [ - f'{pkg}{version or ""}' for (pkg, version) in sorted(requirements.items()) - ] - return constrained_requirements - - -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")) - ) - - -VERSION = get_version("forum", "__init__.py") - -if sys.argv[-1] == "tag": - print("Tagging the version on github:") - os.system("git tag -a %s -m 'version %s'" % (VERSION, VERSION)) - os.system("git push --tags") - sys.exit() - -README = open( - os.path.join(os.path.dirname(__file__), "README.rst"), encoding="utf8" -).read() -CHANGELOG = open( - os.path.join(os.path.dirname(__file__), "CHANGELOG.rst"), encoding="utf8" -).read() - -setup( - name="openedx-forum", - version=VERSION, - description="""Open edX forum application""", - long_description=README + "\n\n" + CHANGELOG, - long_description_content_type="text/x-rst", - author="Open edX Project", - author_email="oscm@openedx.org", - url="https://github.com/openedx/forum", - packages=find_packages( - include=["forum", "forum.*"], - exclude=["*tests"], - ), - include_package_data=True, - install_requires=load_requirements("requirements/base.in"), - python_requires=">=3.12", - license="AGPL 3.0", - zip_safe=False, - keywords="Python edx", - classifiers=[ - "Development Status :: 3 - Alpha", - "Framework :: Django", - "Framework :: Django :: 4.2", - "Framework :: Django :: 5.2", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", - "Natural Language :: English", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", - ], - entry_points={ - "lms.djangoapp": [ - "forum = forum.apps:ForumConfig", - ], - }, -) diff --git a/test_utils/client.py b/test_utils/client.py index d7ee58d1..662f4efa 100644 --- a/test_utils/client.py +++ b/test_utils/client.py @@ -15,9 +15,7 @@ class APIClient(Client): This client sends JSON data with the correct headers. """ - def send_request( - self, method: str, path: str, data: Any, *args: Any, **kwargs: Any - ) -> Any: + def send_request(self, method: str, path: str, data: Any, *args: Any, **kwargs: Any) -> Any: """ Send a request with the specified HTTP method and JSON data. diff --git a/test_utils/mock_es_backend.py b/test_utils/mock_es_backend.py index 781d50f3..0f46c373 100644 --- a/test_utils/mock_es_backend.py +++ b/test_utils/mock_es_backend.py @@ -3,7 +3,8 @@ """ from typing import Any -from forum.search.es import ElasticsearchIndexBackend, ElasticsearchDocumentBackend + +from forum.search.es import ElasticsearchDocumentBackend, ElasticsearchIndexBackend class MockElasticsearchIndexBackend(ElasticsearchIndexBackend): @@ -14,9 +15,7 @@ class MockElasticsearchIndexBackend(ElasticsearchIndexBackend): are overridden to provide mocked behavior without performing actual operations. """ - def rebuild_indices( - self, batch_size: int = 500, extra_catchup_minutes: int = 5 - ) -> None: + def rebuild_indices(self, batch_size: int = 500, extra_catchup_minutes: int = 5) -> None: """Mock method for rebuilding Elasticsearch indices.""" def create_indices(self) -> list[str]: @@ -30,9 +29,7 @@ def delete_unused_indices(self) -> int: """Mock method for deleting unused Elasticsearch indices.""" return 0 - def move_alias( - self, alias_name: str, index_name: str, force_delete: bool = False - ) -> None: + def move_alias(self, alias_name: str, index_name: str, force_delete: bool = False) -> None: """Mock method for moving Elasticsearch aliases.""" def refresh_indices(self) -> None: @@ -50,15 +47,11 @@ class MockElasticsearchDocumentBackend(ElasticsearchDocumentBackend): Mocked class for ElasticsearchDocumentBackend to return dummy values. """ - def update_document( - self, index_name: str, doc_id: str | int, update_data: dict[str, Any] - ) -> None: + def update_document(self, index_name: str, doc_id: str | int, update_data: dict[str, Any]) -> None: """Mock method for updating a document in Elasticsearch.""" def delete_document(self, index_name: str, doc_id: str | int) -> None: """Mock method for deleting a document from Elasticsearch.""" - def index_document( - self, index_name: str, doc_id: str | int, document: dict[str, Any] - ) -> None: + def index_document(self, index_name: str, doc_id: str | int, document: dict[str, Any]) -> None: """Mock method for indexing a document in Elasticsearch.""" diff --git a/tests/conftest.py b/tests/conftest.py index 3b3c2c6b..7a8b77b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,8 @@ Init file for tests. """ -from typing import Any, Generator +from collections.abc import Generator +from typing import Any from unittest.mock import patch import mongomock @@ -13,8 +14,8 @@ from forum.backends.mysql.api import MySQLBackend from test_utils.client import APIClient from test_utils.mock_es_backend import ( - MockElasticsearchIndexBackend, MockElasticsearchDocumentBackend, + MockElasticsearchIndexBackend, ) diff --git a/tests/e2e/test_search_es.py b/tests/e2e/test_search_es.py index f1265663..630c9d25 100644 --- a/tests/e2e/test_search_es.py +++ b/tests/e2e/test_search_es.py @@ -4,7 +4,7 @@ import logging import time -from typing import Any, Optional +from typing import Any from urllib.parse import urlencode import pytest @@ -62,9 +62,7 @@ def refresh_elastic_search_indices() -> None: get_index_search_backend().refresh_indices() -def test_invalid_request( - api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] -) -> None: +def test_invalid_request(api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str]) -> None: """ Test that invalid requests to the search API return a 400 status. @@ -327,9 +325,7 @@ def create_threads_and_comments_for_filter_tests( return threads_ids, threads_comments -def assert_response_contains( - response: Response, expected_indexes: list[int], threads_ids: list[str] -) -> None: +def assert_response_contains(response: Response, expected_indexes: list[int], threads_ids: list[str]) -> None: """Assert that the response contains the expected thread IDs.""" assert response.status_code == 200 threads = response.json()["collection"] @@ -347,21 +343,15 @@ def test_filter_threads_by_course_id( course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" user_id, username = user_data - threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1, user_id, username, backend - ) + threads_ids, _ = create_threads_and_comments_for_filter_tests(course_id_0, course_id_1, user_id, username, backend) refresh_elastic_search_indices() params = {"text": "text", "course_id": course_id_0} response = perform_search_query(api_client, params) - assert_response_contains( - response, [i for i in range(30) if i % 2 == 0], threads_ids - ) + assert_response_contains(response, [i for i in range(30) if i % 2 == 0], threads_ids) -def test_filter_threads_by_context( - api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] -) -> None: +def test_filter_threads_by_context(api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str]) -> None: """Test filtering threads by context.""" backend = patched_get_backend() @@ -369,9 +359,7 @@ def test_filter_threads_by_context( course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" user_id, username = user_data - threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1, user_id, username, backend - ) + threads_ids, _ = create_threads_and_comments_for_filter_tests(course_id_0, course_id_1, user_id, username, backend) refresh_elastic_search_indices() params = {"text": "text", "context": "standalone"} @@ -379,9 +367,7 @@ def test_filter_threads_by_context( assert_response_contains(response, list(range(30, 35)), threads_ids) -def test_filter_threads_by_unread( - api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] -) -> None: +def test_filter_threads_by_unread(api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str]) -> None: """Test filtering threads by unread status.""" backend = patched_get_backend() course_id_0 = "course-v1:Arbisoft+SE002+2024_S2" @@ -389,9 +375,7 @@ def test_filter_threads_by_unread( user_id = "1" user_id, username = user_data - threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1, user_id, username, backend - ) + threads_ids, _ = create_threads_and_comments_for_filter_tests(course_id_0, course_id_1, user_id, username, backend) refresh_elastic_search_indices() backend.mark_as_read(user_id, threads_ids[0]) @@ -402,23 +386,17 @@ def test_filter_threads_by_unread( "unread": "True", } response = perform_search_query(api_client, params) - assert_response_contains( - response, [i for i in range(1, 30) if i % 2 == 0], threads_ids - ) + assert_response_contains(response, [i for i in range(1, 30) if i % 2 == 0], threads_ids) -def test_filter_threads_by_flagged( - api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] -) -> None: +def test_filter_threads_by_flagged(api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str]) -> None: """Test filtering threads by flagged status.""" backend = patched_get_backend() course_id_0 = "course-v1:Arbisoft+SE002+2024_S2" course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" user_id, username = user_data - threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1, user_id, username, backend - ) + threads_ids, _ = create_threads_and_comments_for_filter_tests(course_id_0, course_id_1, user_id, username, backend) refresh_elastic_search_indices() params = {"text": "text", "course_id": course_id_0, "flagged": "True"} @@ -481,22 +459,16 @@ def test_filter_threads_by_commentable_id( course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" user_id, username = user_data - threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1, user_id, username, backend - ) + threads_ids, _ = create_threads_and_comments_for_filter_tests(course_id_0, course_id_1, user_id, username, backend) refresh_elastic_search_indices() params = {"text": "text", "commentable_id": "commentable0"} response = perform_search_query(api_client, params) - assert_response_contains( - response, [i for i in range(30) if i % 3 == 0], threads_ids - ) + assert_response_contains(response, [i for i in range(30) if i % 3 == 0], threads_ids) params = {"text": "text", "commentable_ids": "commentable0,commentable1"} response = perform_search_query(api_client, params) - assert_response_contains( - response, [i for i in range(30) if i % 3 in [0, 1]], threads_ids - ) + assert_response_contains(response, [i for i in range(30) if i % 3 in [0, 1]], threads_ids) def test_filter_threads_by_group_id( @@ -508,36 +480,26 @@ def test_filter_threads_by_group_id( course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" user_id, username = user_data - threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1, user_id, username, backend - ) + threads_ids, _ = create_threads_and_comments_for_filter_tests(course_id_0, course_id_1, user_id, username, backend) refresh_elastic_search_indices() params = {"text": "text", "group_id": "1"} response = perform_search_query(api_client, params) - assert_response_contains( - response, [i for i in range(30) if i % 5 in [0, 1]], threads_ids - ) + assert_response_contains(response, [i for i in range(30) if i % 5 in [0, 1]], threads_ids) params = {"text": "text", "group_ids": "1,2"} response = perform_search_query(api_client, params) - assert_response_contains( - response, [i for i in range(30) if i % 5 in [0, 1, 2]], threads_ids - ) + assert_response_contains(response, [i for i in range(30) if i % 5 in [0, 1, 2]], threads_ids) -def test_filter_threads_combined( - api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] -) -> None: +def test_filter_threads_combined(api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str]) -> None: """Test filtering threads with multiple filters combined.""" backend = patched_get_backend() course_id_0 = "course-v1:Arbisoft+SE002+2024_S2" course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" user_id, username = user_data - threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1, user_id, username, backend - ) + threads_ids, _ = create_threads_and_comments_for_filter_tests(course_id_0, course_id_1, user_id, username, backend) refresh_elastic_search_indices() params = { @@ -550,9 +512,7 @@ def test_filter_threads_combined( assert_response_contains(response, [0, 6], threads_ids) -def test_pagination( - api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] -) -> None: +def test_pagination(api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str]) -> None: """ Test pagination of search results. Ensures that results are correctly paginated and that the order of threads is as expected across different pages. @@ -578,7 +538,7 @@ def test_pagination( refresh_elastic_search_indices() - def check_pagination(per_page: Optional[int], num_pages: int) -> None: + def check_pagination(per_page: int | None, num_pages: int) -> None: result_ids = [] params = {"text": "text"} if per_page: @@ -599,9 +559,7 @@ def check_pagination(per_page: Optional[int], num_pages: int) -> None: check_pagination(None, 3) -def test_sorting( - api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] -) -> None: +def test_sorting(api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str]) -> None: """ Test the sorting functionality for threads based on various criteria, such as date, activity, votes, and comments. Asserts that the threads are sorted correctly according to the specified sorting key. @@ -646,7 +604,7 @@ def test_sorting( refresh_elastic_search_indices() - def fetch_and_check(sort_key: Optional[str], expected_indexes: list[int]) -> None: + def fetch_and_check(sort_key: str | None, expected_indexes: list[int]) -> None: params = {"text": "text"} if sort_key: params["sort_key"] = str(sort_key) @@ -657,9 +615,7 @@ def fetch_and_check(sort_key: Optional[str], expected_indexes: list[int]) -> Non threads = result["collection"] expected_ids = [threads_ids[i] for i in expected_indexes] actual_ids = [thread["id"] for thread in threads] - assert ( - actual_ids == expected_ids - ), f"Expected {expected_ids}, but got {actual_ids}" + assert actual_ids == expected_ids, f"Expected {expected_ids}, but got {actual_ids}" # Test various sorting scenarios fetch_and_check("date", [5, 4, 3, 2, 1, 0]) @@ -669,9 +625,7 @@ def fetch_and_check(sort_key: Optional[str], expected_indexes: list[int]) -> Non fetch_and_check(None, [5, 4, 3, 2, 1, 0]) # Default sorting by date -def test_spelling_correction( - api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] -) -> None: +def test_spelling_correction(api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str]) -> None: """ Test the spelling correction feature in search. Verifies that misspelled words in both thread titles and comment bodies are correct @@ -702,17 +656,15 @@ def test_spelling_correction( ) refresh_elastic_search_indices() - def check_correction(original_text: str, corrected_text: Optional[str]) -> None: + def check_correction(original_text: str, corrected_text: str | None) -> None: params = {"text": original_text} response = perform_search_query(api_client, params) assert response.status_code == 200 result = response.json() - assert ( - result.get("corrected_text") == corrected_text - ), f"Expected '{corrected_text}', but got '{result.get('corrected_text')}'" - assert result[ - "collection" - ], f"Expected non-empty collection for '{original_text}', but got empty." + assert result.get("corrected_text") == corrected_text, ( + f"Expected '{corrected_text}', but got '{result.get('corrected_text')}'" + ) + assert result["collection"], f"Expected non-empty collection for '{original_text}', but got empty." # Test: can correct a word appearing only in a comment check_correction("pinapples", "pineapples") @@ -770,9 +722,7 @@ def test_spelling_correction_with_mush_clause( assert response.status_code == 200 result = response.json() corrected_text = result.get("corrected_text") - assert ( - corrected_text is None - ), f"Expected 'corrected_text' to be None, but got a value '{corrected_text}'." + assert corrected_text is None, f"Expected 'corrected_text' to be None, but got a value '{corrected_text}'." assert not result["collection"], "Expected an empty collection, but got results." @@ -817,19 +767,17 @@ def test_total_results_and_num_pages( # Refresh Elasticsearch indices to ensure all comments are searchable refresh_elastic_search_indices() - def test_text( - text: str, expected_total_results: int, expected_num_pages: int - ) -> None: + def test_text(text: str, expected_total_results: int, expected_num_pages: int) -> None: params = {"course_id": course_id, "text": text, "per_page": "10"} response = perform_search_query(api_client, params) assert response.status_code == 200 result = response.json() - assert ( - result["total_results"] == expected_total_results - ), f"Expected total_results {expected_total_results}, but got {result['total_results']}" - assert ( - result["num_pages"] == expected_num_pages - ), f"Expected num_pages {expected_num_pages}, but got {result['num_pages']}" + assert result["total_results"] == expected_total_results, ( + f"Expected total_results {expected_total_results}, but got {result['total_results']}" + ) + assert result["num_pages"] == expected_num_pages, ( + f"Expected num_pages {expected_num_pages}, but got {result['num_pages']}" + ) # Running the tests test_text("all", 100, 10) @@ -839,9 +787,7 @@ def test_text( test_text("one", 1, 1) -def test_unicode_data( - api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] -) -> None: +def test_unicode_data(api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str]) -> None: """ Test the handling of Unicode characters in search queries. Verifies that threads containing Unicode characters are searchable and return correct results when queried with ASCII search terms. diff --git a/tests/e2e/test_search_meilisearch.py b/tests/e2e/test_search_meilisearch.py index a1cfc4a3..9436272d 100644 --- a/tests/e2e/test_search_meilisearch.py +++ b/tests/e2e/test_search_meilisearch.py @@ -4,8 +4,8 @@ import typing as t -from django.test import override_settings import pytest +from django.test import override_settings import forum.search.meilisearch @@ -15,9 +15,7 @@ @pytest.fixture(autouse=True) def configure_meilisearch_search_backend() -> t.Generator[t.Any, t.Any, t.Any]: """Configure Django to use Meilisearch as a search backend.""" - with override_settings( - FORUM_SEARCH_BACKEND="forum.search.meilisearch.MeilisearchBackend" - ): + with override_settings(FORUM_SEARCH_BACKEND="forum.search.meilisearch.MeilisearchBackend"): yield @@ -35,18 +33,14 @@ def meilisearch_cleanup() -> None: def test_initialize_indexes() -> None: index_backend = forum.search.meilisearch.MeilisearchIndexBackend() index_backend.initialize_indices() - indexes = sorted( - [r.uid for r in index_backend.meilisearch_client.get_indexes()["results"]] - ) + indexes = sorted([r.uid for r in index_backend.meilisearch_client.get_indexes()["results"]]) assert [ "comment_threads", "comments", ] == indexes -def test_insert_document( - patched_get_backend: t.Any, user_data: tuple[str, str] -) -> None: +def test_insert_document(patched_get_backend: t.Any, user_data: tuple[str, str]) -> None: index_backend = forum.search.meilisearch.MeilisearchIndexBackend() index_backend.initialize_indices() diff --git a/tests/e2e/test_users.py b/tests/e2e/test_users.py index de1119e2..9b9943e6 100644 --- a/tests/e2e/test_users.py +++ b/tests/e2e/test_users.py @@ -4,7 +4,7 @@ import random import time -from typing import Any, Optional +from typing import Any import pytest from faker import Faker @@ -44,7 +44,7 @@ def setup_10_threads(author_id: str, author_username: str, backend: Any) -> list def add_flags( content_type: str, - content_data: Optional[dict[str, Any]], + content_data: dict[str, Any] | None, expected_data: dict[str, Any], backend: Any, ) -> None: @@ -68,12 +68,8 @@ def add_flags( historical_abuse_flaggers=historical_abuse_flaggers, ) - expected_data[content_data["author_id"]]["active_flags"] += ( - 1 if abuse_flaggers else 0 - ) - expected_data[content_data["author_id"]]["inactive_flags"] += ( - 1 if historical_abuse_flaggers else 0 - ) + expected_data[content_data["author_id"]]["active_flags"] += 1 if abuse_flaggers else 0 + expected_data[content_data["author_id"]]["inactive_flags"] += 1 if historical_abuse_flaggers else 0 def build_structure_and_response( @@ -104,9 +100,7 @@ def build_structure_and_response( thread_author = random.choice(authors) expected_data[str(thread_author["external_id"])]["threads"] += 1 if with_timestamps: - expected_data[str(thread_author["external_id"])]["last_activity_at"] = ( - time.strftime("%Y-%m-%dT%H:%M:%SZ") - ) + expected_data[str(thread_author["external_id"])]["last_activity_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ") thread_id = backend.create_thread( { "title": fake.word(), @@ -124,9 +118,9 @@ def build_structure_and_response( comment_author = random.choice(authors) expected_data[str(comment_author["external_id"])]["responses"] += 1 if with_timestamps: - expected_data[str(comment_author["external_id"])][ - "last_activity_at" - ] = time.strftime("%Y-%m-%dT%H:%M:%SZ") + expected_data[str(comment_author["external_id"])]["last_activity_at"] = time.strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) comment_id = backend.create_comment( { "body": fake.sentence(), @@ -143,9 +137,9 @@ def build_structure_and_response( reply_author = random.choice(authors) expected_data[str(reply_author["external_id"])]["replies"] += 1 if with_timestamps: - expected_data[str(reply_author["external_id"])][ - "last_activity_at" - ] = time.strftime("%Y-%m-%dT%H:%M:%SZ") + expected_data[str(reply_author["external_id"])]["last_activity_at"] = time.strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) reply_id = backend.create_comment( { @@ -168,15 +162,11 @@ def build_structure_and_response( @pytest.mark.parametrize("sort_key", [None, "recency", "flagged"]) -def test_get_user_stats( - api_client: Any, sort_key: Optional[str], patched_get_backend: Any -) -> None: +def test_get_user_stats(api_client: Any, sort_key: str | None, patched_get_backend: Any) -> None: """Test retrieving user stats with various sorting options.""" backend = patched_get_backend() course_id = fake.word() - authors_ids = [ - backend.find_or_create_user(str(i), username=f"author-{i}") for i in range(1, 7) - ] + authors_ids = [backend.find_or_create_user(str(i), username=f"author-{i}") for i in range(1, 7)] authors = [backend.get_user(author_id) or {} for author_id in authors_ids] build_structure_and_response(course_id, authors, backend) @@ -188,9 +178,7 @@ def test_get_user_stats( res_data = response.json()["user_stats"] if sort_key == "recency": - expected_order = sorted( - res_data, key=lambda x: (x["last_activity_at"], x["username"]), reverse=True - ) + expected_order = sorted(res_data, key=lambda x: (x["last_activity_at"], x["username"]), reverse=True) elif sort_key == "flagged": expected_order = sorted( res_data, @@ -211,9 +199,7 @@ def test_stats_for_user_with_no_activity(api_client: Any) -> None: """Test handling stats for user with no activity.""" invalid_course_id = "course-v1:edX+DNE+Not_EXISTS" - response = api_client.get_json( - f"/api/v2/users/{invalid_course_id}/stats", params={} - ) + response = api_client.get_json(f"/api/v2/users/{invalid_course_id}/stats", params={}) assert response.status_code == 200 res_data = response.json()["user_stats"] @@ -226,26 +212,19 @@ def test_user_stats_filtered_by_user(api_client: Any, patched_get_backend: Any) course_id = fake.word() # Create some users - authors_ids = [ - backend.find_or_create_user(str(i), username=f"userauthor-{i}") - for i in range(1, 11) - ] + authors_ids = [backend.find_or_create_user(str(i), username=f"userauthor-{i}") for i in range(1, 11)] authors = [backend.get_user(author_id) or {} for author_id in authors_ids] # Build structure and response full_data = build_structure_and_response(course_id, authors, backend) # Randomly sample and shuffle usernames - usernames = [ - "userauthor-1" - ] # random.sample([f"userauthor-{i}" for i in range(1, 4)], 2) + usernames = ["userauthor-1"] # random.sample([f"userauthor-{i}" for i in range(1, 4)], 2) usernames_str = ",".join(usernames) # Get user stats filtered by usernames - response = api_client.get_json( - f"/api/v2/users/{course_id}/stats?usernames={usernames_str}", params={} - ) + response = api_client.get_json(f"/api/v2/users/{course_id}/stats?usernames={usernames_str}", params={}) assert response.status_code == 200 res_data = response.json()["user_stats"] @@ -259,17 +238,12 @@ def test_user_stats_filtered_by_user(api_client: Any, patched_get_backend: Any) assert res_data == expected_result -def test_user_stats_with_recency_sort( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_user_stats_with_recency_sort(api_client: APIClient, patched_get_backend: Any) -> None: """Test returning user stats with recency sort.""" backend = patched_get_backend() course_id = fake.word() # Create some users - authors_ids = [ - backend.find_or_create_user(str(i), username=f"userauthor-{i}") - for i in range(1, 6) - ] + authors_ids = [backend.find_or_create_user(str(i), username=f"userauthor-{i}") for i in range(1, 6)] authors = [backend.get_user(author_id) or {} for author_id in authors_ids] # Build structure with timestamps @@ -285,24 +259,17 @@ def test_user_stats_with_recency_sort( res_data = response.json()["user_stats"] # Sort by last_activity_at and username in reverse order - sorted_order = sorted( - res_data, key=lambda x: (x["last_activity_at"], x["username"]), reverse=True - ) + sorted_order = sorted(res_data, key=lambda x: (x["last_activity_at"], x["username"]), reverse=True) assert res_data == sorted_order @pytest.fixture(name="original_stats") -def get_original_stats( - api_client: APIClient, patched_get_backend: Any -) -> tuple[dict[str, Any], str, str]: +def get_original_stats(api_client: APIClient, patched_get_backend: Any) -> tuple[dict[str, Any], str, str]: """Setup the initial data structure and save stats.""" backend = patched_get_backend() course_id = fake.word() - authors_ids = [ - backend.find_or_create_user(str(i), username=f"userauthor-{i}") - for i in range(1, 4) - ] + authors_ids = [backend.find_or_create_user(str(i), username=f"userauthor-{i}") for i in range(1, 4)] authors = [backend.get_user(author_id) or {} for author_id in authors_ids] build_structure_and_response(course_id, authors, backend) @@ -323,15 +290,13 @@ def get_new_stats( api_client: APIClient, course_id: str, original_username: str, -) -> Optional[dict[str, Any]]: +) -> dict[str, Any] | None: """Fetch the new stats after performing actions.""" response = api_client.get_json(f"/api/v2/users/{course_id}/stats", params={}) assert response.status_code == 200 res_data = response.json()["user_stats"] - return next( - (stat for stat in res_data if stat["username"] == original_username), None - ) + return next((stat for stat in res_data if stat["username"] == original_username), None) def test_handles_deleting_threads( @@ -433,9 +398,7 @@ def test_handles_deleting_responses( user = backend.get_user_by_username(username) assert user is not None - comment = backend.find_comment( - author_id=user["_id"], course_id=course_id, parent_id=None - ) + comment = backend.find_comment(author_id=user["_id"], course_id=course_id, parent_id=None) assert comment is not None response = api_client.delete_json(f"/api/v2/comments/{str(comment['_id'])}") @@ -491,9 +454,7 @@ def test_handles_deleting_replies( assert user is not None # Find a reply (comment with a parent_id) - reply = backend.find_comment( - author_id=user["_id"], course_id=course_id, is_parent_comment=False - ) + reply = backend.find_comment(author_id=user["_id"], course_id=course_id, is_parent_comment=False) assert reply is not None # Delete the reply @@ -522,9 +483,7 @@ def test_handles_removing_flags( assert user is not None # Find a comment with existing abuse flaggers - comment = backend.find_comment( - author_id=user["_id"], course_id=course_id, with_abuse_flaggers=True - ) + comment = backend.find_comment(author_id=user["_id"], course_id=course_id, with_abuse_flaggers=True) assert comment is not None # Set abuse flaggers to two users @@ -560,9 +519,7 @@ def test_handles_removing_flags( assert new_stats["active_flags"] == stats["active_flags"] - 1 -def test_build_course_stats_with_anonymous_posts( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_build_course_stats_with_anonymous_posts(api_client: APIClient, patched_get_backend: Any) -> None: """Test that anonymous posts are not included in user stats after a non-anonymous post.""" backend = patched_get_backend() # Create a test user @@ -605,15 +562,10 @@ def test_update_user_stats(api_client: APIClient, patched_get_backend: Any) -> N backend = patched_get_backend() # Create a test course ID and users course_id = fake.word() - authors_ids = [ - backend.find_or_create_user(user_id=str(i), username=f"author-{i}") - for i in range(1, 7) - ] + authors_ids = [backend.find_or_create_user(user_id=str(i), username=f"author-{i}") for i in range(1, 7)] authors = [backend.get_user(author_id) or {} for author_id in authors_ids] # Build the expected data without initial stats - expected_data = build_structure_and_response( - course_id, authors, backend, build_initial_stats=False - ) + expected_data = build_structure_and_response(course_id, authors, backend, build_initial_stats=False) # Sort the data for expected result (threads, responses, replies) expected_result = sorted( @@ -639,9 +591,7 @@ def test_update_user_stats(api_client: APIClient, patched_get_backend: Any) -> N assert response.status_code == 200 res = response.json() - assert ( - res["user_stats"] == expected_result - ) # User stats should now match the expected data + assert res["user_stats"] == expected_result # User stats should now match the expected data def test_mark_thread_as_read(api_client: APIClient, patched_get_backend: Any) -> None: @@ -668,15 +618,11 @@ def test_mark_thread_as_read(api_client: APIClient, patched_get_backend: Any) -> # Reload the user and verify read state user = backend.get_user(user_id) or {} read_states = [ - course_state - for course_state in user["read_states"] - if course_state["course_id"] == thread["course_id"] + course_state for course_state in user["read_states"] if course_state["course_id"] == thread["course_id"] ] read_date = read_states[0]["last_read_times"][str(thread["_id"])] - assert ( - read_date >= thread["updated_at"] - ) # Verify the read date is on or after the thread's updated_at + assert read_date >= thread["updated_at"] # Verify the read date is on or after the thread's updated_at def test_retire_user_inactive(api_client: APIClient, patched_get_backend: Any) -> None: diff --git a/tests/test_backends/test_mysql/test_api.py b/tests/test_backends/test_mysql/test_api.py index d89a537f..ee2aa0a0 100644 --- a/tests/test_backends/test_mysql/test_api.py +++ b/tests/test_backends/test_mysql/test_api.py @@ -58,12 +58,7 @@ def test_un_flag_as_abuse_success() -> None: assert user.pk not in comment_thread.abuse_flaggers assert un_flagged_entity["_id"] == str(comment_thread.pk) - assert ( - AbuseFlagger.objects.filter( - user=user, content_object_id=comment_thread.pk - ).count() - == 0 - ) + assert AbuseFlagger.objects.filter(user=user, content_object_id=comment_thread.pk).count() == 0 @pytest.mark.django_db @@ -128,9 +123,7 @@ def test_update_stats_for_course_updates_existing_stat() -> None: ) AbuseFlagger.objects.create(user=user, content=comment_thread) AbuseFlagger.objects.create(user=user_2, content=comment_thread_2) - course_stat = CourseStat.objects.create( - user=user, course_id=course_id, active_flags=2 - ) + course_stat = CourseStat.objects.create(user=user, course_id=course_id, active_flags=2) backend.update_stats_for_course(str(user.pk), course_id, active_flags=2, threads=2) @@ -153,9 +146,7 @@ def test_update_stats_for_course_ignores_invalid_keys() -> None: context="course", ) AbuseFlagger.objects.create(user=user, content=comment_thread) - course_stat = CourseStat.objects.create( - user=user, course_id=course_id, active_flags=1 - ) + course_stat = CourseStat.objects.create(user=user, course_id=course_id, active_flags=1) # Update stats with an invalid key backend.update_stats_for_course(str(user.pk), course_id, invalid_key=10) diff --git a/tests/test_backends/test_mysql/test_models.py b/tests/test_backends/test_mysql/test_models.py index 6598c13a..b7162294 100644 --- a/tests/test_backends/test_mysql/test_models.py +++ b/tests/test_backends/test_mysql/test_models.py @@ -28,9 +28,7 @@ @pytest.mark.django_db def test_forum_user_creation() -> None: """Test that a ForumUser is created when a User is created.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") forum_user = ForumUser.objects.create(user=user, default_sort_key="date") assert forum_user.user == user assert forum_user.default_sort_key == "date" @@ -39,9 +37,7 @@ def test_forum_user_creation() -> None: @pytest.mark.django_db def test_forum_user_unique_constraint() -> None: """Test that a ForumUser is not created when a User already has one.""" - user1 = User.objects.create( - username="testuser1", email="test1@example.com", password="password" - ) + user1 = User.objects.create(username="testuser1", email="test1@example.com", password="password") ForumUser.objects.create(user=user1, default_sort_key="date") with pytest.raises(IntegrityError): ForumUser.objects.create(user=user1, default_sort_key="date") @@ -50,9 +46,7 @@ def test_forum_user_unique_constraint() -> None: @pytest.mark.django_db def test_forum_user_default_sort_key_default_value() -> None: """Test that the default_sort_key is set to 'date' when not provided.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") forum_user = ForumUser.objects.create(user=user) assert forum_user.default_sort_key == "date" @@ -60,9 +54,7 @@ def test_forum_user_default_sort_key_default_value() -> None: @pytest.mark.django_db def test_course_stat_creation() -> None: """Test that a CourseStat is created when a Course is created.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") course_stat = CourseStat.objects.create(user=user, course_id="course123") assert course_stat.user == user assert course_stat.course_id == "course123" @@ -77,9 +69,7 @@ def test_course_stat_creation() -> None: @pytest.mark.django_db def test_course_stat_unique_constraint() -> None: """Test that a CourseStat is not created when a User already has one.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") CourseStat.objects.create(user=user, course_id="course123") with pytest.raises(IntegrityError): CourseStat.objects.create(user=user, course_id="course123") @@ -88,9 +78,7 @@ def test_course_stat_unique_constraint() -> None: @pytest.mark.django_db def test_comment_thread_creation() -> None: """Test that a CommentThread is created when a Comment is created.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -112,9 +100,7 @@ def test_comment_thread_creation() -> None: @pytest.mark.django_db def test_comment_creation() -> None: """Test that a Comment is created when a CommentThread is created.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -140,9 +126,7 @@ def test_comment_creation() -> None: @pytest.mark.django_db def test_comment_thread_update() -> None: """Test that a Comment's thread is updated when the thread is updated.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -161,9 +145,7 @@ def test_comment_thread_update() -> None: @pytest.mark.django_db def test_comment_update() -> None: """Test that a Comment's body is updated when the comment is updated.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -186,9 +168,7 @@ def test_comment_update() -> None: @pytest.mark.django_db def test_forum_user_update() -> None: """Test that a ForumUser's data is updated when the user is updated.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") forum_user = ForumUser.objects.create(user=user, default_sort_key="date") forum_user.default_sort_key = "votes" forum_user.save() @@ -198,9 +178,7 @@ def test_forum_user_update() -> None: @pytest.mark.django_db def test_comment_thread_delete() -> None: """Test that a CommentThread's data is deleted when the thread is deleted.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -216,9 +194,7 @@ def test_comment_thread_delete() -> None: @pytest.mark.django_db def test_comment_delete() -> None: """Test that a Comment's data is deleted when the comment is deleted.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -240,9 +216,7 @@ def test_comment_delete() -> None: @pytest.mark.django_db def test_forum_user_delete() -> None: """Test that a ForumUser's data is deleted when the user is deleted.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") forum_user = ForumUser.objects.create(user=user, default_sort_key="date") forum_user.delete() assert ForumUser.objects.filter(id=forum_user.pk).count() == 0 @@ -251,9 +225,7 @@ def test_forum_user_delete() -> None: @pytest.mark.django_db def test_edit_history_creation() -> None: """Test that an EditHistory is created when a Comment is edited.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -277,9 +249,7 @@ def test_edit_history_creation() -> None: @pytest.mark.django_db def test_edit_history_update() -> None: """Test that an EditHistory is updated when a Comment is edited.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -302,9 +272,7 @@ def test_edit_history_update() -> None: @pytest.mark.django_db def test_edit_history_delete() -> None: """Test that an EditHistory is deleted when a Comment is deleted.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -326,9 +294,7 @@ def test_edit_history_delete() -> None: @pytest.mark.django_db def test_abuseflagger_creation() -> None: """Test that an AbuseFlagger is created when a Comment is flagged.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -337,9 +303,7 @@ def test_abuseflagger_creation() -> None: thread_type="discussion", context="course", ) - abuseflagger = AbuseFlagger.objects.create( - user=user, content=comment_thread, flagged_at=timezone.now() - ) + abuseflagger = AbuseFlagger.objects.create(user=user, content=comment_thread, flagged_at=timezone.now()) assert abuseflagger.user == user assert abuseflagger.content == comment_thread assert abuseflagger.flagged_at is not None @@ -348,9 +312,7 @@ def test_abuseflagger_creation() -> None: @pytest.mark.django_db def test_abuseflagger_update() -> None: """Test that an AbuseFlagger is updated when a Comment is flagged.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -359,9 +321,7 @@ def test_abuseflagger_update() -> None: thread_type="discussion", context="course", ) - abuseflagger = AbuseFlagger.objects.create( - user=user, content=comment_thread, flagged_at=timezone.now() - ) + abuseflagger = AbuseFlagger.objects.create(user=user, content=comment_thread, flagged_at=timezone.now()) update_flagged_at = timezone.now() + timedelta(hours=1) abuseflagger.flagged_at = update_flagged_at abuseflagger.save() @@ -371,9 +331,7 @@ def test_abuseflagger_update() -> None: @pytest.mark.django_db def test_abuseflagger_delete() -> None: """Test that an AbuseFlagger is deleted when a Comment is unflagged.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -382,9 +340,7 @@ def test_abuseflagger_delete() -> None: thread_type="discussion", context="course", ) - abuseflagger = AbuseFlagger.objects.create( - user=user, content=comment_thread, flagged_at=timezone.now() - ) + abuseflagger = AbuseFlagger.objects.create(user=user, content=comment_thread, flagged_at=timezone.now()) abuseflagger.delete() assert AbuseFlagger.objects.filter(id=abuseflagger.pk).count() == 0 @@ -392,9 +348,7 @@ def test_abuseflagger_delete() -> None: @pytest.mark.django_db def test_historicalabuseflagger_creation() -> None: """Test that a HistoricalAbuseFlagger is created when an AbuseFlagger is updated.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -414,9 +368,7 @@ def test_historicalabuseflagger_creation() -> None: @pytest.mark.django_db def test_historicalabuseflagger_update() -> None: """Test that a HistoricalAbuseFlagger is updated when an AbuseFlagger is updated.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -437,9 +389,7 @@ def test_historicalabuseflagger_update() -> None: @pytest.mark.django_db def test_historicalabuseflagger_delete() -> None: """Test that a HistoricalAbuseFlagger is deleted when an AbuseFlagger is deleted.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -452,17 +402,13 @@ def test_historicalabuseflagger_delete() -> None: user=user, content=comment_thread, flagged_at=timezone.now() ) historicalabuseflagger.delete() - assert ( - HistoricalAbuseFlagger.objects.filter(id=historicalabuseflagger.pk).count() == 0 - ) + assert HistoricalAbuseFlagger.objects.filter(id=historicalabuseflagger.pk).count() == 0 @pytest.mark.django_db def test_readstate_creation() -> None: """Test that a ReadState is created when a user reads a comment thread.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") course_id = "course123" read_state = ReadState.objects.create(user=user, course_id=course_id) assert read_state.user == user @@ -472,9 +418,7 @@ def test_readstate_creation() -> None: @pytest.mark.django_db def test_readstate_update() -> None: """Test that a ReadState is updated when a user reads a comment thread.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") course_id = "course123" read_state = ReadState.objects.create(user=user, course_id=course_id) new_course_id = "course456" @@ -486,9 +430,7 @@ def test_readstate_update() -> None: @pytest.mark.django_db def test_readstate_delete() -> None: """Test that a ReadState is deleted when a user is deleted.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") course_id = "course123" read_state = ReadState.objects.create(user=user, course_id=course_id) read_state.delete() @@ -498,9 +440,7 @@ def test_readstate_delete() -> None: @pytest.mark.django_db def test_lastreadtime_creation() -> None: """Test that a LastReadTime is created when a user reads a comment thread.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") course_id = "course123" read_state = ReadState.objects.create(user=user, course_id=course_id) comment_thread = CommentThread.objects.create( @@ -522,9 +462,7 @@ def test_lastreadtime_creation() -> None: @pytest.mark.django_db def test_lastreadtime_update() -> None: """Test that a LastReadTime is updated when a user reads a comment thread.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") course_id = "course123" read_state = ReadState.objects.create(user=user, course_id=course_id) comment_thread = CommentThread.objects.create( @@ -547,9 +485,7 @@ def test_lastreadtime_update() -> None: @pytest.mark.django_db def test_lastreadtime_delete() -> None: """Test that a LastReadTime is deleted when a user's read state is deleted.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") course_id = "course123" read_state = ReadState.objects.create(user=user, course_id=course_id) comment_thread = CommentThread.objects.create( @@ -570,9 +506,7 @@ def test_lastreadtime_delete() -> None: @pytest.mark.django_db def test_lastreadtime_multiple_read_states() -> None: """Test that a LastReadTime is created for each read state when a user reads a comment.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") course_id1 = "course123" course_id2 = "course456" read_state1 = ReadState.objects.create(user=user, course_id=course_id1) @@ -608,9 +542,7 @@ def test_lastreadtime_multiple_read_states() -> None: @pytest.mark.django_db def test_lastreadtime_multiple_comment_threads() -> None: """Test that a LastReadTime is created for each comment thread when a user reads a comment.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") course_id = "course123" read_state = ReadState.objects.create(user=user, course_id=course_id) comment_thread1 = CommentThread.objects.create( @@ -644,9 +576,7 @@ def test_lastreadtime_multiple_comment_threads() -> None: @pytest.mark.django_db def test_readstate_multiple_last_read_times() -> None: """Test that a ReadState can have multiple LastReadTime instances.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") course_id = "course123" read_state = ReadState.objects.create(user=user, course_id=course_id) comment_thread1 = CommentThread.objects.create( @@ -665,12 +595,8 @@ def test_readstate_multiple_last_read_times() -> None: thread_type="discussion", context="course", ) - LastReadTime.objects.create( - read_state=read_state, comment_thread=comment_thread1, timestamp=timezone.now() - ) - LastReadTime.objects.create( - read_state=read_state, comment_thread=comment_thread2, timestamp=timezone.now() - ) + LastReadTime.objects.create(read_state=read_state, comment_thread=comment_thread1, timestamp=timezone.now()) + LastReadTime.objects.create(read_state=read_state, comment_thread=comment_thread2, timestamp=timezone.now()) assert read_state.last_read_times.count() == 2 assert read_state.last_read_times.filter(comment_thread=comment_thread1).exists() assert read_state.last_read_times.filter(comment_thread=comment_thread2).exists() @@ -679,9 +605,7 @@ def test_readstate_multiple_last_read_times() -> None: @pytest.mark.django_db def test_readstate_last_read_time_update() -> None: """Test that updating a LastReadTime instance updates the read state's last read time.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") course_id = "course123" read_state = ReadState.objects.create(user=user, course_id=course_id) comment_thread = CommentThread.objects.create( @@ -698,18 +622,13 @@ def test_readstate_last_read_time_update() -> None: new_timestamp = timezone.now() + timedelta(hours=1) last_read_time.timestamp = new_timestamp last_read_time.save() - assert ( - read_state.last_read_times.get(comment_thread=comment_thread).timestamp - == new_timestamp - ) + assert read_state.last_read_times.get(comment_thread=comment_thread).timestamp == new_timestamp @pytest.mark.django_db def test_readstate_last_read_time_delete() -> None: """Test that deleting a LastReadTime instance removes it from the read state's last read times.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") course_id = "course123" read_state = ReadState.objects.create(user=user, course_id=course_id) comment_thread = CommentThread.objects.create( @@ -730,9 +649,7 @@ def test_readstate_last_read_time_delete() -> None: @pytest.mark.django_db def test_uservotes_creation() -> None: """Test that creating a UserVote instance creates a vote for the user.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") content = CommentThread.objects.create( author=user, course_id="course123", @@ -750,9 +667,7 @@ def test_uservotes_creation() -> None: @pytest.mark.django_db def test_uservotes_update() -> None: """Test that updating a UserVote instance updates the vote for the user.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") content = CommentThread.objects.create( author=user, course_id="course123", @@ -770,9 +685,7 @@ def test_uservotes_update() -> None: @pytest.mark.django_db def test_uservotes_delete() -> None: """Test that deleting a UserVote instance removes the vote for the user.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") content = CommentThread.objects.create( author=user, course_id="course123", @@ -789,9 +702,7 @@ def test_uservotes_delete() -> None: @pytest.mark.django_db def test_uservotes_generic_foreign_key() -> None: """Test that the generic foreign key is correctly set.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") content1 = CommentThread.objects.create( author=user, course_id="course123", @@ -816,12 +727,8 @@ def test_uservotes_generic_foreign_key() -> None: def test_subscription_creation() -> None: """Test that a subscription is created when a user votes on a content.""" content_type = ContentType.objects.get_for_model(CommentThread) - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) - subscription = Subscription.objects.create( - subscriber=user, source_content_type=content_type, source_object_id=1 - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") + subscription = Subscription.objects.create(subscriber=user, source_content_type=content_type, source_object_id=1) assert subscription.subscriber == user assert subscription.source_content_type == content_type assert subscription.source_object_id == 1 @@ -831,24 +738,16 @@ def test_subscription_creation() -> None: def test_subscription_unique_together() -> None: """Test that the unique together constraint is enforced.""" content_type = ContentType.objects.get_for_model(CommentThread) - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) - Subscription.objects.create( - subscriber=user, source_content_type=content_type, source_object_id=1 - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") + Subscription.objects.create(subscriber=user, source_content_type=content_type, source_object_id=1) with pytest.raises(IntegrityError): - Subscription.objects.create( - subscriber=user, source_content_type=content_type, source_object_id=1 - ) + Subscription.objects.create(subscriber=user, source_content_type=content_type, source_object_id=1) @pytest.mark.django_db def test_comment_thread_author_username_set_on_creation() -> None: """Test that author_username is automatically set on CommentThread creation.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -863,9 +762,7 @@ def test_comment_thread_author_username_set_on_creation() -> None: @pytest.mark.django_db def test_comment_author_username_set_on_creation() -> None: """Test that author_username is automatically set on Comment creation.""" - user = User.objects.create( - username="testuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="testuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -886,9 +783,7 @@ def test_comment_author_username_set_on_creation() -> None: @pytest.mark.django_db def test_comment_thread_author_username_preserved_when_provided() -> None: """Test that author_username is preserved when explicitly provided.""" - user = User.objects.create( - username="newusername", email="test@example.com", password="password" - ) + user = User.objects.create(username="newusername", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, author_username="originalusername", @@ -904,9 +799,7 @@ def test_comment_thread_author_username_preserved_when_provided() -> None: @pytest.mark.django_db def test_comment_author_username_preserved_when_provided() -> None: """Test that author_username is preserved when explicitly provided.""" - user = User.objects.create( - username="newusername", email="test@example.com", password="password" - ) + user = User.objects.create(username="newusername", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -928,9 +821,7 @@ def test_comment_author_username_preserved_when_provided() -> None: @pytest.mark.django_db def test_comment_thread_retired_username_priority_on_creation() -> None: """Test that retired_username takes priority when set during creation.""" - user = User.objects.create( - username="retired_user_abc123", email="test@example.com", password="password" - ) + user = User.objects.create(username="retired_user_abc123", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, retired_username="originaluser", @@ -948,9 +839,7 @@ def test_comment_thread_retired_username_priority_on_creation() -> None: @pytest.mark.django_db def test_comment_retired_username_priority_on_creation() -> None: """Test that retired_username takes priority when set during creation.""" - user = User.objects.create( - username="retired_user_abc123", email="test@example.com", password="password" - ) + user = User.objects.create(username="retired_user_abc123", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -974,9 +863,7 @@ def test_comment_retired_username_priority_on_creation() -> None: @pytest.mark.django_db def test_comment_thread_to_dict_uses_author_username() -> None: """Test that to_dict uses author_username when available.""" - user = User.objects.create( - username="currentuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="currentuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, author_username="historicaluser", @@ -993,9 +880,7 @@ def test_comment_thread_to_dict_uses_author_username() -> None: @pytest.mark.django_db def test_comment_to_dict_uses_author_username() -> None: """Test that to_dict uses author_username when available.""" - user = User.objects.create( - username="currentuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="currentuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -1018,9 +903,7 @@ def test_comment_to_dict_uses_author_username() -> None: @pytest.mark.django_db def test_comment_thread_to_dict_fallback_to_retired_username() -> None: """Test that to_dict falls back to retired_username when author_username is None.""" - user = User.objects.create( - username="retired_user_abc123", email="test@example.com", password="password" - ) + user = User.objects.create(username="retired_user_abc123", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, retired_username="retireduser", @@ -1041,9 +924,7 @@ def test_comment_thread_to_dict_fallback_to_retired_username() -> None: @pytest.mark.django_db def test_comment_to_dict_fallback_to_retired_username() -> None: """Test that to_dict falls back to retired_username when author_username is None.""" - user = User.objects.create( - username="retired_user_abc123", email="test@example.com", password="password" - ) + user = User.objects.create(username="retired_user_abc123", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -1070,9 +951,7 @@ def test_comment_to_dict_fallback_to_retired_username() -> None: @pytest.mark.django_db def test_comment_thread_to_dict_fallback_to_current_username() -> None: """Test that to_dict falls back to current username when both author_username and retired_username are None.""" - user = User.objects.create( - username="currentuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="currentuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", @@ -1093,9 +972,7 @@ def test_comment_thread_to_dict_fallback_to_current_username() -> None: @pytest.mark.django_db def test_comment_to_dict_fallback_to_current_username() -> None: """Test that to_dict falls back to current username when both author_username and retired_username are None.""" - user = User.objects.create( - username="currentuser", email="test@example.com", password="password" - ) + user = User.objects.create(username="currentuser", email="test@example.com", password="password") comment_thread = CommentThread.objects.create( author=user, course_id="course123", diff --git a/tests/test_management/test_commands/test_migration_commands.py b/tests/test_management/test_commands/test_migration_commands.py index bef8400a..4b8aec11 100644 --- a/tests/test_management/test_commands/test_migration_commands.py +++ b/tests/test_management/test_commands/test_migration_commands.py @@ -1,13 +1,13 @@ """Test forum mongodb migration commands.""" -from io import StringIO from datetime import timedelta +from io import StringIO from typing import Any import pytest from bson import ObjectId -from django.core.management import call_command from django.contrib.auth.models import User # pylint: disable=E5142 +from django.core.management import call_command from django.utils import timezone from pymongo.database import Database @@ -244,9 +244,7 @@ def test_migrate_subscriptions(patched_mongodb: Database[Any]) -> None: mongo_thread = MongoContent.objects.get(mongo_id=str(comment_thread_id)) - assert Subscription.objects.filter( - subscriber=user, source_object_id=mongo_thread.content_object_id - ).exists() + assert Subscription.objects.filter(subscriber=user, source_object_id=mongo_thread.content_object_id).exists() def test_delete_course_data(patched_mongodb: Database[Any]) -> None: @@ -359,9 +357,7 @@ def test_delete_dry_run(patched_mongodb: Database[Any]) -> None: } ) out = StringIO() - call_command( - "forum_delete_course_from_mongodb", "test_course", "--dry-run", stdout=out - ) + call_command("forum_delete_course_from_mongodb", "test_course", "--dry-run", stdout=out) output = out.getvalue() assert "Performing dry run. No data will be deleted." in output @@ -481,26 +477,16 @@ def test_last_read_times_migration(patched_mongodb: Database[Any]) -> None: assert thread.body == "Test body" read_state = ReadState.objects.get(user=user, course_id="test_course") - last_read_time = LastReadTime.objects.filter( - read_state=read_state, comment_thread=thread - ).first() + last_read_time = LastReadTime.objects.filter(read_state=read_state, comment_thread=thread).first() assert last_read_time is not None updated_last_read_time_for_thread = timezone.now() patched_mongodb.users.update_one( {"_id": "1"}, - { - "$set": { - "read_states.0.last_read_times": { - str(comment_thread_id): updated_last_read_time_for_thread - } - } - }, + {"$set": {"read_states.0.last_read_times": {str(comment_thread_id): updated_last_read_time_for_thread}}}, ) call_command("forum_migrate_course_from_mongodb_to_mysql", "test_course") - updated_last_read_time = LastReadTime.objects.filter( - read_state=read_state, comment_thread=thread - ).first() + updated_last_read_time = LastReadTime.objects.filter(read_state=read_state, comment_thread=thread).first() assert updated_last_read_time is not None assert updated_last_read_time.timestamp > last_read_time.timestamp @@ -1060,9 +1046,7 @@ def test_migrate_preserves_timestamps(patched_mongodb: Database[Any]) -> None: ) # Verify subscription timestamps are preserved - subscription = Subscription.objects.get( - subscriber_id=1, source_object_id=mongo_thread.content_object_id - ) + subscription = Subscription.objects.get(subscriber_id=1, source_object_id=mongo_thread.content_object_id) subscription_created_age = now - subscription.created_at assert subscription_created_age > timedelta(days=24), ( diff --git a/tests/test_meilisearch.py b/tests/test_meilisearch.py index 20672e90..4b369750 100644 --- a/tests/test_meilisearch.py +++ b/tests/test_meilisearch.py @@ -2,9 +2,10 @@ Unit tests for the meilisearch search backend. """ -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import search.meilisearch as m + from forum.search import meilisearch TEST_ID = "abcd" @@ -32,9 +33,7 @@ def test_create_document() -> None: assert { "id": TEST_ID, m.PRIMARY_KEY_FIELD_NAME: TEST_PK, - } == meilisearch.create_document( - {"field_should_not_be_here": "some_value"}, TEST_ID - ) + } == meilisearch.create_document({"field_should_not_be_here": "some_value"}, TEST_ID) assert { "id": TEST_ID, @@ -45,9 +44,7 @@ def test_create_document() -> None: def test_index_document() -> None: backend = meilisearch.MeilisearchDocumentBackend() - with patch.object( - backend, "get_index", return_value=Mock(add_documents=Mock()) - ) as mock_get_index: + with patch.object(backend, "get_index", return_value=Mock(add_documents=Mock())) as mock_get_index: backend.index_document( "my_index", TEST_ID, @@ -70,9 +67,7 @@ def test_index_document() -> None: def test_delete_document() -> None: backend = meilisearch.MeilisearchDocumentBackend() - with patch.object( - backend, "get_index", return_value=Mock(add_documents=Mock()) - ) as mock_get_index: + with patch.object(backend, "get_index", return_value=Mock(add_documents=Mock())) as mock_get_index: backend.delete_document("my_index", TEST_ID) mock_get_index.assert_called_once_with("my_index") mock_get_index().delete_document.assert_called_once_with(TEST_PK) diff --git a/tests/test_settings.py b/tests/test_settings.py index ee44defe..b54bb16b 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -8,9 +8,7 @@ def test_plugin_settings_typesense() -> None: - settings = MagicMock( - spec=["TYPESENSE_ENABLED", "FEATURES"], TYPESENSE_ENABLED=True, FEATURES={} - ) + settings = MagicMock(spec=["TYPESENSE_ENABLED", "FEATURES"], TYPESENSE_ENABLED=True, FEATURES={}) # This function changes settings in-place. plugin_settings(settings) diff --git a/tests/test_typesense.py b/tests/test_typesense.py index 16da561e..9ac9404e 100644 --- a/tests/test_typesense.py +++ b/tests/test_typesense.py @@ -2,13 +2,13 @@ Unit tests for the typesense search backend. """ -from unittest.mock import patch, MagicMock, Mock +from unittest.mock import MagicMock, Mock, patch import pytest from typesense.exceptions import ObjectNotFound -from forum.search import typesense from forum import constants +from forum.search import typesense def test_quote_filter() -> None: @@ -78,12 +78,8 @@ def test_document_from_comment() -> None: def test_search_threads(mock_get_client: Mock) -> None: mock_client = MagicMock() mock_get_client.return_value = mock_client - mock_search = mock_client.collections[ - "forum_unittest_prefix_forum" - ].documents.search - mock_search.return_value = { - "hits": [{"document": {"thread_id": "ONE"}}, {"document": {"thread_id": "TWO"}}] - } + mock_search = mock_client.collections["forum_unittest_prefix_forum"].documents.search + mock_search.return_value = {"hits": [{"document": {"thread_id": "ONE"}}, {"document": {"thread_id": "TWO"}}]} backend = typesense.TypesenseThreadSearchBackend() assert sorted( @@ -177,11 +173,7 @@ def test_index_invalid_type() -> None: def test_delete_document(mock_get_client: Mock) -> None: mock_client = MagicMock() mock_get_client.return_value = mock_client - mock_delete = ( - mock_client.collections["forum_unittest_prefix_forum"] - .documents["comment-MYCOMMENTID"] - .delete - ) + mock_delete = mock_client.collections["forum_unittest_prefix_forum"].documents["comment-MYCOMMENTID"].delete backend = typesense.TypesenseDocumentBackend() backend.delete_document("comments", "MYCOMMENTID") diff --git a/tests/test_views/test_commentables.py b/tests/test_views/test_commentables.py index 2d650715..0cea257f 100644 --- a/tests/test_views/test_commentables.py +++ b/tests/test_views/test_commentables.py @@ -1,9 +1,8 @@ """Test commentables count api endpoint.""" -from typing import Any - import random import uuid +from typing import Any import pytest @@ -12,9 +11,7 @@ pytestmark = pytest.mark.django_db -def test_get_commentables_counts_api( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_get_commentables_counts_api(api_client: APIClient, patched_get_backend: Any) -> None: """ Test retrieving counts of discussion and question threads for multiple commentables within a course. """ diff --git a/tests/test_views/test_comments.py b/tests/test_views/test_comments.py index 799a6145..629d8f78 100644 --- a/tests/test_views/test_comments.py +++ b/tests/test_views/test_comments.py @@ -1,6 +1,7 @@ """Test comments api endpoints.""" from typing import Any + import pytest from test_utils.client import APIClient @@ -89,9 +90,7 @@ def test_get_comment_api(api_client: APIClient, patched_get_backend: Any) -> Non assert comment["child_count"] == 0 -def test_update_comment_endorsed_api( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_update_comment_endorsed_api(api_client: APIClient, patched_get_backend: Any) -> None: """ Test updating the endorsed status of a parent comment. """ @@ -174,37 +173,25 @@ def test_delete_child_comment(api_client: APIClient, patched_get_backend: Any) - assert new_child_count == previous_child_count - 1 -def test_returns_400_when_comment_does_not_exist( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_returns_400_when_comment_does_not_exist(api_client: APIClient, patched_get_backend: Any) -> None: """Test 400 status code in case of invalid comment.""" backend = patched_get_backend incorrect_comment_id = backend.generate_id() response = api_client.get_json(f"/api/v2/comments/{incorrect_comment_id}", {}) assert response.status_code == 400 - assert response.json() == { - "error": f"Comment does not exist with Id: {incorrect_comment_id}" - } + assert response.json() == {"error": f"Comment does not exist with Id: {incorrect_comment_id}"} - response = api_client.put_json( - f"/api/v2/comments/{incorrect_comment_id}", data={"body": "new_body"} - ) + response = api_client.put_json(f"/api/v2/comments/{incorrect_comment_id}", data={"body": "new_body"}) assert response.status_code == 400 - assert response.json() == { - "error": f"Comment does not exist with Id: {incorrect_comment_id}" - } + assert response.json() == {"error": f"Comment does not exist with Id: {incorrect_comment_id}"} response = api_client.delete_json(f"/api/v2/comments/{incorrect_comment_id}") assert response.status_code == 400 - assert response.json() == { - "error": f"Comment does not exist with Id: {incorrect_comment_id}" - } + assert response.json() == {"error": f"Comment does not exist with Id: {incorrect_comment_id}"} -def test_updates_body_correctly( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_updates_body_correctly(api_client: APIClient, patched_get_backend: Any) -> None: """ Test updating the body of a comment. """ @@ -238,9 +225,7 @@ def test_updates_body_correctly( assert edit_history[0]["editor_username"] == editing_username -def test_updates_body_correctly_without_user_id( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_updates_body_correctly_without_user_id(api_client: APIClient, patched_get_backend: Any) -> None: """ Test updating the body of a comment without user id. """ @@ -258,9 +243,7 @@ def test_updates_body_correctly_without_user_id( assert ("edit_history" not in updated_comment) is True -def test_update_endorsed_and_body_simultaneously( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_update_endorsed_and_body_simultaneously(api_client: APIClient, patched_get_backend: Any) -> None: """ Test updating the body and endorse status of a comment simultaneously. """ @@ -279,9 +262,7 @@ def test_update_endorsed_and_body_simultaneously( assert updated_comment["endorsed"] is True -def test_thread_comment_post_api( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_thread_comment_post_api(api_client: APIClient, patched_get_backend: Any) -> None: """ Test creating a new parent comment. """ diff --git a/tests/test_views/test_flags.py b/tests/test_views/test_flags.py index a1965026..466b298b 100644 --- a/tests/test_views/test_flags.py +++ b/tests/test_views/test_flags.py @@ -1,6 +1,7 @@ """Test flags api endpoints.""" from typing import Any + import pytest from test_utils.client import APIClient @@ -125,9 +126,7 @@ def test_comment_flag_api(api_client: APIClient, patched_get_backend: Any) -> No assert comment["abuse_flaggers"] == [] -def test_comment_flag_api_invalid_data( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_comment_flag_api_invalid_data(api_client: APIClient, patched_get_backend: Any) -> None: """ Test the comment flag API with invalid data. @@ -146,9 +145,7 @@ def test_comment_flag_api_invalid_data( assert response.json() == {"error": "User / Comment doesn't exist"} -def test_comment_flag_api_with_all_param( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_comment_flag_api_with_all_param(api_client: APIClient, patched_get_backend: Any) -> None: """ Test the comment flag API with the `all` parameter. @@ -221,7 +218,7 @@ def test_comment_flag_api_with_all_param( comment = backend.get_comment(unflagged_comment["id"]) assert comment is not None assert comment["abuse_flaggers"] == [] - assert set(comment["historical_abuse_flaggers"]) == set([flag_user, flag_user_2]) + assert set(comment["historical_abuse_flaggers"]) == {flag_user, flag_user_2} # Test thread abuse and unabuse. response = api_client.put_json( @@ -251,9 +248,7 @@ def test_comment_flag_api_with_all_param( assert thread["historical_abuse_flaggers"] == [flag_user] -def test_flag_unflag_thread_twice( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_flag_unflag_thread_twice(api_client: APIClient, patched_get_backend: Any) -> None: """ Test flagging a thread, unflagging it, flagging it again, and unflagging it again. """ @@ -287,10 +282,7 @@ def test_flag_unflag_thread_twice( assert thread is not None assert thread["abuse_flaggers"] == [flag_user] # After first flag, historical_abuse_flaggers should be unchanged or contain flag_user - assert ( - flag_user in thread["historical_abuse_flaggers"] - or thread["historical_abuse_flaggers"] == [] - ) + assert flag_user in thread["historical_abuse_flaggers"] or thread["historical_abuse_flaggers"] == [] # Unflag the thread response = api_client.put_json( diff --git a/tests/test_views/test_pins.py b/tests/test_views/test_pins.py index fc9bdecc..1eae172b 100644 --- a/tests/test_views/test_pins.py +++ b/tests/test_views/test_pins.py @@ -1,6 +1,7 @@ """Test pin/unpin thread api endpoints.""" from typing import Any + import pytest from test_utils.client import APIClient @@ -8,9 +9,7 @@ pytestmark = pytest.mark.django_db -def test_pin_and_unpin_thread_api( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_pin_and_unpin_thread_api(api_client: APIClient, patched_get_backend: Any) -> None: """ Test the pin/unpin thread API. This test checks that a user can pin/unpin a thread. @@ -66,9 +65,7 @@ def test_pin_and_unpin_thread_api( assert thread["pinned"] is False -def test_pin_unpin_thread_api_invalid_data( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_pin_unpin_thread_api_invalid_data(api_client: APIClient, patched_get_backend: Any) -> None: """ Test the invalid data for pin/unpin thread API. This test checks that if user/thread exists or not. diff --git a/tests/test_views/test_search.py b/tests/test_views/test_search.py index af0eb9d9..948a2db0 100644 --- a/tests/test_views/test_search.py +++ b/tests/test_views/test_search.py @@ -19,7 +19,7 @@ """ import time -from typing import Any, Optional +from typing import Any from unittest.mock import patch from urllib.parse import urlencode @@ -43,9 +43,9 @@ def assert_result_total(response: Response, expected_total: int) -> None: def get_search_response( api_client: APIClient, params: dict[str, str], - get_thread_ids_value: Optional[list[str]] = None, - get_suggested_text_value: Optional[str] = "", - get_thread_ids_with_corrected_text_values: Optional[list[str]] = None, + get_thread_ids_value: list[str] | None = None, + get_suggested_text_value: str | None = "", + get_thread_ids_with_corrected_text_values: list[str] | None = None, ) -> Response: """ Helper function to patch ElasticsearchThreadSearchBackend methods and get search response. @@ -59,9 +59,7 @@ def get_search_response( """ get_thread_ids_value = get_thread_ids_value or [] - get_thread_ids_with_corrected_text_values = ( - get_thread_ids_with_corrected_text_values or [] - ) + get_thread_ids_with_corrected_text_values = get_thread_ids_with_corrected_text_values or [] with patch.object( ElasticsearchThreadSearchBackend, @@ -79,9 +77,7 @@ def get_search_response( return_value=get_thread_ids_with_corrected_text_values, ): encoded_params = urlencode(params) - return api_client.get_json( - f"/api/v2/search/threads?{encoded_params}", {} - ) + return api_client.get_json(f"/api/v2/search/threads?{encoded_params}", {}) def refresh_elastic_search_indices() -> None: @@ -133,9 +129,7 @@ def test_invalid_request(api_client: APIClient, patched_get_backend: Any) -> Non assert response.status_code == 400 -def test_search_returns_empty_for_deleted_thread( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_search_returns_empty_for_deleted_thread(api_client: APIClient, patched_get_backend: Any) -> None: """ Test that searching for a deleted thread returns no results. @@ -171,9 +165,7 @@ def test_search_returns_empty_for_deleted_thread( assert_result_total(response, 0) -def test_search_returns_only_updated_thread( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_search_returns_only_updated_thread(api_client: APIClient, patched_get_backend: Any) -> None: """ Test that searching for a thread returns only the updated version. @@ -213,9 +205,7 @@ def test_search_returns_only_updated_thread( assert_result_total(response, 1) -def test_search_returns_empty_for_deleted_comment( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_search_returns_empty_for_deleted_comment(api_client: APIClient, patched_get_backend: Any) -> None: """ Test that searching for a deleted comment returns no results. @@ -258,9 +248,7 @@ def test_search_returns_empty_for_deleted_comment( assert_result_total(response, 0) -def test_search_returns_only_updated_comment( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_search_returns_only_updated_comment(api_client: APIClient, patched_get_backend: Any) -> None: """ Test that searching for a comment returns only the updated version. @@ -385,16 +373,12 @@ def test_filter_threads(api_client: APIClient, patched_get_backend: Any) -> None refresh_elastic_search_indices() # Test filtering by course_id - def assert_response_contains( - response: Response, expected_indexes: list[int] - ) -> None: + def assert_response_contains(response: Response, expected_indexes: list[int]) -> None: assert response.status_code == 200 threads = response.json()["collection"] expected_ids = {threads_ids[i] for i in expected_indexes} actual_ids = {thread["id"] for thread in threads} - assert ( - actual_ids == expected_ids - ), f"Expected {expected_ids}, but got {actual_ids}" + assert actual_ids == expected_ids, f"Expected {expected_ids}, but got {actual_ids}" # Test filtering by course_id params = {"text": "text", "course_id": course_id_0} @@ -469,9 +453,7 @@ def assert_response_contains( # Test filtering by commentable_ids params = {"text": "text", "commentable_ids": "commentable0,commentable1"} - response = get_search_response( - api_client, params, [threads_ids[i] for i in range(35) if i % 3 in [0, 1]] - ) + response = get_search_response(api_client, params, [threads_ids[i] for i in range(35) if i % 3 in [0, 1]]) assert_response_contains(response, [i for i in range(30) if i % 3 in [0, 1]]) # Test filtering by group_id @@ -521,7 +503,7 @@ def test_pagination(api_client: APIClient, patched_get_backend: Any) -> None: refresh_elastic_search_indices() - def check_pagination(per_page: Optional[int], num_pages: int) -> None: + def check_pagination(per_page: int | None, num_pages: int) -> None: result_ids = [] params = {"text": "text"} if per_page: @@ -586,7 +568,7 @@ def test_sorting(api_client: APIClient, patched_get_backend: Any) -> None: backend.update_thread(thread_id=threads_ids[2], votes=votes) refresh_elastic_search_indices() - def fetch_and_check(sort_key: Optional[str], expected_indexes: list[int]) -> None: + def fetch_and_check(sort_key: str | None, expected_indexes: list[int]) -> None: params = {"text": "text"} if sort_key: params["sort_key"] = str(sort_key) @@ -597,9 +579,7 @@ def fetch_and_check(sort_key: Optional[str], expected_indexes: list[int]) -> Non threads = result["collection"] expected_ids = [threads_ids[i] for i in expected_indexes] actual_ids = [thread["id"] for thread in threads] - assert ( - actual_ids == expected_ids - ), f"Expected {expected_ids}, but got {actual_ids}" + assert actual_ids == expected_ids, f"Expected {expected_ids}, but got {actual_ids}" # Test various sorting scenarios # fetch_and_check("date", [5, 4, 3, 2, 1, 0]) @@ -647,7 +627,7 @@ def get_threda_ids_for_fixtures(original_text: str) -> list[str]: else: return [] - def check_correction(original_text: str, corrected_text: Optional[str]) -> None: + def check_correction(original_text: str, corrected_text: str | None) -> None: params = {"text": original_text} get_thread_ids_value = get_threda_ids_for_fixtures(original_text) response = get_search_response( @@ -659,12 +639,10 @@ def check_correction(original_text: str, corrected_text: Optional[str]) -> None: ) assert response.status_code == 200 result = response.json() - assert ( - result.get("corrected_text") == corrected_text - ), f"Expected '{corrected_text}', but got '{result.get('corrected_text')}'" - assert result[ - "collection" - ], f"Expected non-empty collection for '{original_text}', but got empty." + assert result.get("corrected_text") == corrected_text, ( + f"Expected '{corrected_text}', but got '{result.get('corrected_text')}'" + ) + assert result["collection"], f"Expected non-empty collection for '{original_text}', but got empty." # Test: can correct a word appearing only in a comment check_correction("pinapples", "pineapples") @@ -688,9 +666,7 @@ def check_correction(original_text: str, corrected_text: Optional[str]) -> None: check_correction("greed", None) -def test_spelling_correction_with_mush_clause( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_spelling_correction_with_mush_clause(api_client: APIClient, patched_get_backend: Any) -> None: """ Test the spelling correction feature & mush clause in the search. Verifies the even if the text matches with the threds it should also consider other @@ -725,15 +701,11 @@ def test_spelling_correction_with_mush_clause( assert response.status_code == 200 result = response.json() corrected_text = result.get("corrected_text") - assert ( - corrected_text is None - ), f"Expected 'corrected_text' to be None, but got a value '{corrected_text}'." + assert corrected_text is None, f"Expected 'corrected_text' to be None, but got a value '{corrected_text}'." assert not result["collection"], "Expected an empty collection, but got results." -def test_total_results_and_num_pages( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_total_results_and_num_pages(api_client: APIClient, patched_get_backend: Any) -> None: """ Test the total number of results and pagination of search results. Ensures that the total count of search results and the number of pages are calculated @@ -788,20 +760,18 @@ def get_thread_ids_for_fixture(text: str) -> list[str]: return threads_ids[::100] return [] - def test_text( - text: str, expected_total_results: int, expected_num_pages: int - ) -> None: + def test_text(text: str, expected_total_results: int, expected_num_pages: int) -> None: params = {"course_id": course_id, "text": text, "per_page": "10"} get_thread_ids_value = get_thread_ids_for_fixture(text) response = get_search_response(api_client, params, get_thread_ids_value) assert response.status_code == 200 result = response.json() - assert ( - result["total_results"] == expected_total_results - ), f"Expected total_results {expected_total_results}, but got {result['total_results']}" - assert ( - result["num_pages"] == expected_num_pages - ), f"Expected num_pages {expected_num_pages}, but got {result['num_pages']}" + assert result["total_results"] == expected_total_results, ( + f"Expected total_results {expected_total_results}, but got {result['total_results']}" + ) + assert result["num_pages"] == expected_num_pages, ( + f"Expected num_pages {expected_num_pages}, but got {result['num_pages']}" + ) # Running the tests test_text("all", 100, 10) diff --git a/tests/test_views/test_subscriptions.py b/tests/test_views/test_subscriptions.py index 4ea58279..9e646e35 100644 --- a/tests/test_views/test_subscriptions.py +++ b/tests/test_views/test_subscriptions.py @@ -1,6 +1,7 @@ """Tests for subscription apis.""" from typing import Any + import pytest from test_utils.client import APIClient @@ -8,9 +9,7 @@ pytestmark = pytest.mark.django_db -def test_get_subscribed_threads( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_get_subscribed_threads(api_client: APIClient, patched_get_backend: Any) -> None: """ Test getting subscribed threads for a user. """ @@ -31,18 +30,14 @@ def test_get_subscribed_threads( } ) backend.subscribe_user(user_id, comment_thread_id, source_type="CommentThread") - response = api_client.get( - f"/api/v2/users/{user_id}/subscribed_threads?course_id={course_id}" - ) + response = api_client.get(f"/api/v2/users/{user_id}/subscribed_threads?course_id={course_id}") assert response.status_code == 200 threads = response.json()["collection"] assert len(threads) == 1 assert threads[0]["id"] == comment_thread_id -def test_get_subscribed_threads_with_filters( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_get_subscribed_threads_with_filters(api_client: APIClient, patched_get_backend: Any) -> None: """ Test getting subscribed threads for a user with filters. """ @@ -62,17 +57,13 @@ def test_get_subscribed_threads_with_filters( ) backend.subscribe_user(user_id, comment_thread_id, source_type="CommentThread") - response = api_client.get( - f"/api/v2/users/{user_id}/subscribed_threads?flagged=true&course_id={course_id}" - ) + response = api_client.get(f"/api/v2/users/{user_id}/subscribed_threads?flagged=true&course_id={course_id}") assert response.status_code == 200 threads = response.json()["collection"] assert len(threads) == 0 backend.update_thread(comment_thread_id, abuse_flaggers=[user_id]) - response = api_client.get( - f"/api/v2/users/{user_id}/subscribed_threads?flagged=true&course_id={course_id}" - ) + response = api_client.get(f"/api/v2/users/{user_id}/subscribed_threads?flagged=true&course_id={course_id}") assert response.status_code == 200 threads = response.json()["collection"] assert len(threads) == 1 @@ -105,9 +96,7 @@ def test_subscribe_thread(api_client: APIClient, patched_get_backend: Any) -> No data={"source_type": "thread", "source_id": comment_thread_id}, ) assert response.status_code == 200 - subscription = backend.subscribe_user( - user_id, comment_thread_id, source_type="CommentThread" - ) + subscription = backend.subscribe_user(user_id, comment_thread_id, source_type="CommentThread") assert subscription is not None @@ -134,22 +123,16 @@ def test_unsubscribe_thread(api_client: APIClient, patched_get_backend: Any) -> ) backend.subscribe_user(user_id, comment_thread_id, source_type="CommentThread") - response = api_client.delete( - f"/api/v2/users/{user_id}/subscriptions?source_id={comment_thread_id}" - ) + response = api_client.delete(f"/api/v2/users/{user_id}/subscriptions?source_id={comment_thread_id}") assert response.status_code == 200 assert backend.get_subscription(user_id, comment_thread_id) is None # Attempt to unsubscribe from a thread that the user is not subscribed to - response = api_client.delete( - f"/api/v2/users/{user_id}/subscriptions?source_id={comment_thread_id}" - ) + response = api_client.delete(f"/api/v2/users/{user_id}/subscriptions?source_id={comment_thread_id}") assert response.status_code == 400 -def test_get_subscribed_threads_with_pagination( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_get_subscribed_threads_with_pagination(api_client: APIClient, patched_get_backend: Any) -> None: """ Test getting subscribed threads for a user with pagination. """ @@ -196,9 +179,7 @@ def test_get_subscribed_threads_with_pagination( backend.subscribe_user(user_id, comment_thread_id_2, source_type="CommentThread") backend.subscribe_user(user_id, comment_thread_id_3, source_type="CommentThread") - response = api_client.get( - f"/api/v2/users/{user_id}/subscribed_threads?page=1&per_page=2&course_id={course_id}" - ) + response = api_client.get(f"/api/v2/users/{user_id}/subscribed_threads?page=1&per_page=2&course_id={course_id}") assert response.status_code == 200 threads = response.json()["collection"] assert len(threads) == 2 @@ -208,9 +189,7 @@ def test_get_subscribed_threads_with_pagination( comment_thread_id_3, ] - response = api_client.get( - f"/api/v2/users/{user_id}/subscribed_threads?page=2&per_page=2&course_id={course_id}" - ) + response = api_client.get(f"/api/v2/users/{user_id}/subscribed_threads?page=2&per_page=2&course_id={course_id}") assert response.status_code == 200 threads = response.json()["collection"] assert len(threads) == 1 @@ -221,9 +200,7 @@ def test_get_subscribed_threads_with_pagination( ] -def test_get_thread_subscriptions( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_get_thread_subscriptions(api_client: APIClient, patched_get_backend: Any) -> None: """ Test getting subscriptions of a thread. """ @@ -244,30 +221,22 @@ def test_get_thread_subscriptions( "author_username": author_username, } ) - subscription = backend.subscribe_user( - user_id, comment_thread_id, source_type="CommentThread" - ) + subscription = backend.subscribe_user(user_id, comment_thread_id, source_type="CommentThread") assert subscription - response = api_client.get( - f"/api/v2/threads/{comment_thread_id}/subscriptions?page=1" - ) + response = api_client.get(f"/api/v2/threads/{comment_thread_id}/subscriptions?page=1") assert response.status_code == 200 subscriptions = response.json()["collection"] assert len(subscriptions) == 1 assert subscriptions[0]["id"] == subscription["_id"] - response = api_client.get( - f"/api/v2/threads/{comment_thread_id}/subscriptions?page=2" - ) + response = api_client.get(f"/api/v2/threads/{comment_thread_id}/subscriptions?page=2") assert response.status_code == 200 subscriptions = response.json()["collection"] assert len(subscriptions) == 0 -def test_get_thread_subscriptions_with_pagination( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_get_thread_subscriptions_with_pagination(api_client: APIClient, patched_get_backend: Any) -> None: """ Test getting subscriptions of a thread with pagination. """ @@ -291,27 +260,21 @@ def test_get_thread_subscriptions_with_pagination( backend.find_or_create_user(user_id, username=f"user{user_id}") backend.subscribe_user(user_id, comment_thread_id, source_type="CommentThread") - response = api_client.get( - f"/api/v2/threads/{comment_thread_id}/subscriptions?page=1&per_page=2" - ) + response = api_client.get(f"/api/v2/threads/{comment_thread_id}/subscriptions?page=1&per_page=2") assert response.status_code == 200 subscriptions = response.json()["collection"] assert len(subscriptions) == 2 assert subscriptions[0]["subscriber_id"] == user_ids[0] assert subscriptions[1]["subscriber_id"] == user_ids[1] - response = api_client.get( - f"/api/v2/threads/{comment_thread_id}/subscriptions?page=2&per_page=2" - ) + response = api_client.get(f"/api/v2/threads/{comment_thread_id}/subscriptions?page=2&per_page=2") assert response.status_code == 200 subscriptions = response.json()["collection"] assert len(subscriptions) == 2 assert subscriptions[0]["subscriber_id"] == user_ids[2] assert subscriptions[1]["subscriber_id"] == user_ids[3] - response = api_client.get( - f"/api/v2/threads/{comment_thread_id}/subscriptions?page=3&per_page=2" - ) + response = api_client.get(f"/api/v2/threads/{comment_thread_id}/subscriptions?page=3&per_page=2") assert response.status_code == 200 subscriptions = response.json()["collection"] assert len(subscriptions) == 1 diff --git a/tests/test_views/test_threads.py b/tests/test_views/test_threads.py index dd4fe47f..3eac0b6a 100644 --- a/tests/test_views/test_threads.py +++ b/tests/test_views/test_threads.py @@ -1,7 +1,8 @@ """Test threads api endpoints.""" import time -from typing import Any, Optional +from typing import Any + import pytest from test_utils.client import APIClient @@ -11,10 +12,10 @@ def setup_models( backend: Any, - user_id: Optional[str] = None, - username: Optional[str] = None, - course_id: Optional[str] = None, - thread_type: Optional[str] = None, + user_id: str | None = None, + username: str | None = None, + course_id: str | None = None, + thread_type: str | None = None, ) -> tuple[str, str]: """ Setup models. @@ -95,9 +96,7 @@ def test_update_thread(api_client: APIClient, patched_get_backend: Any) -> None: assert updated_thread_from_db["thread_type"] == "question" -def test_update_thread_without_user_id( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_update_thread_without_user_id(api_client: APIClient, patched_get_backend: Any) -> None: """Test updaing a thread without user id.""" backend = patched_get_backend _, thread_id = setup_models(backend=backend) @@ -141,9 +140,7 @@ def test_update_close_reason(api_client: APIClient, patched_get_backend: Any) -> assert updated_thread_from_db["closed_by_id"] == user_id -def test_closing_and_reopening_thread_clears_reason_code( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_closing_and_reopening_thread_clears_reason_code(api_client: APIClient, patched_get_backend: Any) -> None: """Test close a thread and reopen a thread through update thread API.""" backend = patched_get_backend user_id, thread_id = setup_models(backend=backend) @@ -173,9 +170,7 @@ def test_closing_and_reopening_thread_clears_reason_code( assert updated_thread_from_db["closed_by_id"] is None -def test_update_thread_not_exist( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_update_thread_not_exist(api_client: APIClient, patched_get_backend: Any) -> None: """Test thread does not exists through update thread API.""" backend = patched_get_backend wrong_thread_id = backend.generate_id() @@ -227,9 +222,7 @@ def test_delete_thread(api_client: APIClient, patched_get_backend: Any) -> None: assert backend.get_subscription(subscriber_id=user_id, source_id=thread_id) is None -def test_delete_thread_not_exist( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_delete_thread_not_exist(api_client: APIClient, patched_get_backend: Any) -> None: """Test thread does not exists through delete thread API.""" backend = patched_get_backend wrong_thread_id = backend.generate_id() @@ -261,9 +254,7 @@ def test_filter_by_course(api_client: APIClient, patched_get_backend: Any) -> No assert res["course_id"] == "course1" -def test_filter_exclude_standalone( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_filter_exclude_standalone(api_client: APIClient, patched_get_backend: Any) -> None: """Test filter exclude standalone threads through get thread API.""" backend = patched_get_backend setup_models(backend=backend) @@ -291,9 +282,7 @@ def test_filter_exclude_standalone( assert res["context"] == "course" -def test_api_with_count_flagged( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_api_with_count_flagged(api_client: APIClient, patched_get_backend: Any) -> None: """Test thread API with count flagged.""" backend = patched_get_backend _, thread_id = setup_models(backend) @@ -435,18 +424,14 @@ def test_unresponded_filter(api_client: APIClient, patched_get_backend: Any) -> create_comments_in_a_thread(backend, thread_id) setup_models(backend, "2", "user2") - response = api_client.get_json( - "/api/v2/threads", params={"course_id": "course1", "unresponded": "true"} - ) + response = api_client.get_json("/api/v2/threads", params={"course_id": "course1", "unresponded": "true"}) assert response.status_code == 200 thread = response.json()["collection"] assert len(thread) == 1 -def test_get_user_threads_context( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_get_user_threads_context(api_client: APIClient, patched_get_backend: Any) -> None: """Test get_user_threads filters threads by context.""" backend = patched_get_backend user_id, course_thread_id = setup_models(backend=backend) @@ -472,9 +457,7 @@ def test_get_user_threads_context( assert standalone_thread_id not in ids # Explicit standalone context: only the standalone thread is returned - response = api_client.get_json( - "/api/v2/threads", {"course_id": "course1", "context": "standalone"} - ) + response = api_client.get_json("/api/v2/threads", {"course_id": "course1", "context": "standalone"}) assert response.status_code == 200 ids = [t["id"] for t in response.json()["collection"]] assert standalone_thread_id in ids @@ -518,9 +501,7 @@ def test_filter_by_post_type(api_client: APIClient, patched_get_backend: Any) -> assert thread["thread_type"] == "question" -def test_filter_unanswered_questions( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_filter_unanswered_questions(api_client: APIClient, patched_get_backend: Any) -> None: """Test filter unanswered questions through get thread API.""" backend = patched_get_backend course_id = "course1" @@ -616,9 +597,7 @@ def test_get_thread(api_client: APIClient, patched_get_backend: Any) -> None: assert thread["thread_type"] == "discussion" -def test_computes_endorsed_correctly( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_computes_endorsed_correctly(api_client: APIClient, patched_get_backend: Any) -> None: """Test computes endorsed correctly through get thread API.""" backend = patched_get_backend _, thread_id = setup_models(backend) @@ -650,9 +629,7 @@ def test_computes_endorsed_correctly( assert thread["endorsed"] is True -def test_no_children_for_informational_request( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_no_children_for_informational_request(api_client: APIClient, patched_get_backend: Any) -> None: """Test no children returned from get thread by thread_id API""" backend = patched_get_backend _, thread_id = setup_models(backend) @@ -783,9 +760,7 @@ def test_endorement_is_none_after_unanswering_a_comment_in_question( assert thread["children"][0]["endorsement"] is None -def test_response_for_thread_type_question( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_response_for_thread_type_question(api_client: APIClient, patched_get_backend: Any) -> None: """ Test responses for thread_type question. It varies according to queryparams. @@ -963,9 +938,7 @@ def test_filter_by_group_ids(api_client: APIClient, patched_get_backend: Any) -> assert results[2]["group_id"] is None -def test_pagination_in_thread_comments( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_pagination_in_thread_comments(api_client: APIClient, patched_get_backend: Any) -> None: """ Test pagination in the thread comments, including various cases such as exact limits, skipping, and when there are fewer comments than the limit. diff --git a/tests/test_views/test_users.py b/tests/test_views/test_users.py index 9cdfaf62..dac3d9d2 100644 --- a/tests/test_views/test_users.py +++ b/tests/test_views/test_users.py @@ -1,6 +1,7 @@ """Tests for Users apis.""" from typing import Any + import pytest from forum.constants import RETIRED_BODY, RETIRED_TITLE @@ -41,18 +42,14 @@ def test_create_user(api_client: APIClient, patched_get_backend: Any) -> None: backend = patched_get_backend user_id = backend.generate_id() username = "test-user" - response = api_client.post_json( - "/api/v2/users", data={"id": user_id, "username": username} - ) + response = api_client.post_json("/api/v2/users", data={"id": user_id, "username": username}) assert response.status_code == 200 user = backend.get_user(user_id) assert user assert user["username"] == username -def test_create_user_with_existing_id( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_create_user_with_existing_id(api_client: APIClient, patched_get_backend: Any) -> None: """Test create user with an existing id.""" backend = patched_get_backend user_id = backend.generate_id() @@ -61,15 +58,11 @@ def test_create_user_with_existing_id( user_id, username, ) - response = api_client.post_json( - "/api/v2/users", data={"id": user_id, "username": "test-user-2"} - ) + response = api_client.post_json("/api/v2/users", data={"id": user_id, "username": "test-user-2"}) assert response.status_code == 400 -def test_create_user_with_existing_username( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_create_user_with_existing_username(api_client: APIClient, patched_get_backend: Any) -> None: """Test create user with an existing username.""" backend = patched_get_backend user_id = backend.generate_id() @@ -78,9 +71,7 @@ def test_create_user_with_existing_username( user_id, username, ) - response = api_client.post_json( - "/api/v2/users", data={"id": backend.generate_id(), "username": username} - ) + response = api_client.post_json("/api/v2/users", data={"id": backend.generate_id(), "username": username}) assert response.status_code == 400 @@ -94,30 +85,22 @@ def test_update_user(api_client: APIClient, patched_get_backend: Any) -> None: user_id, username, ) - response = api_client.put_json( - f"/api/v2/users/{user_id}", data={"username": new_username} - ) + response = api_client.put_json(f"/api/v2/users/{user_id}", data={"username": new_username}) assert response.status_code == 200 user = backend.get_user(user_id) assert user assert user["username"] == new_username -def test_update_non_existent_user( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_update_non_existent_user(api_client: APIClient, patched_get_backend: Any) -> None: """Test updating non-existent user.""" backend = patched_get_backend user_id = backend.generate_id() - response = api_client.put_json( - f"/api/v2/users/{user_id}", data={"username": "new-test-user"} - ) + response = api_client.put_json(f"/api/v2/users/{user_id}", data={"username": "new-test-user"}) assert response.status_code == 200 -def test_update_user_with_conflicting_info( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_update_user_with_conflicting_info(api_client: APIClient, patched_get_backend: Any) -> None: """Test updating user with conflicting information.""" backend = patched_get_backend user_id = backend.generate_id() @@ -131,9 +114,7 @@ def test_update_user_with_conflicting_info( backend.generate_id(), conflicting_username, ) - response = api_client.put_json( - f"/api/v2/users/{user_id}", data={"username": conflicting_username} - ) + response = api_client.put_json(f"/api/v2/users/{user_id}", data={"username": conflicting_username}) assert response.status_code == 400 @@ -161,9 +142,7 @@ def test_get_non_existent_user(api_client: APIClient, patched_get_backend: Any) assert response.status_code == 404 -def test_get_user_with_no_votes( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_get_user_with_no_votes(api_client: APIClient, patched_get_backend: Any) -> None: """Test getting user with no votes.""" backend = patched_get_backend user_id = backend.generate_id() @@ -204,9 +183,7 @@ def test_get_user_with_votes(api_client: APIClient, patched_get_backend: Any) -> user = backend.get_user(user_id) assert thread assert user - backend.upvote_content( - thread["_id"], user["external_id"], content_type="CommentThread" - ) + backend.upvote_content(thread["_id"], user["external_id"], content_type="CommentThread") response = api_client.get(f"/api/v2/users/{user_id}?complete=true") assert response.status_code == 200 user = response.json() @@ -214,9 +191,7 @@ def test_get_user_with_votes(api_client: APIClient, patched_get_backend: Any) -> assert user["upvoted_ids"] == [thread_id] -def test_get_active_threads_requires_course_id( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_get_active_threads_requires_course_id(api_client: APIClient, patched_get_backend: Any) -> None: """Test getting active threads requires course id.""" backend = patched_get_backend user_id = backend.generate_id() @@ -250,9 +225,7 @@ def test_get_active_threads(api_client: APIClient, patched_get_backend: Any) -> assert len(threads) == 10 -def test_marks_thread_as_read_for_user( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_marks_thread_as_read_for_user(api_client: APIClient, patched_get_backend: Any) -> None: """Test marking a thread as read for a user.""" backend = patched_get_backend user_id = backend.generate_id() @@ -310,9 +283,7 @@ def test_replaces_username(api_client: APIClient, patched_get_backend: Any) -> N assert user["username"] == username new_username = "test_username_replacement" - response = api_client.post_json( - f"/api/v2/users/{user_id}/replace_username", data={"new_username": new_username} - ) + response = api_client.post_json(f"/api/v2/users/{user_id}/replace_username", data={"new_username": new_username}) assert response.status_code == 200 updated_user = backend.get_user(user_id) assert updated_user @@ -324,15 +295,11 @@ def test_attempts_to_replace_username_of_non_existent_user( ) -> None: """Test replace_username api.""" new_username = "test_username_replacement" - response = api_client.post_json( - "/api/v2/users/1234/replace_username", data={"new_username": new_username} - ) + response = api_client.post_json("/api/v2/users/1234/replace_username", data={"new_username": new_username}) assert response.status_code == 400 -def test_attempts_to_replace_username_and_username_on_content( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_attempts_to_replace_username_and_username_on_content(api_client: APIClient, patched_get_backend: Any) -> None: """Test replace_username api with content.""" backend = patched_get_backend user_id = backend.generate_id() @@ -345,9 +312,7 @@ def test_attempts_to_replace_username_and_username_on_content( user = backend.get_user(user_id) new_username = "test_username_replacement" - response = api_client.post_json( - f"/api/v2/users/{user_id}/replace_username", data={"new_username": new_username} - ) + response = api_client.post_json(f"/api/v2/users/{user_id}/replace_username", data={"new_username": new_username}) assert response.status_code == 200 user = backend.get_user(user_id) @@ -390,9 +355,7 @@ def test_attempts_to_retire_user_without_sending_retired_username( assert response.status_code == 500 -def test_attempts_to_retire_non_existent_user( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_attempts_to_retire_non_existent_user(api_client: APIClient, patched_get_backend: Any) -> None: """Test retire non-existent user.""" backend = patched_get_backend user_id = backend.generate_id() @@ -437,9 +400,7 @@ def test_retire_user(api_client: APIClient, patched_get_backend: Any) -> None: assert content["author_username"] == retired_username -def test_retire_user_with_subscribed_threads( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_retire_user_with_subscribed_threads(api_client: APIClient, patched_get_backend: Any) -> None: """Test retire user with subscribed threads.""" backend = patched_get_backend user_id = backend.generate_id() @@ -467,9 +428,7 @@ def test_retire_user_with_subscribed_threads( } ) backend.subscribe_user(user_id, thread_id, "CommentThread") - response = api_client.get( - f"/api/v2/users/{user_id}/subscribed_threads?course_id=course1" - ) + response = api_client.get(f"/api/v2/users/{user_id}/subscribed_threads?course_id=course1") assert response.status_code == 200 body = response.json() assert body["thread_count"] == 1 @@ -544,9 +503,7 @@ def test_get_user_post_counts(api_client: APIClient, patched_get_backend: Any) - assert data["comment_count"] == 3 -def test_get_user_post_counts_missing_course_id( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_get_user_post_counts_missing_course_id(api_client: APIClient, patched_get_backend: Any) -> None: """Test that GET /users//posts returns 400 when course_id is missing.""" backend = patched_get_backend user_id = backend.generate_id() @@ -555,9 +512,7 @@ def test_get_user_post_counts_missing_course_id( assert response.status_code == 400 -def test_get_user_post_counts_invalid_user( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_get_user_post_counts_invalid_user(api_client: APIClient, patched_get_backend: Any) -> None: """Test that GET /users//posts returns 404 for unknown user.""" backend = patched_get_backend assert backend.get_user("999999") is None @@ -594,9 +549,7 @@ def test_delete_user_posts(api_client: APIClient, patched_get_backend: Any) -> N } ) thread_ids.append(thread_id) - response = api_client.delete_json( - f"/api/v2/users/{user_id}/posts?course_id={course_id}" - ) + response = api_client.delete_json(f"/api/v2/users/{user_id}/posts?course_id={course_id}") assert response.status_code == 200 data = response.json() assert data["thread_count"] == 2 @@ -607,9 +560,7 @@ def test_delete_user_posts(api_client: APIClient, patched_get_backend: Any) -> N assert counts.json()["comment_count"] == 0 -def test_delete_user_posts_missing_course_id( - api_client: APIClient, patched_get_backend: Any -) -> None: +def test_delete_user_posts_missing_course_id(api_client: APIClient, patched_get_backend: Any) -> None: """Test that DELETE /users//posts returns 400 when course_id is missing.""" backend = patched_get_backend user_id = backend.generate_id() diff --git a/tests/test_views/test_votes.py b/tests/test_views/test_votes.py index 98253051..53e3ba71 100644 --- a/tests/test_views/test_votes.py +++ b/tests/test_views/test_votes.py @@ -61,9 +61,7 @@ def get_thread(user: dict[str, Any], patched_get_backend: Any) -> dict[str, Any] @pytest.fixture(name="comment") -def get_comment( - user: dict[str, Any], thread: dict[str, Any], patched_get_backend: Any -) -> dict[str, Any]: +def get_comment(user: dict[str, Any], thread: dict[str, Any], patched_get_backend: Any) -> dict[str, Any]: """ Fixture to create and return a test comment. diff --git a/tox.ini b/tox.ini index 5e383d25..98db1168 100644 --- a/tox.ini +++ b/tox.ini @@ -1,102 +1,72 @@ [tox] -envlist = py312-django{42,52},package +envlist = py312-django{42,52}, quality, docs, mypy +requires = tox-uv>=1 [doc8] ; D001 = Line too long ignore = D001 -[pycodestyle] -exclude = .git,.tox,migrations -max-line-length = 120 - -[pydocstyle] -; D101 = Missing docstring in public class -; D200 = One-line docstring should fit on one line with quotes -; D203 = 1 blank line required before class docstring -; D212 = Multi-line docstring summary should start at the first line -; D215 = Section underline is over-indented (numpy style) -; D404 = First word of the docstring should not be This (numpy style) -; D405 = Section name should be properly capitalized (numpy style) -; D406 = Section name should end with a newline (numpy style) -; D407 = Missing dashed underline after section (numpy style) -; D408 = Section underline should be in the line following the section's name (numpy style) -; D409 = Section underline should match the length of its name (numpy style) -; D410 = Missing blank line after section (numpy style) -; D411 = Missing blank line before section (numpy style) -; D412 = No blank lines allowed between a section header and its content (numpy style) -; D413 = Missing blank line after last section (numpy style) -; D414 = Section has no content (numpy style) -ignore = D101,D200,D203,D212,D215,D404,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414 -match-dir = (?!migrations) - -[pytest] -DJANGO_SETTINGS_MODULE = forum.settings.test -addopts = --cov forum --cov tests --cov-report term-missing --cov-report xml -norecursedirs = .* docs requirements site-packages e2e - [testenv] -deps = - setuptools - django42: Django>=4.2,<4.3 - django52: Django>=5.2,<5.3 - -r{toxinidir}/requirements/test.txt +runner = uv-venv-lock-runner +dependency_groups = + django42: django42 + django52: test commands = python manage.py check pytest {posargs} +[testenv:quality] +runner = uv-venv-lock-runner +dependency_groups = quality +commands = + ruff check . + ruff format --check . + [testenv:docs] +runner = uv-venv-lock-runner +basepython = python3.12 setenv = DJANGO_SETTINGS_MODULE = forum.settings.test PYTHONPATH = {toxinidir} - # Adding the option here instead of as a default in the docs Makefile because that Makefile is generated by shpinx. SPHINXOPTS = -W allowlist_externals = make rm -deps = - setuptools - wheel - -r{toxinidir}/requirements/test.txt - -r{toxinidir}/requirements/doc.txt +dependency_groups = doc commands = doc8 --ignore-path docs/_build README.rst docs rm -f docs/forum.rst rm -f docs/modules.rst make -e -C docs clean make -e -C docs html - python -m build --wheel - twine check dist/* -[testenv:quality] -allowlist_externals = - make - rm - touch -deps = - -r{toxinidir}/requirements/quality.txt +[testenv:mypy] +runner = uv-venv-lock-runner +dependency_groups = quality commands = - make test-quality selfcheck + mypy forum [testenv:pii_check] -deps = - -r{toxinidir}/requirements/test.txt +runner = uv-venv-lock-runner +dependency_groups = test +setenv = + DJANGO_SETTINGS_MODULE = forum.settings.test commands = - make test-pii + code_annotations django_find_annotations --config_file .pii_annotations.yml --lint --report --coverage [testenv:e2e] +runner = uv-venv-lock-runner +dependency_groups = test allowlist_externals = make - rm - touch -deps = - -r{toxinidir}/requirements/test.txt commands = make test-e2e [testenv:package] -deps = - build - twine +runner = uv-venv-lock-runner +dependency_groups = test-base +allowlist_externals = + bash commands = python -m build twine check dist/* diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..77a2697d --- /dev/null +++ b/uv.lock @@ -0,0 +1,2918 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.15' and extra != 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test'", + "python_full_version < '3.15' and extra != 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test'", + "extra != 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42' and extra != 'group-13-openedx-forum-doc' and extra != 'group-13-openedx-forum-quality' and extra != 'group-13-openedx-forum-test'", + "python_full_version >= '3.15' and extra != 'group-13-openedx-forum-django42' and extra != 'group-13-openedx-forum-test'", + "python_full_version < '3.15' and extra != 'group-13-openedx-forum-django42' and extra != 'group-13-openedx-forum-test'", +] +conflicts = [[ + { package = "openedx-forum", group = "django42" }, + { package = "openedx-forum", group = "test" }, +], [ + { package = "openedx-forum", group = "django42" }, + { package = "openedx-forum", group = "quality" }, +], [ + { package = "openedx-forum", group = "dev" }, + { package = "openedx-forum", group = "django42" }, +], [ + { package = "openedx-forum", group = "django42" }, + { package = "openedx-forum", group = "doc" }, +]] + +[manifest] +constraints = [ + { name = "django", specifier = "<6.0" }, + { name = "elasticsearch", specifier = "<7.14.0" }, +] + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + +[[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 = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, +] + +[[package]] +name = "asgiref" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, +] + +[[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 = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + +[[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 = "build" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, +] + +[[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 = "camel-converter" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/cb/55feabf13527c464db60145e2e0dccd2f190b0a4a417cac2473bd2b18508/camel_converter-5.1.0.tar.gz", hash = "sha256:d979f23de232aa262dcf15a263ed9af934ff4378644dae23a7c2f8d40f4fff8f", size = 71481, upload-time = "2026-03-05T15:30:58.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/c3/8fc18a914da9747b7a652180758a46a3d5f22efd0dffd5a8b97acfb1ebf3/camel_converter-5.1.0-py3-none-any.whl", hash = "sha256:93f05d7d84a4353ddfd87bca909d1de45c11f5757f6a460de10534397146fe88", size = 6960, upload-time = "2026-03-05T15:31:00.135Z" }, +] + +[package.optional-dependencies] +pydantic = [ + { name = "pydantic" }, +] + +[[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]] +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' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +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 = "chardet" +version = "7.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/b6/9df434a8eeba2e6628c465a1dfa31034228ef79b26f76f46278f4ef7e49d/chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", size = 784800, upload-time = "2026-04-13T21:33:39.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/33/29de185079e6675c3f375546e30a559b7ddc75ce972f18d6e566cd9ea4eb/chardet-7.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", size = 874870, upload-time = "2026-04-13T21:33:05.977Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", size = 854859, upload-time = "2026-04-13T21:33:07.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/21/edb36ad5dfa48d7f8eed97ab43931ecdaa8c15166c21b1d614967e49d681/chardet-7.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", size = 875032, upload-time = "2026-04-13T21:33:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/e5/59/a32a241d861cf180853a11c8e5a67641cb1b2af13c3a5ccce83ec07e2c9f/chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", size = 888283, upload-time = "2026-04-13T21:33:10.213Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/e1ee6a77abf3782c00e05b89c4d4328c8353bf9500661c4348df1dd68614/chardet-7.4.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", size = 879974, upload-time = "2026-04-13T21:33:11.448Z" }, + { url = "https://files.pythonhosted.org/packages/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", size = 943973, upload-time = "2026-04-13T21:33:12.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", size = 873769, upload-time = "2026-04-13T21:33:14.002Z" }, + { url = "https://files.pythonhosted.org/packages/55/5f/25bdec773905bff0ff6cf35ca73b17bd05593b4f87bd8c5fa43705f7167d/chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", size = 853991, upload-time = "2026-04-13T21:33:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/a29380ee0b215d23d77733b5ad60c5c0c7969650e080c667acdf9462040d/chardet-7.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", size = 874024, upload-time = "2026-04-13T21:33:16.915Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", size = 887410, upload-time = "2026-04-13T21:33:18.368Z" }, + { url = "https://files.pythonhosted.org/packages/63/1c/44a9a9e0c59c185a5d307ceaeee8768afa1558f0a24f7a4b5fa11b67586b/chardet-7.4.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", size = 879269, upload-time = "2026-04-13T21:33:20.377Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b3/5d0e77ea774bd3224321c248880ea0c0379000ac5c2bb6d77609549de247/chardet-7.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131", size = 944155, upload-time = "2026-04-13T21:33:21.694Z" }, + { url = "https://files.pythonhosted.org/packages/70/a8/bf0811d859e13801279a2ae64f37a408027b282f2047bc0001c75dd356ad/chardet-7.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", size = 872887, upload-time = "2026-04-13T21:33:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", size = 853964, upload-time = "2026-04-13T21:33:24.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/17fa103ea9caf5d325a5e4051ab2ba65996fd66baa60b81ee41af1f54e10/chardet-7.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", size = 876006, upload-time = "2026-04-13T21:33:26.098Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", size = 887680, upload-time = "2026-04-13T21:33:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/94a3c673327392652ee8bdea9a45bc8a5f5365197a7387d68f0eed007115/chardet-7.4.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", size = 879865, upload-time = "2026-04-13T21:33:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", size = 939594, upload-time = "2026-04-13T21:33:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/33/e0/d06e42fd6f02a58e5e227e5106587751cb38adcff0aaf949add744b78b6e/chardet-7.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", size = 889714, upload-time = "2026-04-13T21:33:32.772Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ed/40d091954d48abea037baae6be8fb79905e5f78d34d12ea955132c7d8011/chardet-7.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", size = 872319, upload-time = "2026-04-13T21:33:34.427Z" }, + { url = "https://files.pythonhosted.org/packages/bb/77/82a46821dbfbdfe062710d2bf2ede13426304e3567a23c57d919c0c31630/chardet-7.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", size = 892021, upload-time = "2026-04-13T21:33:35.766Z" }, + { url = "https://files.pythonhosted.org/packages/49/57/42d30c562bda5b4a839766c1aad8d5856b798ad2a1c3247b72a679afec94/chardet-7.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", size = 902509, upload-time = "2026-04-13T21:33:37.096Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/0a40afdb50a0fe041ab95553b835a8160b6cf0e81edf2ae2fe9f5224cbf9/chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", size = 626562, upload-time = "2026-04-13T21:33:38.559Z" }, +] + +[[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' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +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' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +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 = "diff-cover" +version = "10.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet" }, + { name = "jinja2" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/21/057e816125c162662d2a2cc2ebcd72dd333e78e51678298d07dd3146011a/diff_cover-10.3.0.tar.gz", hash = "sha256:474dbc63e815fbb7567d7b7ca5b104123e96129f25426ebdbc9a1bdbb935b2c6", size = 106546, upload-time = "2026-05-30T14:17:14.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0a/a96e57a7a3fca419cd5ceff0d13dee2166520fa67103fb82624ad64700fb/diff_cover-10.3.0-py3-none-any.whl", hash = "sha256:2e47d5ab3868d1e92131c11f364f3f4a8583c97123d3bbc6b6cc8ce0a4cc2202", size = 58989, upload-time = "2026-05-30T14:17:12.858Z" }, +] + +[[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 = "django" +version = "4.2.30" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref", marker = "extra == 'group-13-openedx-forum-django42'" }, + { name = "sqlparse", marker = "extra == 'group-13-openedx-forum-django42'" }, + { name = "tzdata", marker = "(sys_platform == 'win32' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/b5/f1a53dc68da6429d6e0345bb848161e2381a2e9f02700148911e8582c2b3/django-4.2.30.tar.gz", hash = "sha256:4ebc7a434e3819db6cf4b399fb5b3f536310a30e8486f08b66886840be84b37c", size = 10468707, upload-time = "2026-04-07T14:05:45.57Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/b7/a7c96f239cf91313a6589233fed55111c7063b26683b226802732c455dbc/django-4.2.30-py3-none-any.whl", hash = "sha256:4d07aaf1c62f9984842b67c2874ebbf7056a17be253860299b93ae1881faad65", size = 7997231, upload-time = "2026-04-07T14:05:38.241Z" }, +] + +[[package]] +name = "django" +version = "5.2.15" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] +dependencies = [ + { name = "asgiref", marker = "extra == 'group-13-openedx-forum-dev' or extra != 'group-13-openedx-forum-django42' or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "sqlparse", marker = "extra == 'group-13-openedx-forum-dev' or extra != 'group-13-openedx-forum-django42' or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "tzdata", marker = "(sys_platform == 'win32' and extra == 'group-13-openedx-forum-dev') or (sys_platform == 'win32' and extra != 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/e3/31722f7284c9f43333daff9aee9184678e4487adcb5506af0db8cea09ce1/django-5.2.15.tar.gz", hash = "sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7", size = 10873669, upload-time = "2026-06-03T13:03:35.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/b5/38140b1643c00d5c46ce69c78e6980fd285aee223100319631bedee4f5e7/django-5.2.15-py3-none-any.whl", hash = "sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970", size = 8311957, upload-time = "2026-06-03T13:03:31.329Z" }, +] + +[[package]] +name = "django-crum" +version = "0.7.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "4.2.30", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-django42'" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-dev' or extra != 'group-13-openedx-forum-django42' or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/1d/c56588f67130aeef8828e47535e8551337d2ae02f91f1414da61bc5e49fb/django-crum-0.7.9.tar.gz", hash = "sha256:65e9bc0f070a663fafc4d9e357f45fd4e6f01838b20a9e2fb7670f5706754288", size = 5168, upload-time = "2020-11-10T17:15:35.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/f9/8fad70a3bd011a6be7c5c6067278f006a25341eb39d901fbda307e26804c/django_crum-0.7.9-py2.py3-none-any.whl", hash = "sha256:037cc8b822975bb1d41cd24269b59a512cc77448fadc3f34ab9a17b229b4b471", size = 4714, upload-time = "2020-11-10T17:15:33.781Z" }, +] + +[[package]] +name = "django-stubs" +version = "6.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" } }, + { name = "django-stubs-ext" }, + { name = "types-pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/de/1b8ccb0909970fb4a8b48426f67132164110304875abdb4e4912c55480f4/django_stubs-6.0.6.tar.gz", hash = "sha256:dfc01e052e33c7f8f0c30c3ff8eda0903ee29ac710d1e46d9effd773744a69b0", size = 281381, upload-time = "2026-06-23T08:22:54.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/b0/b73855fb9cf381fa1d754ea33c25f14f5b11c297f48dc3a4e0359dc3adc2/django_stubs-6.0.6-py3-none-any.whl", hash = "sha256:c488fea05a9eac40ddbdc69887f63a5c0922cb13df285291ee99c9bbc89bc4f1", size = 546491, upload-time = "2026-06-23T08:22:52.466Z" }, +] + +[[package]] +name = "django-stubs-ext" +version = "6.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" } }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/8b/dc3c37cf994836ee09bc07f6a0c0ea840b975449940bd7ff77cc97a732f3/django_stubs_ext-6.0.6.tar.gz", hash = "sha256:e6f09884e48d7c5b250a373dfa22aa3e83bb91b8babbd8d05187f6b92247f232", size = 6674, upload-time = "2026-06-23T08:21:38.355Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/e4/7d60a1bdb092807318af34a809dc6674b95cc78ec4d3aaa6027174e7201c/django_stubs_ext-6.0.6-py3-none-any.whl", hash = "sha256:5470c970f61a3ccf5aae7633feef1a097f944f417b955da200c9ac26ecd9134b", size = 10361, upload-time = "2026-06-23T08:21:37.228Z" }, +] + +[[package]] +name = "django-waffle" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "4.2.30", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-django42'" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-dev' or extra != 'group-13-openedx-forum-django42' or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/e1/6f533da0d4ac89f427dfd9410e39bfc14ae3a23335ecd549d76be4b2a834/django_waffle-5.0.0.tar.gz", hash = "sha256:62f9d00eedf68dafb82657beab56e601bddedc1ea1ccfef91d83df8658708509", size = 37761, upload-time = "2025-06-12T07:38:54.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/d2/6f0d664bd35a3fdd0403655c7c32ec290704923f11541ef356b180cd8fbf/django_waffle-5.0.0-py3-none-any.whl", hash = "sha256:3312851d9d926b76b9e90712355781700a383b82b5bf2b61e1f1be97532c0f3d", size = 48137, upload-time = "2025-06-12T07:38:53.698Z" }, +] + +[[package]] +name = "djangorestframework" +version = "3.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "4.2.30", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-django42'" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-dev' or extra != 'group-13-openedx-forum-django42' or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" }, +] + +[[package]] +name = "djangorestframework-stubs" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django-stubs" }, + { name = "types-pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/a1/45e25dab89b3dfc5da7a138f7e82aca4e970269c53a3fceed1e94b31d390/djangorestframework_stubs-3.17.0.tar.gz", hash = "sha256:74c0307cad304c03c15aee5955ec2fdeab0ac854b5bea569dc124892792e2b96", size = 33115, upload-time = "2026-05-13T18:58:36.554Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/ff/6e27d4aea12f67d14e0c0ec41869307ae98ad3816858ddc2a56bc3f4e0b5/djangorestframework_stubs-3.17.0-py3-none-any.whl", hash = "sha256:babe2703f0401507780848439f49f76222a178b4fc73a6dcb30d0952a0a6dbc6", size = 57629, upload-time = "2026-05-13T18:58:34.779Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "doc8" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pygments" }, + { name = "restructuredtext-lint" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/91/88bb55225046a2ee9c2243d47346c78d2ed861c769168f451568625ad670/doc8-2.0.0.tar.gz", hash = "sha256:1267ad32758971fbcf991442417a3935c7bc9e52550e73622e0e56ba55ea1d40", size = 28436, upload-time = "2025-06-13T13:08:53.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/e9/90b7d243364d3dce38c8c2a1b8c103d7a8d1383c2b24c735fae0eee038dd/doc8-2.0.0-py3-none-any.whl", hash = "sha256:9862710027f793c25f9b1899150660e4bf1d4c9a6738742e71f32011e2e3f590", size = 25861, upload-time = "2025-06-13T13:08:51.839Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "edx-ccx-keys" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "edx-opaque-keys" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/ca/118458ec6a14e5040a4feedb496fee99eb261e8c9b81112e16dabb77efcf/edx_ccx_keys-2.0.2.tar.gz", hash = "sha256:da17b2b3281011a540d5ab847acf7946c4a57dd10c0931cec3ff9ac3c1fa33a0", size = 19528, upload-time = "2024-12-02T13:23:33.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/f8/870ec0597d5f8bd3235130ae7d888650531618c552932c89f57e5251f410/edx_ccx_keys-2.0.2-py3-none-any.whl", hash = "sha256:4071fc0f3a76288c2f7d784a8b53aa8c5e672604a1d8c5a1891fa1bf5612791a", size = 16226, upload-time = "2024-12-02T13:23:32.276Z" }, +] + +[[package]] +name = "edx-django-utils" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "django", version = "4.2.30", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-django42'" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-dev' or extra != 'group-13-openedx-forum-django42' or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "django-crum" }, + { name = "django-waffle" }, + { name = "psutil" }, + { name = "pynacl" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/68/96a58ff4e9d0173d2ad8cc2b2f446ae00c462cd84d8efd39f085a4589801/edx_django_utils-8.0.1.tar.gz", hash = "sha256:fadf4954b12556a79fadd9cc6bc8efd45da4a90c043a63c915339a3cc90d610b", size = 109365, upload-time = "2025-09-29T18:17:06.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/ae/a8e618fee2be5f1a8ae3e2d1b6ce0b405fbca2726dd11557e1b982cf58ae/edx_django_utils-8.0.1-py2.py3-none-any.whl", hash = "sha256:849c253b5e14ddfbdc47c5e36143fad54147778a53e0b49095c1fecab24edd9e", size = 121130, upload-time = "2025-09-29T18:17:04.628Z" }, +] + +[[package]] +name = "edx-i18n-tools" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" } }, + { name = "lxml", extra = ["html-clean"], marker = "extra == 'group-13-openedx-forum-dev' or extra != 'group-13-openedx-forum-django42' or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "path" }, + { name = "polib" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/7a/800a34409b305a74f8848ae61cca6398a9d3b4f905aaf1c5754378596954/edx_i18n_tools-2.0.0.tar.gz", hash = "sha256:20b4c2b978ccb0c5164570375d082b47d59eae7f1f7513a0fe5bf04b0e7d6dc2", size = 38552, upload-time = "2026-03-10T15:29:03.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/5e/d3f836a41a10a9a76a290277876c2326eafa77f3073a15bbbff25cdb3c1d/edx_i18n_tools-2.0.0-py2.py3-none-any.whl", hash = "sha256:d3f6d9ee36ca5af367653eabdfc71dd967fd4d5dcceeb855d23a058ce391eb35", size = 32872, upload-time = "2026-03-10T15:29:02.763Z" }, +] + +[[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 = "edx-opaque-keys" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymongo" }, + { name = "stevedore" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/12/eeb5e5431f7e600875e620bd2592f77f04ee509f474f52cc6b0ab1f4312b/edx_opaque_keys-4.0.0.tar.gz", hash = "sha256:d9c06793cf99629836432f2a9f34b0bb24ad7a66a1e397ffa716f714aa43b269", size = 68222, upload-time = "2026-04-02T19:00:56.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/1b/e9edebd12fd461b605691759a01c12e73a78cb50a4cd0b04ab58bb8303d9/edx_opaque_keys-4.0.0-py3-none-any.whl", hash = "sha256:dab0fd985b13d6d6515049606f93684d1eb284207b6d5e8da0721e29efdd0088", size = 77651, upload-time = "2026-04-02T19:00:55.259Z" }, +] + +[[package]] +name = "edx-search" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "4.2.30", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-django42'" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-dev' or extra != 'group-13-openedx-forum-django42' or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "edx-toggles" }, + { name = "elasticsearch" }, + { name = "event-tracking" }, + { name = "meilisearch" }, + { name = "typesense" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/ae/d974b681924415d7eb9a34927654da78bf07d06510fe6f8b648c6c119c4d/edx_search-5.0.2.tar.gz", hash = "sha256:cd89d7ed047b88acf574aa7d26fab55609e2e5d8768793303deb7ca683872e10", size = 84509, upload-time = "2026-06-26T17:41:54.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/28/7e89df1819c111ec67eadc6e3098023973150e26fb792d584c213c7635fb/edx_search-5.0.2-py3-none-any.whl", hash = "sha256:a1e1b64982be7eb2c7c30a33322f04315219fde828d93214a0c5300ecacb1032", size = 97597, upload-time = "2026-06-26T17:41:52.802Z" }, +] + +[[package]] +name = "edx-toggles" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "code-annotations" }, + { name = "django", version = "4.2.30", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-django42'" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-dev' or extra != 'group-13-openedx-forum-django42' or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "django-crum" }, + { name = "django-waffle" }, + { name = "edx-django-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/21/91f555509cc44c9a146ab0178c8b3aed92d7296551be80dff91f73e73891/edx_toggles-6.0.0.tar.gz", hash = "sha256:a2b20324fc85bdc91f7b3c1418f2162171e054e47fe6627dec413c6c43a9b46f", size = 37181, upload-time = "2026-04-07T16:37:10.207Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/08/5997a64defd6e699ecd8a4b5d0678afc360426a3486ca805db78d898e324/edx_toggles-6.0.0-py2.py3-none-any.whl", hash = "sha256:a1a40ff55c8d655b1150fe121ede7354eb01ce84408d2cb7fd32cfc7731de9f3", size = 33247, upload-time = "2026-04-07T16:37:09.036Z" }, +] + +[[package]] +name = "elasticsearch" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/50/220b2d903eccaf6065b4d2d546c06d216e3c8eabcb8d9cd56af691cb712b/elasticsearch-7.13.4.tar.gz", hash = "sha256:52dda85f76eeb85ec873bf9ffe0ba6849e544e591f66d4048a5e48016de268e0", size = 218961, upload-time = "2021-07-20T17:09:12.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/82/c3ce164af6f1e4f82e747c50e684b3fd74c418b194c2062e0e30ade933f0/elasticsearch-7.13.4-py2.py3-none-any.whl", hash = "sha256:5920df0ab2630778680376d86bea349dc99860977eec9b6d2bd0860f337313f2", size = 356300, upload-time = "2021-07-20T17:09:09.711Z" }, +] + +[[package]] +name = "event-tracking" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "celery" }, + { name = "django", version = "4.2.30", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-django42'" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-dev' or extra != 'group-13-openedx-forum-django42' or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "edx-django-utils" }, + { name = "edx-toggles" }, + { name = "openedx-events" }, + { name = "pymongo" }, + { name = "pytz" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/88/b508cdd939c0a7b8a23c28f223714f9905d21ab58ee47c29f8ce065acfcf/event_tracking-4.0.2.tar.gz", hash = "sha256:42112080eb95721820c5388a71202f6781358f677826f26044792aecc0c04c90", size = 44093, upload-time = "2026-05-28T20:26:15.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/05/660d5bc89b4f588120b55f72aab5db500207cc88b75cc9410e209f5413ef/event_tracking-4.0.2-py3-none-any.whl", hash = "sha256:2faa3fc904f693efe47bc04da41b498e067934378efbc54d7daf8256826dd188", size = 54765, upload-time = "2026-05-28T20:26:14.864Z" }, +] + +[[package]] +name = "faker" +version = "40.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/2d/3ead106cef42eab305e15f9c089dcc68a40387d5ce04af18585af4ebbb44/faker-40.28.1.tar.gz", hash = "sha256:2a63fb51abab8790636d4030a094cf942404cbccc9c288d1cad70e2e3e9ecd58", size = 2022717, upload-time = "2026-07-01T22:23:43.763Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/a6/6111b9f13c1564b0e2f5dbeeb611fd00dc731e6add2336f62b798598c73d/faker-40.28.1-py3-none-any.whl", hash = "sha256:e8d3f5c469100a553d246dce7937c291308068a5ec6c9c3a228d7878b50720be", size = 2061052, upload-time = "2026-07-01T22:23:41.946Z" }, +] + +[[package]] +name = "fastavro" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/5b/ccb338db71f347e3bc031d268bf6dc41e5ead63b6997b8e72af92f05e18e/fastavro-1.12.2.tar.gz", hash = "sha256:3c79502d56cf6b76210032e1c53494ddfbc73c140bccf2ef4092b3f0825323ab", size = 1030127, upload-time = "2026-04-24T14:36:01.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/bc/fe5731d6724d978694fbd3196bc1c0d7cab3fd0766e9551c40c39f798b52/fastavro-1.12.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e331896e8efffc72fa03e63b87ebfc37960113127da8e0f5152d91664ffed68", size = 964331, upload-time = "2026-04-24T14:36:31.297Z" }, + { url = "https://files.pythonhosted.org/packages/98/36/50abf1145e4f1c4f418cd4b5f2ac806643d0b14e360b60e953826edf1b34/fastavro-1.12.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f01ebaada59d74fdf6d28e5031a961a413b3752e9edb0c03866fa18480cf4c8", size = 3340170, upload-time = "2026-04-24T14:36:33.364Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8c/76ef4641e6c1c1aa3e6bb3c9efb5533ffda5dd975c8b5ae54e794322d9e3/fastavro-1.12.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25ef6855935f67582740ffa6bb978e40ec51be876117a3555c36fa2488dcdf25", size = 3425061, upload-time = "2026-04-24T14:36:35.497Z" }, + { url = "https://files.pythonhosted.org/packages/31/10/379ff23425b2b470d5209cbc6736a6e5cbc34392ff17bb7355b8fd4aa0ca/fastavro-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84a4f76a0aece0aa72b5ed8162ba2ff8c78908b8361b5a5d92ddd161977ccb74", size = 3243618, upload-time = "2026-04-24T14:36:37.969Z" }, + { url = "https://files.pythonhosted.org/packages/88/29/4c8f9e7cd78f932f0d82823899e67a6d7f7e8f2524992db03956f9d9f5ef/fastavro-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e8da77d201916f6771fc357fda8267c2a256d7aa11923d43bc5f2fc155878b", size = 3378427, upload-time = "2026-04-24T14:36:40.278Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/eafeb302aaaea6055d4a9c11272b4aeaf713e43fe8eaf782f43a1fee2b44/fastavro-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:1924349c74666c89417bd5cc2749f598e2f15f1d56ee81428b2317ab02c88aae", size = 441077, upload-time = "2026-04-24T14:36:41.791Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/67e831041ba8efc16265c65bd71ba92e1095bba19b91be99e102f19d9be6/fastavro-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:4c346cf449baf3b113e997c34151ad205e7135bc429469b005b180ade7e65e28", size = 378205, upload-time = "2026-04-24T14:36:43.679Z" }, + { url = "https://files.pythonhosted.org/packages/83/39/f489a441d41cc9c0a8449fb1325d7a9c9eb57a5634e6ab19dfb0a1105324/fastavro-1.12.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:57bb6b908cb2e05baab63b04c3a31be3b4545a10bfab9748b8763016b5256704", size = 958566, upload-time = "2026-04-24T14:36:45.49Z" }, + { url = "https://files.pythonhosted.org/packages/31/69/776cc025aee2d02acacb734cf690d2fbc295eaadde1b5d47caf8c77a6a2b/fastavro-1.12.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a007f95cc682f56e6d83f1d17c29c00bf719d6fe8e003282b535af3a1ba09c0", size = 3276390, upload-time = "2026-04-24T14:36:47.875Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bc/b7e15fa788f42cbe65827af2ec06c9ad91bb9f72c213110dbef61b53a5b0/fastavro-1.12.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e90460b0cd21f62be3cb26087e706e2cebb7b3fcef9e05b4473b61bb0415b5e", size = 3372779, upload-time = "2026-04-24T14:36:50.122Z" }, + { url = "https://files.pythonhosted.org/packages/79/c2/98993ca810231fc1397212f48c3d46626983722a24bbaaa5c27ee0963751/fastavro-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ccd15966b8218d41b06ec3e7c2556be89a8a693026c771e6564d2e40bbaf8ea", size = 3187591, upload-time = "2026-04-24T14:36:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/c180f340eba6478f1b20deccdd17e2b4a4d5074dafd812e3c4254fd035f7/fastavro-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06b6971d3dae10cb34353b857d16ad21ebd6f0ea394e86c96abdcad109005d6e", size = 3320589, upload-time = "2026-04-24T14:36:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e9/aca0456216b5b8992e7b0a8542711b66799c05bfe24c8e32ef6f56e7eb93/fastavro-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:98dfcdfaf1498ae2f0e2fafe900a82e8320cc81d8ae5a95b8b8879eaa3298c39", size = 440883, upload-time = "2026-04-24T14:36:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7e/984896e716af504927be71b80a1e9661aa96c6f9e1e777d52823aacb99f2/fastavro-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:3888ef7a51adc77cdf07251bc762566a1be36211e1cff689f13980f3776a2f36", size = 377536, upload-time = "2026-04-24T14:36:58.274Z" }, + { url = "https://files.pythonhosted.org/packages/e9/42/09a1e1f8d9998d73848a6ff0aad6713ae6abf0dbf99918776f8ef33344a7/fastavro-1.12.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:283dcd3129b632021894425974bedd0eb6db3bbf5994e448ccad10db4d803d31", size = 1049506, upload-time = "2026-04-24T14:36:59.797Z" }, + { url = "https://files.pythonhosted.org/packages/52/ef/80cc16f43919d532f25a707f34b275cccc09dca87a05b000fbbfc8e8f255/fastavro-1.12.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d125e210d5a0a1f701f12c0ecad9a03f1b04b5eddbce6ca36a1fc217da977ef", size = 3495899, upload-time = "2026-04-24T14:37:02.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/54/a0817d1d0236e9e0233f5c996f450cc795b056b8e06edb531f24b9df82ed/fastavro-1.12.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2d4d66afad78e8f47feaa307728a6b71fe3effc63ba2b9eeb109ee687c9bd397", size = 3399232, upload-time = "2026-04-24T14:37:04.837Z" }, + { url = "https://files.pythonhosted.org/packages/38/0a/650f256c15f5875b6081544b9ba7ed8254329213e7e49e3db0aec68b5bee/fastavro-1.12.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2328ec07925c04c89719e3971c9068a165c7fd474ea87675b1204de0440e71ff", size = 3320222, upload-time = "2026-04-24T14:37:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/f5/54/8351d388f94fbb0870e8cffaae41d3cc607acc8d6a8a6a217e2794829593/fastavro-1.12.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:55dea7e74b834d4b70467fc19c5b9ccb5509fe39abc4d26891187c1b22176423", size = 3337096, upload-time = "2026-04-24T14:37:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/da/eb/b36ba9a88826e8c272df02e2f8b5da717e88b6eb508fddca3ca450043731/fastavro-1.12.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8d37c87826ae7195cfbd20fcd448801f2f563bb38f2691ec6574e39cb9eca6c8", size = 963119, upload-time = "2026-04-24T14:37:11.557Z" }, + { url = "https://files.pythonhosted.org/packages/e1/02/3d7f540fb26ba4ea1f4ebd2783c586614da9ac00906a3092e92fd3f104a2/fastavro-1.12.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c463a3701f293e30d3d62e71e1989f112028d07f87432baf4507eeb57ec3831", size = 3266238, upload-time = "2026-04-24T14:37:13.84Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0b/b77be56c5109da0fc7dcfd7e6b6752fe0a61d0a5c58c6a65e38b4501946a/fastavro-1.12.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f604ba83498e209fff4c7ecc5063a39421dc538dace694bc592f9f338254f3dc", size = 3324020, upload-time = "2026-04-24T14:37:16.096Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6e/951d41f244107e91bf2f59245b71783c03eaab4bdbc960d58316c19652bb/fastavro-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bfac2dada8ddc002e8b7d8289d6fad4f070bc1fec20371cec684a7d10d932e96", size = 3170160, upload-time = "2026-04-24T14:37:18.168Z" }, + { url = "https://files.pythonhosted.org/packages/94/6f/2adb571fda448d4afd2466e1cef2963fefdc6b37847da05249983e415f17/fastavro-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bc44ba6289fb1f5ee318335958dde6ad6d742dcb4bb8930de843e9024c64b68c", size = 3281842, upload-time = "2026-04-24T14:37:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/17/07/4bad2e96c4c6bae40253be2573cc09c1e5b9ccf821e1ff74e0d33b64bf90/fastavro-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:a475418f71c5aed69899813ecccf392429c08c3a63df3030129db71760b0db8f", size = 450903, upload-time = "2026-04-24T14:37:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b7/180f67ba9a46ba23a1ff6432f48d3087d4f2048579ecc262b00426cb1c63/fastavro-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:daec9f9655a1d4636613c47d6d3343f6e039150d66cdce62543e20ca36612a8a", size = 391076, upload-time = "2026-04-24T14:37:24.756Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8f/18f60329b627d2118a4a2b19e8741fbd807d60bf0470554e1bbfb7f1bca3/fastavro-1.12.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:57594b72cf663bbd0f3ad8a319a999fc3d7c71065a6799b2c1d1a6a137894c5b", size = 1055430, upload-time = "2026-05-09T21:53:14.364Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/a1fa1fc29df0efc89d4946a743b09bdc9500591b5b92083eaf8e93664916/fastavro-1.12.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74412132bbfb153cbf704517f2c89f7d3e170feb681b13bceace690f66f8d5fa", size = 3503075, upload-time = "2026-04-24T14:37:26.826Z" }, + { url = "https://files.pythonhosted.org/packages/82/bf/4f669e10b6bc38a731ee3400aed1a1e2d0a3e3cf411e72f6b320d3af0eaf/fastavro-1.12.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e367a84c9133018e0a3bc822abe78d7f1f9a6092991a0ec409468cf4ef260282", size = 3410900, upload-time = "2026-04-24T14:37:29.233Z" }, + { url = "https://files.pythonhosted.org/packages/10/39/ecb19fdae4158a7730b5963fbf1b6d38d74678392d73083be518642af0c1/fastavro-1.12.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:044fafca0853e9ae14009de7763ac9e8e8f8b96f8a4e90bd58b695443266a370", size = 3335637, upload-time = "2026-04-24T14:37:31.472Z" }, + { url = "https://files.pythonhosted.org/packages/32/f1/f21bd5319113e89ceceed2df840df21e9c5150d181db74b6ba80400f9f48/fastavro-1.12.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afede7324822800e4f90e96b9514188a237a60f35e8e7a10b2129c10c78f6e4d", size = 3356664, upload-time = "2026-04-24T14:37:34.231Z" }, +] + +[[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 = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "id" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237, upload-time = "2024-12-04T19:53:05.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" }, +] + +[[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 = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/cf/ea4ef2920830dea3f5ab2ea4da6fb67724e6dca80ee2553788c3607243d0/jaraco_functools-4.5.0.tar.gz", hash = "sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03", size = 20272, upload-time = "2026-05-15T21:34:10.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl", hash = "sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4", size = 10594, upload-time = "2026-05-15T21:34:08.595Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[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 = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "secretstorage", marker = "sys_platform == 'linux' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[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]] +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 = "lxml" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" }, + { url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" }, + { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" }, + { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" }, + { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" }, + { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" }, + { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" }, + { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" }, + { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" }, + { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" }, + { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" }, + { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" }, + { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" }, + { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" }, + { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" }, + { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" }, + { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" }, + { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" }, + { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" }, + { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" }, + { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" }, + { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" }, + { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" }, + { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" }, + { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" }, + { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" }, +] + +[package.optional-dependencies] +html-clean = [ + { name = "lxml-html-clean" }, +] + +[[package]] +name = "lxml-html-clean" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/63/195dfdde380a84df309e3bccf4384b034b745dba43426886f7ae623b4fba/lxml_html_clean-0.4.5.tar.gz", hash = "sha256:e2a4c7d5beedd17cd7b484d848a0571e54baa239a4f9df5546e3acba7f990560", size = 24142, upload-time = "2026-05-20T12:17:53.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/bd/6e2b76a6c5dee10397db9c929f0c5066766ec1036046f0335b7ca7ca08b8/lxml_html_clean-0.4.5-py3-none-any.whl", hash = "sha256:c76fcadd1e5bfb9b8bafc2200d51e4e78eb0dad67f56881c21dfb6484c7e7746", size = 14573, upload-time = "2026-05-20T12:17:52.215Z" }, +] + +[[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 = "meilisearch" +version = "0.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "camel-converter", extra = ["pydantic"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/04/5b16f2c7448c45bd5579ba4b69d2704d0fd1f48d659ba2496ae0897e2f91/meilisearch-0.42.0.tar.gz", hash = "sha256:b17bb2cf97ec9bd79bfdc76ebed4963a38ff9f3f958dc1dc43b3fbd278d3ae98", size = 32380, upload-time = "2026-06-30T04:19:35.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/db/484c564b92b6e937f5b420bc7f39b19051ad7c4007901f2320c56ea7c29f/meilisearch-0.42.0-py3-none-any.whl", hash = "sha256:a376a028184f91790208325f5898026a91ad5e88f6bd95d69efdc4e4f9f0f94f", size = 32670, upload-time = "2026-06-30T04:19:34.322Z" }, +] + +[[package]] +name = "mongomock" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pytz" }, + { name = "sentinels" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/a4/4a560a9f2a0bec43d5f63104f55bc48666d619ca74825c8ae156b08547cf/mongomock-4.3.0.tar.gz", hash = "sha256:32667b79066fabc12d4f17f16a8fd7361b5f4435208b3ba32c226e52212a8c30", size = 135862, upload-time = "2024-11-16T11:23:25.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/4d/8bea712978e3aff017a2ab50f262c620e9239cc36f348aae45e48d6a4786/mongomock-4.3.0-py2.py3-none-any.whl", hash = "sha256:5ef86bd12fc8806c6e7af32f21266c61b6c4ba96096f85129852d1c4fec1327e", size = 64891, upload-time = "2024-11-16T11:23:24.748Z" }, +] + +[[package]] +name = "more-itertools" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, +] + +[[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 = "mysqlclient" +version = "2.2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/b0/9df076488cb2e536d40ce6dbd4273c1f20a386e31ffe6e7cb613902b3c2a/mysqlclient-2.2.8.tar.gz", hash = "sha256:8ed20c5615a915da451bb308c7d0306648a4fd9a2809ba95c992690006306199", size = 92287, upload-time = "2026-02-10T10:58:37.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/1f/7d6b3fbfc8a317805fdb8bbfbc23af99f2090497090e82513daf491a29ce/mysqlclient-2.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:000c7ec3d11e7c411db832e4cfcd7f05db47464326381f5d5ae991b4bb572f93", size = 207171, upload-time = "2026-02-10T10:58:39.827Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d5/76e369b0fdccd2eb9ed7d890e4e3e23aa1344fea62f0180d7f1574285e54/mysqlclient-2.2.8-cp313-cp313-win_amd64.whl", hash = "sha256:a81f5e12f8d05439709cb02fba97f9f76d1a6c528164f2260d8798fec969e300", size = 207158, upload-time = "2026-02-10T10:58:38.663Z" }, + { url = "https://files.pythonhosted.org/packages/42/44/6ae21b4e8490eb5167d04fa7452c462ffd72a502b96bf531477ef044a380/mysqlclient-2.2.8-cp314-cp314-win_amd64.whl", hash = "sha256:9bed7c8d3b629bdc09e17fb628d5b3b0a5fd1f12b09432b464b9126c727bedc0", size = 211644, upload-time = "2026-02-10T10:58:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/037d55623be9f681236e04abe12e1290847c06bd48270c3f19ac33493cbf/mysqlclient-2.2.8-cp314-cp314t-win_amd64.whl", hash = "sha256:260cce0e81446c83bf0a389e0fae38d68547d9f8fc0833bc733014e10ce28a99", size = 213067, upload-time = "2026-02-10T10:58:43.389Z" }, +] + +[[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 = "openedx-atlas" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/97/6cece8758ce11aa2511300929051077d498e23d4387f89caf86c076e664b/openedx-atlas-0.7.0.tar.gz", hash = "sha256:a698366c902c88e533cfcfea3ef1a6e32a693a275e673eaad67747463ba4efd5", size = 23165, upload-time = "2025-04-08T07:51:37.842Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/1c/868da1caa571d1c01e785a03110f90a0490465fc34a138ef7ce6dc5deed8/openedx_atlas-0.7.0-py3-none-any.whl", hash = "sha256:4884d98e03282181b14f6acefb264cd0843a892ac8ec91b9b53163d1155b4718", size = 22385, upload-time = "2025-04-08T07:51:36.502Z" }, +] + +[[package]] +name = "openedx-events" +version = "11.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "django", version = "4.2.30", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-django42'" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-dev' or extra != 'group-13-openedx-forum-django42' or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "edx-ccx-keys" }, + { name = "edx-django-utils" }, + { name = "edx-opaque-keys" }, + { name = "fastavro" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/8f/cc77bf1748e6c16125b04c47586b46fb43cac66f836780c73029db3393f8/openedx_events-11.2.0.tar.gz", hash = "sha256:b3216aeb959c3429c0091fa40ff8045322a0569a8cf36fa8910458f92923e472", size = 83833, upload-time = "2026-04-22T17:29:25.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/5d/6af3fcf206ff649a04eb35fb314721a9182f2edca06f7fc2f8ac66ba0b9d/openedx_events-11.2.0-py3-none-any.whl", hash = "sha256:679527b1e96a73c70a886c4c64c9109371040f73529bf32ea6ac9a442979ab5b", size = 61344, upload-time = "2026-04-22T17:29:24.146Z" }, +] + +[[package]] +name = "openedx-forum" +source = { editable = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "django", version = "4.2.30", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-django42'" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-openedx-forum-dev' or extra != 'group-13-openedx-forum-django42' or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "djangorestframework" }, + { name = "edx-search" }, + { name = "elasticsearch" }, + { name = "mysqlclient" }, + { name = "openedx-atlas" }, + { name = "pymongo" }, + { name = "requests" }, + { name = "typesense" }, +] + +[package.dev-dependencies] +ci = [ + { name = "tox" }, + { name = "tox-uv" }, +] +dev = [ + { name = "build" }, + { name = "code-annotations" }, + { name = "coverage" }, + { name = "diff-cover" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" } }, + { name = "djangorestframework-stubs" }, + { name = "edx-i18n-tools" }, + { name = "edx-lint" }, + { name = "faker" }, + { name = "mongomock" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "ruff" }, + { name = "tox" }, + { name = "tox-uv" }, + { name = "twine" }, + { name = "types-beautifulsoup4" }, + { name = "types-requests" }, +] +django42 = [ + { name = "build" }, + { name = "code-annotations" }, + { name = "coverage" }, + { name = "django", version = "4.2.30", source = { registry = "https://pypi.org/simple" } }, + { name = "edx-lint" }, + { name = "faker" }, + { name = "mongomock" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "twine" }, +] +doc = [ + { name = "build" }, + { name = "code-annotations" }, + { name = "coverage" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" } }, + { name = "doc8" }, + { name = "edx-lint" }, + { name = "faker" }, + { name = "mongomock" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "sphinx" }, + { name = "sphinx-book-theme" }, + { name = "twine" }, +] +quality = [ + { name = "build" }, + { name = "code-annotations" }, + { name = "coverage" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" } }, + { name = "djangorestframework-stubs" }, + { name = "edx-lint" }, + { name = "faker" }, + { name = "mongomock" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "ruff" }, + { name = "twine" }, + { name = "types-beautifulsoup4" }, + { name = "types-requests" }, +] +test = [ + { name = "build" }, + { name = "code-annotations" }, + { name = "coverage" }, + { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" } }, + { name = "edx-lint" }, + { name = "faker" }, + { name = "mongomock" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "twine" }, +] +test-base = [ + { name = "build" }, + { name = "code-annotations" }, + { name = "coverage" }, + { name = "edx-lint" }, + { name = "faker" }, + { name = "mongomock" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "twine" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4" }, + { name = "django", specifier = ">=4.2" }, + { name = "djangorestframework" }, + { name = "edx-search" }, + { name = "elasticsearch" }, + { name = "mysqlclient" }, + { name = "openedx-atlas" }, + { name = "pymongo" }, + { name = "requests" }, + { name = "typesense" }, +] + +[package.metadata.requires-dev] +ci = [ + { name = "tox" }, + { name = "tox-uv" }, +] +dev = [ + { name = "build" }, + { name = "code-annotations" }, + { name = "coverage" }, + { name = "diff-cover" }, + { name = "django", specifier = ">=5.2,<6.0" }, + { name = "djangorestframework-stubs" }, + { name = "edx-i18n-tools" }, + { name = "edx-lint" }, + { name = "faker" }, + { name = "mongomock" }, + { name = "mypy" }, + { name = "pytest", specifier = ">=7.0" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "ruff" }, + { name = "tox" }, + { name = "tox-uv" }, + { name = "twine" }, + { name = "types-beautifulsoup4" }, + { name = "types-requests" }, +] +django42 = [ + { name = "build" }, + { name = "code-annotations" }, + { name = "coverage" }, + { name = "django", specifier = ">=4.2,<5.0" }, + { name = "edx-lint" }, + { name = "faker" }, + { name = "mongomock" }, + { name = "pytest", specifier = ">=7.0" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "twine" }, +] +doc = [ + { name = "build" }, + { name = "code-annotations" }, + { name = "coverage" }, + { name = "django", specifier = ">=5.2,<6.0" }, + { name = "doc8" }, + { name = "edx-lint" }, + { name = "faker" }, + { name = "mongomock" }, + { name = "pytest", specifier = ">=7.0" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "sphinx" }, + { name = "sphinx-book-theme" }, + { name = "twine" }, +] +quality = [ + { name = "build" }, + { name = "code-annotations" }, + { name = "coverage" }, + { name = "django", specifier = ">=5.2,<6.0" }, + { name = "djangorestframework-stubs" }, + { name = "edx-lint" }, + { name = "faker" }, + { name = "mongomock" }, + { name = "mypy" }, + { name = "pytest", specifier = ">=7.0" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "ruff" }, + { name = "twine" }, + { name = "types-beautifulsoup4" }, + { name = "types-requests" }, +] +test = [ + { name = "build" }, + { name = "code-annotations" }, + { name = "coverage" }, + { name = "django", specifier = ">=5.2,<6.0" }, + { name = "edx-lint" }, + { name = "faker" }, + { name = "mongomock" }, + { name = "pytest", specifier = ">=7.0" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "twine" }, +] +test-base = [ + { name = "build" }, + { name = "code-annotations" }, + { name = "coverage" }, + { name = "edx-lint" }, + { name = "faker" }, + { name = "mongomock" }, + { name = "pytest", specifier = ">=7.0" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "twine" }, +] + +[[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 = "path" +version = "16.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/1c/3950c87aa25437af5f1663cc8627d44ff26f8c5117a5053c9fc3f641027c/path-16.16.0.tar.gz", hash = "sha256:a6a6d916c910dc17e0ddc883358756c5a33d1b6dbdf5d6de86554f399053af58", size = 50905, upload-time = "2024-07-27T09:37:45.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/0f/ddd60b4794cc8a8c086150e13ffbff438dbf306b2739918e65ddb706208f/path-16.16.0-py3-none-any.whl", hash = "sha256:d981989cf87598adc9f5b71ec5192d314a384836e81b4b1f34197138dc4ae659", size = 25531, upload-time = "2024-07-27T09:37:44.312Z" }, +] + +[[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 = "polib" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/9a/79b1067d27e38ddf84fe7da6ec516f1743f31f752c6122193e7bce38bdbf/polib-1.2.0.tar.gz", hash = "sha256:f3ef94aefed6e183e342a8a269ae1fc4742ba193186ad76f175938621dbfc26b", size = 161658, upload-time = "2023-02-23T17:53:56.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/99/45bb1f9926efe370c6dbe324741c749658e44cb060124f28dad201202274/polib-1.2.0-py2.py3-none-any.whl", hash = "sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d", size = 20634, upload-time = "2023-02-23T17:53:59.919Z" }, +] + +[[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 = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[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 = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "pydata-sphinx-theme" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "babel" }, + { name = "beautifulsoup4" }, + { name = "docutils" }, + { name = "pygments" }, + { name = "sphinx" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/0d/8ba33fa83a7dcde13eb3c1c2a0c1cc29950a048bfed6d9b0d8b6bd710b4c/pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde", size = 6723264, upload-time = "2024-12-17T10:53:35.645Z" }, +] + +[[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 = "pylint" +version = "4.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { 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 = "pymongo" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/64/50be6fbac9c79fe2e4c17401a467da2d8764d82833d83cec325afe5cab32/pymongo-4.17.0.tar.gz", hash = "sha256:70ffa08ba641468cc068cf46c06b34f01a8ce3489f6411309fcb5ceabe6b2fc0", size = 2523370, upload-time = "2026-04-20T16:39:53.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/90/60bcb508840135d5ee46b51b1a950f548338aa8145a8366dbe6639ae51ac/pymongo-4.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53ffa94b2340dbf6b055e09a0090618c60482c158ecfc9565642fc996bf0944", size = 930529, upload-time = "2026-04-20T16:38:00.936Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e9/313840f1e52c6dfac47f704428cbfbce59956ebe7633bffc92b03f74f0ad/pymongo-4.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6fe0de9d0f6791abce3471230b32b4817bf89d27b1182b6a550e1ec0fa72aa9a", size = 930665, upload-time = "2026-04-20T16:38:02.915Z" }, + { url = "https://files.pythonhosted.org/packages/78/35/9d3565ea45b1606f635c1e2cd2563c28d66caafdc50f7ad7d979fcd1b363/pymongo-4.17.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e537e95514dae1aaa718f481ec03151a0f0394bcd05f1322896d8fc1330cb729", size = 1762369, upload-time = "2026-04-20T16:38:05.375Z" }, + { url = "https://files.pythonhosted.org/packages/95/ee/149b0d4b1a11c38bff6f14c23d5814c9b0843fd6dc38ad40596bdb1a62d2/pymongo-4.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37a8385c29881b43eab31f584100fa0eaddedd5607adf010147ba1810118be90", size = 1798044, upload-time = "2026-04-20T16:38:07.195Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d4/4cee4a7b8d8f6f0550ef6cd2fea42455c5ed619a220cb6ba4fb40d6a5bc8/pymongo-4.17.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f3ee3d241ed77a4fc99ce3cff3b289c3ebce37f61fdd7349d3592c23b82c8784", size = 1878567, upload-time = "2026-04-20T16:38:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/45/ef/7fe366c84952619ee2f69973566c214775e083dd4df465751912153e4b72/pymongo-4.17.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9eb5d63a3c518cb0804ed678f5e2b875af032d89a7cf57a57360322cf6a4d222", size = 1864881, upload-time = "2026-04-20T16:38:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/2f/35/b577d82c6d1be7aee7ac7e249bc86f7847998345042e5f8360de238e177b/pymongo-4.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e97e03fa13327c87e3fdc5656acd01e71817f0c1dc3221cd8f30de136bf4ec3", size = 1800349, upload-time = "2026-04-20T16:38:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/b8/69/dafcf04f66e130ddd91aeb92e7a692480eda46dcd04ec1dbe82c06619e10/pymongo-4.17.0-cp312-cp312-win32.whl", hash = "sha256:6877214bff5f06f6884a9fc8d9016a4a7a5f51f537f5c51ac3a576f93e7dfb32", size = 900518, upload-time = "2026-04-20T16:38:15.541Z" }, + { url = "https://files.pythonhosted.org/packages/11/35/5c9262a459f988b4eb2605f70815240b77a0d4131136c4326d18f1822b89/pymongo-4.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:9828485f72f63c7d802e0ec41f71906f633c2692621ab3af55ca990186b091b1", size = 920335, upload-time = "2026-04-20T16:38:17.665Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/e9c7265ee176faccf4e52c4797837e794d93569a1046f6b19a4acc36e5ad/pymongo-4.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:1195370a77baf003b59b10e91ecc4706297197f0dd9d29c840cc556dc08f7cee", size = 903289, upload-time = "2026-04-20T16:38:19.33Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/c1206879708b94e82fcd8b9653440ec271f79a3674d122192df383047f5a/pymongo-4.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:809ec74de3b9148ae43fa8df9faf53470f511c8d384f13b99d6f671f2a379f15", size = 985829, upload-time = "2026-04-20T16:38:21.031Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cf/bb044ed85160e5c40f568c7c4f4e8ea16f40764ff5d302e5befbe8f6f814/pymongo-4.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a431b737816bf4cddd4fa0fcef04e424ad36b7692734a64150f872fb8f3208be", size = 985899, upload-time = "2026-04-20T16:38:23.409Z" }, + { url = "https://files.pythonhosted.org/packages/74/0a/f6dfd5ea3901e5d6888da8de8ba728971a1d447debab681cfc56f90d1208/pymongo-4.17.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4fab10f8403169ce92f3cea921609d9ee81107306caae06c08f592d4b8ad2b5", size = 2028569, upload-time = "2026-04-20T16:38:25.343Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c5/081f59a1c02ae8c0dc73ae58e563838c44eec81aeafa7d0b93a637841c9b/pymongo-4.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20323b0b1c1d33770ad1fc68d429c757734ce9ad3594421c3d6618f10572b1b9", size = 2072916, upload-time = "2026-04-20T16:38:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/31/42/6e41d434297ffe8b30d9c3717916591a4a7be9075a0dcc2fafdfaaaa62ed/pymongo-4.17.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5a5de048e6da5c18e27cc2437e8c15b3b0cdc8385c15b41178b0caa3322a09c2", size = 2173234, upload-time = "2026-04-20T16:38:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/3d/cf/1e4a7db352ef9485831c7268dfe8402f0117b32a9ad54b16e810699e3617/pymongo-4.17.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dff3de1294fbbc1db0ba6b511f77b8e540601d092538a31312e99c8a91a78b1e", size = 2156784, upload-time = "2026-04-20T16:38:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/12/10/6195be29962a61ebb5f4bd9e4c7519890b172f7968a0a0d880398c6ddb02/pymongo-4.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faf03e4c2aafd6de626dbd30ba246d369ae33f47f10629d1bbe40f72115027a6", size = 2074446, upload-time = "2026-04-20T16:38:34.004Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/33410b8819837ed370c738587306bdf060b59cef11823be212f4a07703c5/pymongo-4.17.0-cp313-cp313-win32.whl", hash = "sha256:c9786665926a09630c5d420c79762cfadbff35a9438bcbc4c81a9fb5ab9228b7", size = 948435, upload-time = "2026-04-20T16:38:35.922Z" }, + { url = "https://files.pythonhosted.org/packages/6f/77/c0ed522f798a286b99acaa7914ed8d9c80ab091f97f57c59ffed72906e5e/pymongo-4.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:5960519b4d7168f1ecdd3ea10c81b2aedeb9423651aca953cfbc8e76705d3b38", size = 972847, upload-time = "2026-04-20T16:38:37.888Z" }, + { url = "https://files.pythonhosted.org/packages/97/f0/c39480a2db385fde23861d0c8acda41cdaf1d43e46579db72c5c013a2e81/pymongo-4.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:0ff6bd2f735ab5356541e3e57d5b7dbfbc3f2ee1ccb10b6b0f82d58af69d1d8e", size = 951575, upload-time = "2026-04-20T16:38:40.544Z" }, + { url = "https://files.pythonhosted.org/packages/da/49/2b0250762a89737ed6f9cea238331baca061b89a8ddd10dd17fee52c3970/pymongo-4.17.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff5aa3f1c7e3f08eb0e7a016c91ba468b1850ccfd63d9b1f12f56350f4974cef", size = 1040945, upload-time = "2026-04-20T16:38:42.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/1c/7a9b5447a08be20e84b6e5b17330917e8d6d9507daa3cd099a9309f11ad7/pymongo-4.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e816db649ba5d7de0568cf3a9f287a9dc9aad21cf0ca667ab156a7ef47fca0b0", size = 1041187, upload-time = "2026-04-20T16:38:45.358Z" }, + { url = "https://files.pythonhosted.org/packages/78/a1/71704f61632dfc90407a5834fe5f6132854937c4a3648f6c05c351d85a45/pymongo-4.17.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c4fded3a9f1d6a687e36ebd384ac6d00b9b00de1969aa74048e7051ec2a713", size = 2294806, upload-time = "2026-04-20T16:38:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b9/aff42be75108b96c2469b1d9329b912c15108f3e7ef32fdc86da8423c330/pymongo-4.17.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2db66aa8dd253a0fc1fad3b0d23d5b3993f7ebde02fbbd7727128debf2853675", size = 2348231, upload-time = "2026-04-20T16:38:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/f2/30/44c115b8ba1479942c15fd9480eb29a7da0ba68acd56983423ba0deb4a94/pymongo-4.17.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3987e96e7c7be4083d42e8ac2cc6c0d5b78db9973c90fce42ae800b616ca6b20", size = 2467614, upload-time = "2026-04-20T16:38:52.665Z" }, + { url = "https://files.pythonhosted.org/packages/d2/84/21ee95c8bf0ca7acae7ec7eb365d740bf8fc0156c194baf2c3bdfcb85ec0/pymongo-4.17.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cee36b3c0d0354f880fa7a7fdcdaf2bb5e542c2281e25c1bfadf8cfe21eba7d2", size = 2445970, upload-time = "2026-04-20T16:38:55.175Z" }, + { url = "https://files.pythonhosted.org/packages/06/89/081d7f1809d5ca09d1e47e49f2111b245f5694de3a7af32cd3a353a6f43f/pymongo-4.17.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:320b34457b20bbcc79997801f95d25ce00472915ca5241167242b42c4359e027", size = 2348605, upload-time = "2026-04-20T16:38:57.557Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c3/0d949f9d3f2a341c1f635c398c16615e96f89f51ff424ed81e914cf1a4de/pymongo-4.17.0-cp314-cp314-win32.whl", hash = "sha256:df4a644af9ae132d4bfdb2e9516ea51a615fd881caddfbfbd071cf1354844479", size = 1004119, upload-time = "2026-04-20T16:39:00.309Z" }, + { url = "https://files.pythonhosted.org/packages/f7/55/5c3a3db1048054c695c75c5964cc8bedc2247fdb5a75ef6fab4ec8bb013e/pymongo-4.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:c797f8a80957134f6dd9690367a0f8f5906d672119af2c6aa55f0c527b656bed", size = 1032314, upload-time = "2026-04-20T16:39:02.665Z" }, + { url = "https://files.pythonhosted.org/packages/e0/19/e235f39906134cb0ffd5574c5a59c355ef5380f0499644ab94994afbb109/pymongo-4.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:68fca71e05ee5da23a8d73cee8379dfb3d26e609a377cae731d742771ed96946", size = 1007627, upload-time = "2026-04-20T16:39:04.678Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e0/c4c1a86791415b14c684fa0908f9da96de91594a3fd1fa1b8dc689fbb800/pymongo-4.17.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b4384700cffc3f1dd98e088bc0072dedf6d7d68a230bb4b972665cf69c071c1e", size = 1099151, upload-time = "2026-04-20T16:39:06.969Z" }, + { url = "https://files.pythonhosted.org/packages/81/4b/69c67f3e23fd9b23b9bedc7ebd23754881cc9d5c5d5b2a9811e96b07f475/pymongo-4.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:93641192644fa1ee0f34030e774fd31022a27ad11ba22cb1716142231524f8bd", size = 1099346, upload-time = "2026-04-20T16:39:08.996Z" }, + { url = "https://files.pythonhosted.org/packages/a2/19/a5208f62f9508a26d73acc69bd3821b8c8adae253679a3c26d2f9652f0d5/pymongo-4.17.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75bc3aa5b94fdb7138d357ec6ca61cd97e0c79f4f7f0bd3efe9639b15cc50942", size = 2619034, upload-time = "2026-04-20T16:39:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/77/27/426cba1ec5973082a56d4150798529bfdf4151c31391ed1fbbecb23ef2ac/pymongo-4.17.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e8f8e23c6df7c6d6929f5e734980b227706e73ee847517c9ba5af90f7fc466", size = 2689939, upload-time = "2026-04-20T16:39:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2e/f70993d1255e33f6ee59a4ec4371cc65bff7a7e3fda7d55c3386f25287e8/pymongo-4.17.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:15d3f3d732aecac1f8d481bde4029755615639bd3076f258a2147210aec8515a", size = 2824994, upload-time = "2026-04-20T16:39:16.057Z" }, + { url = "https://files.pythonhosted.org/packages/b3/eb/87b0e988ba889e1fcc3430c2cfc166b251872c813e92b43174298bee17ff/pymongo-4.17.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5f62862d0f87be481fa1fe8cb811994486773c94a2b61e509285e3f2890763", size = 2801745, upload-time = "2026-04-20T16:39:18.476Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/3f83412d086f682d4d468761d66ddc49cf161e786ea74073045eb4491c60/pymongo-4.17.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64837adbbd72073301af51bb0fc80e3d7707fe5527cea1033ba0320f0b2f881b", size = 2684636, upload-time = "2026-04-20T16:39:20.878Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/b75f6f4ab6c8beb50b0270a4f1e2530b5774f5e116563440e1677ca1820f/pymongo-4.17.0-cp314-cp314t-win32.whl", hash = "sha256:b93b22eedc62598cf5ee9d8c8007a8e9121c50fd88137012d8985500e9dc3151", size = 1056356, upload-time = "2026-04-20T16:39:22.996Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5e/648c8a238eef18a25ed8a169ea6542d4a860bbec3e95b3d9badac2935c71/pymongo-4.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3689ea34f6b647c7d1e7bdc60fcfb214b2789ed1359a7fb96569c69f50e5f18f", size = 1090964, upload-time = "2026-04-20T16:39:24.989Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cb/d9780b66939c4fc1f024bcc7be23a2abcfe06a9745ca8fa76dc73395482e/pymongo-4.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9543d8f84c2e5608565c08ac679774811e6730770d8a645439b073422a4276fb", size = 1058526, upload-time = "2026-04-20T16:39:27.924Z" }, +] + +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + +[[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 = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { 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-django" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" }, +] + +[[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-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 = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[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 = "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-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 = "restructuredtext-lint" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/e6/eefcad2228f4124f17e01064428fbcd0ade06a274f3063ce3a126a569d6b/restructuredtext_lint-2.0.2.tar.gz", hash = "sha256:dd25209b9e0b726929d8306339faf723734a3137db382bcf27294fa18a6bc52b", size = 17494, upload-time = "2025-11-23T08:05:18.585Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/63/ac52b32b33ae62f2076ed5c4f6b00e065e3ccbb2063e9a2e813b2bfc95bf/restructuredtext_lint-2.0.2-py3-none-any.whl", hash = "sha256:374c0d3e7e0867b2335146a145343ac619400623716b211b9a010c94426bbed7", size = 14198, upload-time = "2025-11-23T08:05:23.267Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[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 = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "sentinels" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/9b/07195878aa25fe6ed209ec74bc55ae3e3d263b60a489c6e73fdca3c8fe05/sentinels-1.1.1.tar.gz", hash = "sha256:3c2f64f754187c19e0a1a029b148b74cf58dd12ec27b4e19c0e5d6e22b5a9a86", size = 4393, upload-time = "2025-08-12T07:57:50.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/65/dea992c6a97074f6d8ff9eab34741298cac2ce23e2b6c74fb7d08afdf85c/sentinels-1.1.1-py3-none-any.whl", hash = "sha256:835d3b28f3b47f5284afa4bf2db6e00f2dc5f80f9923d4b7e7aeeeccf6146a11", size = 3744, upload-time = "2025-08-12T07:57:48.858Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + +[[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 = "soupsieve" +version = "2.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, +] + +[[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-book-theme" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydata-sphinx-theme" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/f7/154786f3cfb7692cd7acc24b6dfe4dcd1146b66f376b17df9e47125555e9/sphinx_book_theme-1.2.0.tar.gz", hash = "sha256:4a7ebfc7da4395309ac942ddfc38fbec5c5254c3be22195e99ad12586fbda9e3", size = 443962, upload-time = "2026-03-09T23:20:30.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/bf/6f506a37c7f8ecc4576caf9486e303c7af249f6d70447bb51dde9d78cb99/sphinx_book_theme-1.2.0-py3-none-any.whl", hash = "sha256:709605d308e1991c5ef0cf19c481dbe9084b62852e317fafab74382a0ee7ccfa", size = 455936, upload-time = "2026-03-09T23:20:28.788Z" }, +] + +[[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-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 = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + +[[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 = "twine" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "(platform_machine != 'ppc64le' and platform_machine != 's390x') or (platform_machine == 'ppc64le' and extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (platform_machine == 'ppc64le' and extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (platform_machine == 'ppc64le' and extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (platform_machine == 'ppc64le' and extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test') or (platform_machine == 's390x' and extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (platform_machine == 's390x' and extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (platform_machine == 's390x' and extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (platform_machine == 's390x' and extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, +] + +[[package]] +name = "types-beautifulsoup4" +version = "4.12.0.20250516" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-html5lib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/d1/32b410f6d65eda94d3dfb0b3d0ca151f12cb1dc4cef731dcf7cbfd8716ff/types_beautifulsoup4-4.12.0.20250516.tar.gz", hash = "sha256:aa19dd73b33b70d6296adf92da8ab8a0c945c507e6fb7d5db553415cc77b417e", size = 16628, upload-time = "2025-05-16T03:09:09.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/79/d84de200a80085b32f12c5820d4fd0addcbe7ba6dce8c1c9d8605e833c8e/types_beautifulsoup4-4.12.0.20250516-py3-none-any.whl", hash = "sha256:5923399d4a1ba9cc8f0096fe334cc732e130269541d66261bb42ab039c0376ee", size = 16879, upload-time = "2025-05-16T03:09:09.051Z" }, +] + +[[package]] +name = "types-html5lib" +version = "1.1.11.20260518" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/5a/0c708d1b0d35ad48b6a223c77c4a882fd016b40c25becb082a92e02a9c00/types_html5lib-1.1.11.20260518.tar.gz", hash = "sha256:4f33c087cb1119d65c4c80eca4323c2b501f9eaf8af9616b8b732ed4d8eae8fa", size = 18420, upload-time = "2026-05-18T06:07:23.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/d0/b088b9f11eb69637d6826843f06caaff60247156735a25512922d3dc2c13/types_html5lib-1.1.11.20260518-py3-none-any.whl", hash = "sha256:9baa7912224ebb37027c5ccb7e3768e43ea47b1dfdd977e7ddc4b0a4a550584d", size = 24339, upload-time = "2026-05-18T06:07:22.876Z" }, +] + +[[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.31.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/b8/c1e8d39996b4929b918aba10dba5de07a8b3f4c8487bb61bb79882544e69/types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0", size = 15535, upload-time = "2023-09-27T06:19:38.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/a1/6f8dc74d9069e790d604ddae70cb46dcbac668f1bb08136e7b0f2f5cd3bf/types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9", size = 14516, upload-time = "2023-09-27T06:19:36.373Z" }, +] + +[[package]] +name = "types-urllib3" +version = "1.26.25.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/de/b9d7a68ad39092368fb21dd6194b362b98a1daeea5dcfef5e1adb5031c7e/types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", size = 11239, upload-time = "2023-07-20T15:19:31.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/7b/3fc711b2efea5e85a7a0bbfe269ea944aa767bbba5ec52f9ee45d362ccf3/types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e", size = 15377, upload-time = "2023-07-20T15:19:30.379Z" }, +] + +[[package]] +name = "types-webencodings" +version = "0.5.0.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/d2/21567fac142315580ce3ee37d08a4e8819921dae833bbcd27b9f8b373799/types_webencodings-0.5.0.20260408.tar.gz", hash = "sha256:28c596619f367e43eee393d85f63e8d2fdb6874c654a8d441c37f8afe29c6d0d", size = 7504, upload-time = "2026-04-08T04:28:51.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/e4/f13be8f6d9a561166f7d963012d0ccc833e13aee3044c4f1a8fb1fee462a/types_webencodings-0.5.0.20260408-py3-none-any.whl", hash = "sha256:19a2afe5c22d9b1e880b49ff823c7b531f473a390fe47ac903c0bdb5cd677dd9", size = 8717, upload-time = "2026-04-08T04:28:50.943Z" }, +] + +[[package]] +name = "typesense" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/92/ca06da7ad7e771804fe21fdcfbc99d24079bedb9ab0ceaedac7ea1001263/typesense-2.0.0.tar.gz", hash = "sha256:9a306e328bf0c0030679c154730ec142efbf07fd0c33845b3efae10291e983e7", size = 79292, upload-time = "2026-02-16T16:37:56.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/a5/a392604f3eeed45d3d72f8f10b45a80082a826338609e62187d9e3614001/typesense-2.0.0-py3-none-any.whl", hash = "sha256:7eecc672e4049f7b5777e1cf1e7900d2d1ae2d28a670eda6545b568d1cde1700", size = 140736, upload-time = "2026-02-16T16:37:55.256Z" }, +] + +[[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 = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[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' or (extra == 'group-13-openedx-forum-dev' and extra == 'group-13-openedx-forum-django42') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-doc') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-quality') or (extra == 'group-13-openedx-forum-django42' and extra == 'group-13-openedx-forum-test')" }, +] +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 = "urllib3" +version = "1.26.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, +] + +[[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" }, +]