From a952fb4cf499eb5a9388d95c22a413b0727934f2 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Sat, 26 Sep 2026 12:58:03 -0400 Subject: [PATCH] build: modernize repo tooling and remove sample-plugin leftovers This repo was bootstrapped from openedx/sample-plugin's backend directory. Remove the course-archiving code, settings, migrations and translations that came with it, and bring the tooling in line with current Open edX practices (openedx/public-engineering#506): - Fix setuptools-scm, which pointed one directory up at the sample-plugin repo root, and move the isort config into pyproject.toml (drop setup.cfg). - Use `>=0` dependency specifiers, a 7-day uv exclude-newer window, and regenerate the edx-lint uv constraints and uv.lock. - Add `make check-constraints` to the quality run; route make targets through `uv run`. - Add GitHub Actions: aggregated CI, semantic release with PyPI trusted publishing, commitlint, upgrade-requirements, standard org workflows, and Dependabot for actions. - Fix test settings and add smoke tests so pytest and Django checks pass. - Point Read the Docs at `uv sync --group doc`, update the Sphinx config, and rewrite the README and getting-started docs. Co-Authored-By: Claude Opus 5.5 (1M context) --- .annotation_safe_list.yml | 26 -- .coveragerc | 4 +- .editorconfig | 4 +- .github/dependabot.yml | 8 + .../add-depr-ticket-to-depr-board.yml | 19 + .../workflows/add-remove-label-on-comment.yml | 20 + .github/workflows/ci.yml | 86 ++++ .github/workflows/commitlint.yml | 10 + .github/workflows/release.yml | 100 +++++ .github/workflows/self-assign-issue.yml | 12 + .../workflows/upgrade-python-requirements.yml | 27 ++ .gitignore | 11 +- .readthedocs.yaml | 19 +- Makefile | 95 ++-- README.md | 31 +- docs/conf.py | 31 +- docs/getting_started.rst | 46 +- docs/index.rst | 16 +- docs/internationalization.rst | 49 --- docs/openedx_plugin_sample.rst | 69 --- docs/testing.rst | 9 +- pylintrc | 13 +- pylintrc_tweaks | 2 +- pyproject.toml | 85 ++-- setup.cfg | 7 - src/openedx_search/__init__.py | 2 +- src/openedx_search/apps.py | 3 +- src/openedx_search/conf/locale/config.yaml | 85 ---- src/openedx_search/migrations/0001_initial.py | 36 -- src/openedx_search/models.py | 3 + src/openedx_search/settings/common.py | 136 ------ src/openedx_search/settings/production.py | 16 - src/openedx_search/settings/test.py | 17 - src/openedx_search/views.py | 268 ----------- test_settings.py | 41 +- tests/__init__.py | 0 tests/test_app.py | 18 + tests/urls.py | 8 + tox.ini | 10 +- uv.lock | 416 +++--------------- 40 files changed, 579 insertions(+), 1279 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/add-depr-ticket-to-depr-board.yml create mode 100644 .github/workflows/add-remove-label-on-comment.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/commitlint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/self-assign-issue.yml create mode 100644 .github/workflows/upgrade-python-requirements.yml delete mode 100644 docs/internationalization.rst delete mode 100644 docs/openedx_plugin_sample.rst delete mode 100644 setup.cfg delete mode 100644 src/openedx_search/conf/locale/config.yaml delete mode 100644 src/openedx_search/migrations/0001_initial.py delete mode 100644 src/openedx_search/settings/common.py delete mode 100644 src/openedx_search/settings/production.py delete mode 100644 src/openedx_search/settings/test.py delete mode 100644 src/openedx_search/views.py delete mode 100644 tests/__init__.py create mode 100644 tests/test_app.py diff --git a/.annotation_safe_list.yml b/.annotation_safe_list.yml index deb98bf..715efe1 100644 --- a/.annotation_safe_list.yml +++ b/.annotation_safe_list.yml @@ -21,29 +21,3 @@ contenttypes.ContentType: ".. no_pii:": "This model has no PII" sessions.Session: ".. no_pii:": "This model has no PII" -social_django.Association: - ".. no_pii:": "This model has no PII" -social_django.Code: - ".. pii:": "Email address" - ".. pii_types:": other - ".. pii_retirement:": local_api -social_django.Nonce: - ".. no_pii:": "This model has no PII" -social_django.Partial: - ".. no_pii:": "This model has no PII" -social_django.UserSocialAuth: - ".. no_pii:": "This model has no PII" -waffle.Flag: - ".. no_pii:": "This model has no PII" -waffle.Sample: - ".. no_pii:": "This model has no PII" -waffle.Switch: - ".. no_pii:": "This model has no PII" -openedx_catalog.CatalogCourse: - ".. no_pii:": "This model has no PII" -openedx_catalog.CourseRun: - ".. no_pii:": "This model has no PII" -organizations.HistoricalOrganization: - ".. no_pii:": "This model has no PII" -organizations.HistoricalOrganizationCourse: - ".. no_pii:": "This model has no PII" diff --git a/.coveragerc b/.coveragerc index c4831b5..ac435f9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,10 +1,8 @@ [run] branch = True data_file = .coverage -source=sample_plugin +source=openedx_search omit = test_settings.py */migrations/* *admin.py - */static/* - */templates/* diff --git a/.editorconfig b/.editorconfig index 5cf5aed..a0eec7a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -64,7 +64,7 @@ # SERIOUSLY. # # ------------------------------ -# Generated by edx-lint version: 5.2.5 +# Generated by edx-lint version: 6.2.0 # ------------------------------ [*] end_of_line = lf @@ -97,4 +97,4 @@ max_line_length = 72 [*.rst] max_line_length = 79 -# f2f02689fced7a2e0c62c2f9803184114dc2ae4b +# 1fa0c820cb13bc717a559f365e5caac85c69459b diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9e401a1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/.github/workflows/add-depr-ticket-to-depr-board.yml b/.github/workflows/add-depr-ticket-to-depr-board.yml new file mode 100644 index 0000000..250e394 --- /dev/null +++ b/.github/workflows/add-depr-ticket-to-depr-board.yml @@ -0,0 +1,19 @@ +# Run the workflow that adds new tickets that are either: +# - labelled "DEPR" +# - title starts with "[DEPR]" +# - body starts with "Proposal Date" (this is the first template field) +# to the org-wide DEPR project board + +name: Add newly created DEPR issues to the DEPR project board + +on: + issues: + types: [opened] + +jobs: + routeissue: + uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master + secrets: + GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }} + GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }} diff --git a/.github/workflows/add-remove-label-on-comment.yml b/.github/workflows/add-remove-label-on-comment.yml new file mode 100644 index 0000000..0f369db --- /dev/null +++ b/.github/workflows/add-remove-label-on-comment.yml @@ -0,0 +1,20 @@ +# This workflow runs when a comment is made on the ticket +# If the comment starts with "label: " it tries to apply +# the label indicated in rest of comment. +# If the comment starts with "remove label: ", it tries +# to remove the indicated label. +# Note: Labels are allowed to have spaces and this script does +# not parse spaces (as often a space is legitimate), so the command +# "label: really long lots of words label" will apply the +# label "really long lots of words label" + +name: Allows for the adding and removing of labels via comment + +on: + issue_comment: + types: [created] + +jobs: + add_remove_labels: + uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4cd6773 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,86 @@ +name: CI + +# Every check that runs on a pull request, aggregated behind a single +# "all required checks passed" job. Branch protection only needs to require +# that one name, so adding or retiring a Django version from the matrix never +# requires a change to the repository settings. + +on: + pull_request: + branches: + - "**" + # So the release workflow can run every check before it publishes anything. + workflow_call: + +concurrency: + # On a pull request, group by the PR so a new push supersedes the run still + # going for the previous one. Any other event -- notably the workflow_call + # from the release -- keys on the unique run id, so release runs never queue + # behind or cancel each other. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + run_tests: + name: tests + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.12"] + toxenv: [quality, docs, pii_check, django52, django60] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: setup python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + with: + # Every matrix job populates the uv cache with a different set of tox + # dependencies, so give each one its own cache key. + cache-suffix: ${{ matrix.toxenv }} + + - name: Install CI dependencies + run: uv sync --group ci + + - name: Run Tests + env: + TOXENV: ${{ matrix.toxenv }} + # `uv sync` does not put tox on PATH, so run it through uv. + run: uv run tox + + - name: Run coverage + if: matrix.python-version == '3.12' && matrix.toxenv == 'django52' + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: unittests + fail_ci_if_error: true + + required_checks: + # This name is what branch protection requires, so changing it means + # updating the repository settings at the same moment. + name: all required checks passed + # `always()` so this still reports when something upstream fails. A skipped + # required check never reports, which blocks the pull request forever. + if: always() + needs: [run_tests] + runs-on: ubuntu-latest + steps: + - name: Check that every job succeeded + run: | + results='${{ join(needs.*.result, ' ') }}' + echo "Job results: $results" + for result in $results; do + if [ "$result" != "success" ]; then + echo "::error::A required job reported '$result'." + exit 1 + fi + done + echo "All jobs succeeded." diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 0000000..fec11d6 --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,10 @@ +# Run commitlint on the commit messages in a pull request. + +name: Lint Commit Messages + +on: + - pull_request + +jobs: + commitlint: + uses: openedx/.github/.github/workflows/commitlint.yml@master diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a6981c7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,100 @@ +name: Semantic Release + +on: + push: + branches: [main] + +jobs: + # The same aggregate a pull request has to pass, so the release runs exactly + # the checks that were required to merge. + run_ci: + uses: ./.github/workflows/ci.yml + + release: + needs: [run_ci] + runs-on: ubuntu-latest + if: github.ref_name == 'main' + concurrency: + group: ${{ github.workflow }}-release-${{ github.ref_name }} + cancel-in-progress: false + + permissions: + contents: write + + steps: + # Check out the branch that triggered the workflow, then force it back to + # the workflow sha: the branch may have moved while CI was running, and we + # must not release changes that were never evaluated. + - name: Setup | Checkout Repository on Release Branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.ref_name }} + + - name: Setup | Force release branch to be at workflow sha + run: | + git reset --hard ${{ github.sha }} + + - name: Action | Semantic Version Release + id: release + uses: python-semantic-release/python-semantic-release@9a026e9303981c866c3425723009becb2437c757 # v10.6.2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + git_committer_name: "github-actions" + git_committer_email: "actions@users.noreply.github.com" + changelog: "false" + # Commit, tag, push and build, but don't create the GitHub release. + # We create it ourselves in the next step so that the distributions + # are attached before the release is published. + vcs_release: "false" + + # If the repo has immutable releases enabled, assets cannot be attached + # after a release is published. `gh release create` with the assets + # creates it as a draft, uploads them, and only then publishes it: + # https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases + - name: Publish | Create GitHub Release with Assets + if: steps.release.outputs.released == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_NOTES: ${{ steps.release.outputs.release_notes }} + TAG: ${{ steps.release.outputs.tag }} + run: | + printf '%s' "$RELEASE_NOTES" > "$RUNNER_TEMP/release_notes.md" + gh release create "$TAG" \ + --verify-tag \ + --title "$TAG" \ + --notes-file "$RUNNER_TEMP/release_notes.md" \ + dist/* + + - 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: + # A separate job so that publishing runs with the least token privilege, + # and so that a failed upload can be retried without redoing the release. + runs-on: ubuntu-latest + needs: release + if: github.ref_name == 'main' && needs.release.outputs.released == 'true' + + permissions: + contents: read + # PyPI trusted publishing (OIDC); no API token is stored in the repo. + id-token: write + + steps: + - name: Setup | 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@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 diff --git a/.github/workflows/self-assign-issue.yml b/.github/workflows/self-assign-issue.yml new file mode 100644 index 0000000..37522fd --- /dev/null +++ b/.github/workflows/self-assign-issue.yml @@ -0,0 +1,12 @@ +# This workflow runs when a comment is made on the ticket +# If the comment starts with "assign me" it assigns the author to the +# ticket (case insensitive) + +name: Assign comment author to ticket if they say "assign me" +on: + issue_comment: + types: [created] + +jobs: + self_assign_by_comment: + uses: openedx/.github/.github/workflows/self-assign-issue.yml@master diff --git a/.github/workflows/upgrade-python-requirements.yml b/.github/workflows/upgrade-python-requirements.yml new file mode 100644 index 0000000..293fcc9 --- /dev/null +++ b/.github/workflows/upgrade-python-requirements.yml @@ -0,0 +1,27 @@ +name: Upgrade Python Requirements + +on: + schedule: + - cron: "0 0 * * 1" + workflow_dispatch: + inputs: + branch: + description: "Target branch against which to create requirements PR" + required: true + default: 'main' + +jobs: + call-upgrade-python-requirements-workflow: + uses: openedx/.github/.github/workflows/upgrade-python-requirements.yml@master + with: + branch: ${{ github.event.inputs.branch || 'main' }} + # optional parameters below; fill in if you'd like github or email notifications + # user_reviewers: "" + # team_reviewers: "" + # email_address: "" + # send_success_notification: false + secrets: + requirements_bot_github_token: ${{ secrets.REQUIREMENTS_BOT_GITHUB_TOKEN }} + requirements_bot_github_email: ${{ secrets.REQUIREMENTS_BOT_GITHUB_EMAIL }} + edx_smtp_username: ${{ secrets.EDX_SMTP_USERNAME }} + edx_smtp_password: ${{ secrets.EDX_SMTP_PASSWORD }} diff --git a/.gitignore b/.gitignore index 80a788a..11ba9e9 100644 --- a/.gitignore +++ b/.gitignore @@ -54,16 +54,13 @@ ghostdriver.log output/*.html output/*/index.html -# Sphinx +# Sphinx (sphinx-apidoc output is regenerated on every build) docs/_build docs/modules.rst -docs/sample_plugin.rst -docs/sample_plugin.*.rst - -# Private requirements -requirements/private.in -requirements/private.txt +docs/openedx_search.rst +docs/openedx_search.*.rst # Test cruft default.db pii_report +pyproject.toml.orig diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1d8cc9a..68aafb7 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,27 +1,22 @@ -# .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details -# Required version: 2 -# Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py fail_on_warning: true -# Set the version of python needed to build these docs. build: - os: "ubuntu-22.04" + os: "ubuntu-24.04" tools: python: "3.12" python: install: - - requirements: requirements/doc.txt - - # This will pip install this repo into the python environment - # if you are using this in a repo that is not pip installable - # then you should remove the following two lines. - - method: pip - path: . + # `uv sync` against the committed uv.lock, so the docs build against the + # same pinned versions as CI. This also installs this package itself. + - method: uv + command: sync + groups: + - doc diff --git a/Makefile b/Makefile index a19af18..f047ec7 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ -.PHONY: clean clean_tox compile_translations coverage diff_cover docs dummy_translations \ - extract_translations fake_translations help pii_check pull_translations \ - quality requirements selfcheck test test-all upgrade compile-requirements validate install_transifex_client +.PHONY: check-constraints clean clean_tox compile-requirements coverage diff_cover docs help pii_check \ + quality requirements selfcheck test test-all upgrade validate .DEFAULT_GOAL := help @@ -12,92 +11,70 @@ help: ## display this help message @awk -F ':.*?## ' '/^[a-zA-Z]/ && NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort clean: ## remove generated byte code, coverage reports, and build artifacts - find . -name '__pycache__' -exec rm -rf {} + - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + - coverage erase + find . -name '__pycache__' -not -path './.venv/*' -exec rm -rf {} + + find . -name '*.pyc' -not -path './.venv/*' -exec rm -f {} + + find . -name '*.pyo' -not -path './.venv/*' -exec rm -f {} + + find . -name '*~' -not -path './.venv/*' -exec rm -f {} + + rm -f .coverage coverage.xml + rm -fr htmlcov/ rm -fr build/ rm -fr dist/ - rm -fr *.egg-info + rm -fr src/*.egg-info clean_tox: ## clear tox requirements cache rm -fr .tox coverage: clean ## generate and view HTML coverage report - pytest --cov-report html + uv run pytest --cov-report html $(BROWSER)htmlcov/index.html docs: ## generate Sphinx HTML documentation, including API docs - tox -e docs + uv run tox -e docs $(BROWSER)docs/_build/html/index.html -compile-requirements: ## generate the uv.lock file without upgrading packages +compile-requirements: ## update uv.lock for pyproject.toml changes without upgrading packages uv lock upgrade: ## upgrade all packages in uv.lock and sync constraints from edx-lint - uv run --with edx-lint edx_lint write_uv_constraints pyproject.toml + uv run --with 'edx-lint>=6.2' edx_lint write_uv_constraints pyproject.toml uv lock --upgrade +# Regenerates the constraints into a scratch copy and diffs, rather than +# checking `git diff`, so that it reports only this drift and not whatever else +# happens to be uncommitted in the working tree. +check-constraints: ## fail if [tool.uv].constraint-dependencies is out of date + @cp pyproject.toml pyproject.toml.orig + @uv run --with 'edx-lint>=6.2' edx_lint write_uv_constraints pyproject.toml >/dev/null + @if cmp -s pyproject.toml.orig pyproject.toml; then \ + rm -f pyproject.toml.orig; \ + else \ + diff -u pyproject.toml.orig pyproject.toml || true; \ + mv pyproject.toml.orig pyproject.toml; \ + echo ""; \ + echo "[tool.uv].constraint-dependencies is out of date with edx-lint."; \ + echo "Run 'make upgrade' and commit the result."; \ + exit 1; \ + fi + quality: ## check coding style with pycodestyle and pylint - tox -e quality + uv run tox -e quality pii_check: ## check for PII annotations on all Django models - tox -e pii_check + uv run tox -e pii_check requirements: clean_tox ## install development environment requirements uv sync --group dev test: clean ## run tests in the current virtualenv - pytest + uv run pytest diff_cover: test ## find diff lines that need test coverage - diff-cover coverage.xml + uv run diff-cover coverage.xml -test-all: quality pii_check ## run tests on every supported Python/Django combination - tox - tox -e docs +test-all: ## run tests on every supported Python/Django combination, plus quality and docs + uv run tox validate: quality pii_check test ## run tests and quality checks selfcheck: ## check that the Makefile is well-formed @echo "The Makefile is well-formed." - -## Localization targets - -extract_translations: ## extract strings to be translated, outputting .mo files - rm -rf docs/_build - cd sample_plugin && i18n_tool extract --no-segment - -compile_translations: ## compile translation files, outputting .po files for each supported language - cd sample_plugin && i18n_tool generate - -detect_changed_source_translations: - cd sample_plugin && i18n_tool changed - -ifeq ($(OPENEDX_ATLAS_PULL),) -pull_translations: ## Pull translations from Transifex - tx pull -t -a -f --mode reviewed --minimum-perc=1 -else -# Experimental: OEP-58 Pulls translations using atlas -pull_translations: - find sample_plugin/conf/locale -mindepth 1 -maxdepth 1 -type d -exec rm -r {} \; - atlas pull $(OPENEDX_ATLAS_ARGS) translations/sample-plugin/sample_plugin/conf/locale:sample_plugin/conf/locale - python manage.py compilemessages - - @echo "Translations have been pulled via Atlas and compiled." -endif - -dummy_translations: ## generate dummy translation (.po) files - cd sample_plugin && i18n_tool dummy - -build_dummy_translations: extract_translations dummy_translations compile_translations ## generate and compile dummy translation files - -validate_translations: build_dummy_translations detect_changed_source_translations ## validate translations - -install_transifex_client: ## Install the Transifex client - # 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 diff --git a/README.md b/README.md index c26a693..dc81630 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,33 @@ # openedx-search +A search library for the [Open edX](https://openedx.org) platform, supporting +[Typesense](https://typesense.org) and [Meilisearch](https://www.meilisearch.com). -## Testing and quality +**Status: experimental.** This repository is at the very start of its life and +has no functionality yet. The intended scope, and the reasons for it, are laid +out in [ADR 0001: Purpose of This Repo](docs/decisions/0001-purpose-of-this-repo.rst). + +## Development + +This repo uses [uv](https://docs.astral.sh/uv/) for dependency management. ```bash -make requirements # install test deps -make test # pytest -make quality # lint +make requirements # create .venv from uv.lock with all dev dependencies +make test # run the unit tests +make quality # run the linters +make test-all # run everything CI runs, via tox ``` -Tests live in [`tests/`](./tests/). +Run `make help` for the full list of targets. See +[Getting Started](docs/getting_started.rst) for how to change dependencies and +how releases work. + +## License + +The code in this repository is licensed under the AGPL-3.0 unless otherwise +noted. See [LICENSE.txt](LICENSE.txt) for details. + +## Reporting Security Issues + +Please do not report security issues in public. Email security@openedx.org +instead. diff --git a/docs/conf.py b/docs/conf.py index d98b72e..d601a65 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,19 +12,17 @@ serve to show the default. """ import os -import re import sys from datetime import datetime +from importlib.metadata import version as get_version from subprocess import check_call from django import setup as django_setup -from importlib.metadata import version as get_version - REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(REPO_ROOT) -VERSION = get_version('openedx-plugin-sample') +VERSION = get_version('openedx-search') # Configure Django for autodoc usage os.environ['DJANGO_SETTINGS_MODULE'] = 'test_settings' django_setup() @@ -76,17 +74,17 @@ top_level_doc = 'index' # General information about the project. -project = 'openedx_search' +project = 'openedx-search' copyright = f'{datetime.now().year}, Axim Collaborative, Inc.' # pylint: disable=redefined-builtin author = 'Axim Collaborative, Inc.' -project_title = 'openedx_search' +project_title = 'openedx-search' 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": 'sample-plugin', # Repo name + "github_user": "openedx", # Username + "github_repo": 'openedx-search', # Repo name "github_version": "main", # Version "conf_py_path": "/docs/", # Path in the checkout to the docs root } @@ -171,7 +169,7 @@ # documentation. # html_theme_options = { - "repository_url": "https://github.com/openedx/sample-plugin", + "repository_url": "https://github.com/openedx/openedx-search", "repository_branch": 'main', "path_to_docs": "docs/", "home_page_in_toc": True, @@ -208,7 +206,7 @@ # The name for this set of Sphinx documents. # " v documentation" by default. # -# html_title = 'openedx_search v0.1.0' +# html_title = 'openedx-search v0.1.0' # A shorter title for the navigation bar. Default is the same as html_title. # @@ -394,7 +392,7 @@ # dir menu entry, description, category) texinfo_documents = [ (top_level_doc, project_title, documentation_title, - author, project_title, 'A sample backend plugin for the Open edX Platform', + author, project_title, 'A search library for the Open edX Platform', 'Miscellaneous'), ] @@ -503,8 +501,7 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'python': ('https://docs.python.org/3.12', None), - 'django': ('https://docs.djangoproject.com/en/4.2/', 'https://docs.djangoproject.com/en/3.2/_objects/'), - 'model_utils': ('https://django-model-utils.readthedocs.io/en/latest/', None), + 'django': ('https://docs.djangoproject.com/en/5.2/', 'https://docs.djangoproject.com/en/5.2/_objects/'), } @@ -517,11 +514,9 @@ def on_init(app): # pylint: disable=unused-argument """ 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 - # If we are, assemble the path manually - bin_path = os.path.abspath(os.path.join(sys.prefix, 'bin')) - apidoc_path = os.path.join(bin_path, apidoc_path) + # Use the sphinx-apidoc installed alongside the running Python, which may + # not be on PATH (e.g. under `uv run` or Read the Docs). + apidoc_path = os.path.join(os.path.dirname(sys.executable), 'sphinx-apidoc') check_call([apidoc_path, '-o', docs_path, os.path.join(root_path, 'src/openedx_search'), os.path.join(root_path, 'src/openedx_search/migrations')]) diff --git a/docs/getting_started.rst b/docs/getting_started.rst index cec5418..dd11795 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -1,18 +1,48 @@ Getting Started ############### -If you have not already done so, create/activate a `virtualenv`_. Unless otherwise stated, assume all terminal code -below is executed within the virtualenv. +This repository uses `uv`_ to manage its virtualenv and dependencies. Install +it first, then from the repository root run: -.. _virtualenv: https://virtualenvwrapper.readthedocs.org/en/latest/ +.. code-block:: bash + $ make requirements -Install dependencies -******************** -Dependencies can be installed via the command below. +This creates ``.venv/`` from the committed ``uv.lock`` with every development +dependency group installed. Prefix commands with ``uv run`` (for example +``uv run pytest``) to run them in that environment; the ``make`` targets do +this for you. -.. code-block:: bash +.. _uv: https://docs.astral.sh/uv/ - $ make requirements +Changing dependencies +********************* + +Dependencies are declared in ``pyproject.toml``: runtime dependencies under +``[project].dependencies`` and development tools under +``[dependency-groups]``. After editing them, run ``make compile-requirements`` +to update ``uv.lock`` without upgrading anything else, and commit both files. + +``make upgrade`` re-resolves everything to the newest allowed versions and +regenerates ``[tool.uv].constraint-dependencies`` from edx-lint. Do not edit +that list by hand; put repo-specific constraints in +``[tool.edx_lint].uv_constraints`` instead. + +See `How To Manage a uv Dependency Version Matrix`_ for adding or retiring a +Django version from the test matrix. + +.. _How To Manage a uv Dependency Version Matrix: https://docs.openedx.org/en/latest/developers/how-tos/manage-uv-dependency-matrix.html + +Releases +******** +Releases are automated with `python-semantic-release`_. Merging to ``main`` +publishes a new version to PyPI when the merged commits warrant one, so commit +messages must follow the `Conventional Commits`_ format described in +`OEP-51`_: ``feat:`` makes a minor release, ``fix:``, ``perf:`` and +``build:`` make a patch release, and a ``!`` or ``BREAKING CHANGE:`` footer +makes a major release. +.. _python-semantic-release: https://python-semantic-release.readthedocs.io/ +.. _Conventional Commits: https://www.conventionalcommits.org/ +.. _OEP-51: https://docs.openedx.org/projects/openedx-proposals/en/latest/best-practices/oep-0051-bp-conventional-commits.html diff --git a/docs/index.rst b/docs/index.rst index a9ce466..e5beb5f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,12 +1,9 @@ -.. openedx_search documentation top level file, created by - sphinx-quickstart on Fri Apr 11 10:00:56 2025. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +openedx-search +############## -openedx_search -===================== - -A sample backend plugin for the Open edX Platform +A search library for the Open edX platform, supporting Typesense and +Meilisearch. See :doc:`decisions/0001-purpose-of-this-repo` for why it exists +and what it is meant to become. Contents: @@ -18,14 +15,13 @@ Contents: concepts/index how-tos/index testing - internationalization modules decisions references/index Indices and tables -################## +****************** * :ref:`genindex` * :ref:`modindex` diff --git a/docs/internationalization.rst b/docs/internationalization.rst deleted file mode 100644 index ce33bbd..0000000 --- a/docs/internationalization.rst +++ /dev/null @@ -1,49 +0,0 @@ -.. _chapter-i18n: - -Internationalization -#################### -All user-facing text content should be marked for translation. Even if this application is only run in English, our -open source users may choose to use another language. Marking content for translation ensures our users have -this choice. - -Follow the `internationalization coding guidelines`_ in the edX Developer's Guide when developing new features. - -.. _internationalization coding guidelines: https://docs.openedx.org/en/latest/developers/references/developer_guide/internationalization/i18n.html - -Updating Translations -********************* -This project uses `Transifex`_ to translate content. After new features are developed the translation source files -should be pushed to Transifex. Our translation community will translate the content, after which we can retrieve the -translations. - -.. _Transifex: https://www.transifex.com/ - -Pushing source translation files to Transifex requires access to the edx-platform. Request access from the Open Source -Team if you will be pushing translation files. You should also `configure the Transifex client`_ if you have not done so -already. - -.. _configure the Transifex client: https://docs.transifex.com/client/config/ - -The `make` targets listed below can be used to push or pull translations. - -.. list-table:: - :widths: 25 75 - :header-rows: 1 - - * - Target - - Description - * - pull_translations - - Pull translations from Transifex - -Fake Translations -***************** -As you develop features it may be helpful to know which strings have been marked for translation, and which are not. -Use the `fake_translations` make target for this purpose. This target will extract all strings marked for translation, -generate fake translations in the Esperanto (eo) language directory, and compile the translations. - -You can trigger the display of the translations by setting your browser's language to Esperanto (eo), and navigating to -a page on the site. Instead of plain English strings, you should see specially-accented English strings that look -like this: - - Thé Fütüré øf Ønlïné Édüçätïøn Ⱡσяєм ι# Før änýøné, änýwhéré, änýtïmé Ⱡσяєм # - diff --git a/docs/openedx_plugin_sample.rst b/docs/openedx_plugin_sample.rst deleted file mode 100644 index bf321db..0000000 --- a/docs/openedx_plugin_sample.rst +++ /dev/null @@ -1,69 +0,0 @@ -openedx\_plugin\_sample package -=============================== - -Submodules ----------- - -openedx\_plugin\_sample.apps module ------------------------------------ - -.. automodule:: openedx_search.apps - :members: - :show-inheritance: - :undoc-members: - -openedx\_plugin\_sample.models module -------------------------------------- - -.. automodule:: openedx_search.models - :members: - :show-inheritance: - :undoc-members: - -openedx\_plugin\_sample.pipeline module ---------------------------------------- - -.. automodule:: openedx_search.pipeline - :members: - :show-inheritance: - :undoc-members: - -openedx\_plugin\_sample.serializers module ------------------------------------------- - -.. automodule:: openedx_search.serializers - :members: - :show-inheritance: - :undoc-members: - -openedx\_plugin\_sample.signals module --------------------------------------- - -.. automodule:: openedx_search.signals - :members: - :show-inheritance: - :undoc-members: - -openedx\_plugin\_sample.urls module ------------------------------------ - -.. automodule:: openedx_search.urls - :members: - :show-inheritance: - :undoc-members: - -openedx\_plugin\_sample.views module ------------------------------------- - -.. automodule:: openedx_search.views - :members: - :show-inheritance: - :undoc-members: - -Module contents ---------------- - -.. automodule:: openedx_search - :members: - :show-inheritance: - :undoc-members: diff --git a/docs/testing.rst b/docs/testing.rst index 770f10d..3264154 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -3,9 +3,8 @@ Testing ####### -openedx_search has an assortment of test cases and code quality -checks to catch potential problems during development. To run them all in the -version of Python you chose for your virtualenv: +openedx-search has an assortment of test cases and code quality +checks to catch potential problems during development. To run them all: .. code-block:: bash @@ -29,8 +28,8 @@ To run just the code quality checks: $ make quality -To run the unit tests under every supported Python version and the code -quality checks: +To run the unit tests under every supported Django version, plus the code +quality, PII annotation and documentation checks, as CI does: .. code-block:: bash diff --git a/pylintrc b/pylintrc index bd140db..9c00605 100644 --- a/pylintrc +++ b/pylintrc @@ -64,12 +64,12 @@ # SERIOUSLY. # # ------------------------------ -# Generated by edx-lint version: 5.6.0 +# Generated by edx-lint version: 6.2.0 # ------------------------------ [MASTER] ignore = migrations persistent = yes -load-plugins = edx_lint.pylint,pylint_django,pylint_celery +load-plugins = edx_lint.pylint,pylint_django [MESSAGES CONTROL] enable = @@ -257,6 +257,8 @@ enable = deprecated-pragma, unrecognized-inline-option, useless-suppression, + + pii-invalid-no-pii-annotation, disable = bad-indentation, broad-exception-raised, @@ -387,4 +389,9 @@ int-import-graph = [EXCEPTIONS] overgeneral-exceptions = builtins.Exception -# 79853056a08a5009db4a6e9a8ca00c194036ac5a +[PII] +pii-terms = + email, + username + +# c5e1b3f5f428a52db26a79de543e11162069fc9b diff --git a/pylintrc_tweaks b/pylintrc_tweaks index 7b6eb35..f904f9c 100644 --- a/pylintrc_tweaks +++ b/pylintrc_tweaks @@ -1,7 +1,7 @@ # pylintrc tweaks for use with edx_lint. [MASTER] ignore = migrations -load-plugins = edx_lint.pylint,pylint_django,pylint_celery +load-plugins = edx_lint.pylint,pylint_django [MESSAGES CONTROL] disable+= diff --git a/pyproject.toml b/pyproject.toml index 9a29af7..d33677e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,33 +24,43 @@ classifiers = [ keywords = [ "Python", "Open edX", + "search", + "Meilisearch", + "Typesense", ] dynamic = ["readme", "version"] +# Unconstrained on purpose. The Open edX platform pins these itself, and a +# library that narrows them causes resolution conflicts when it is installed +# into openedx-platform. `>=0` rather than a bare name because Renovate skips +# any dependency with no version specifier at all, so bare names would never +# get update PRs. Please don't "tidy" these into real floors without a reason +# to; a floor we cannot justify is a compatibility claim we cannot back up. dependencies = [ - "Django", - "openedx-atlas", + "Django>=0", ] [project.urls] Homepage = "https://github.com/openedx/openedx-search" Repository = "https://github.com/openedx/openedx-search" +Issues = "https://github.com/openedx/openedx-search/issues" [dependency-groups] +# `>=0` here for the same reason as [project].dependencies above: Renovate +# skips dependencies with no version specifier. The two Django entries below +# are the real exception -- they define the test matrix. test-base = [ - "pytest-cov", - "pytest-django", - "code-annotations", - "edx-django-utils", - "django-extensions", + "pytest-cov>=0", + "pytest-django>=0", + "code-annotations>=0", ] # Current default Django version used by quality, docs, and the default test # matrix entry. When adding or retiring a Django version from the matrix, see -# docs/how-tos/adding-a-matrix-dependency.rst for the full process. +# https://docs.openedx.org/en/latest/developers/how-tos/manage-uv-dependency-matrix.html test = [ {include-group = "test-base"}, - "Django>=5.0,<6.0", + "Django>=5.2,<6.0", ] django60 = [ {include-group = "test-base"}, @@ -58,28 +68,29 @@ django60 = [ ] quality = [ {include-group = "test"}, - "edx-lint", - "isort", - "pycodestyle", - "pydocstyle", + # A real floor, not a placeholder: `edx_lint write_uv_constraints`, used by + # `make upgrade` and `make check-constraints`, was added in 6.2.0. + "edx-lint>=6.2", + "isort>=0", + "pycodestyle>=0", + "pydocstyle>=0", ] doc = [ {include-group = "test"}, - "doc8", - "sphinx-book-theme", - "twine", - "build", - "Sphinx", + "doc8>=0", + "sphinx-book-theme>=0", + "twine>=0", + "build>=0", + "Sphinx>=0", ] ci = [ - "tox", - "tox-uv", + "tox>=0", + "tox-uv>=1", ] dev = [ {include-group = "quality"}, {include-group = "ci"}, - "diff-cover", - "edx-i18n-tools", + "diff-cover>=0", ] [tool.setuptools] @@ -100,28 +111,35 @@ where = ["src"] build_command = "pip install build && SETUPTOOLS_SCM_PRETEND_VERSION=$NEW_VERSION python -m build" [tool.semantic_release.commit_parser_options] -# Because this repo is meant to be an example, docs changes are relevant -# feature changes and so should produces new releases. -minor_tags = ["feat", "docs"] +minor_tags = ["feat"] # If we change the build system, we want to publish it so that updates to the # build and release system are validated close to when they are merged. patch_tags = ["fix", "perf", "build"] [tool.setuptools_scm] -# The root for the gith repo is one directory up. -root = ".." version_scheme = 'only-version' local_scheme = 'no-local-version' -# Dummy version number for when ../.git isn't accessible. +# Dummy version number for when .git isn't accessible. # This happens, for example, when bind-mounting into a Docker image. fallback_version = "0.0.0.dev0" [tool.uv] +# Supply-chain cooling-off period: never resolve to a release younger than +# this. A hijacked release is usually yanked within a few days, so waiting is +# cheap insurance. uv records the window in uv.lock and it cannot invalidate +# an existing lock as time passes; raising the number of days can, though, if +# the new cutoff excludes something already locked. +# +# To let one urgent security fix through without dropping the window for +# everything, add a temporary exemption rather than changing this value: +# exclude-newer-package = { the-package = false } +exclude-newer = "7 days" + # Each entry lists groups with mutually exclusive version requirements so uv can # produce a single uv.lock that contains a separate resolution for each. Add a # new pair here whenever you add a legacy-version group to [dependency-groups]. -# See docs/how-tos/adding-a-matrix-dependency.rst for the full process. +# See https://docs.openedx.org/en/latest/developers/how-tos/manage-uv-dependency-matrix.html conflicts = [ [{group = "test"}, {group = "django60"}], ] @@ -133,6 +151,9 @@ conflicts = [ constraint-dependencies = [ "Django<7.0", "elasticsearch<7.14.0", + "social-auth-app-django<6.0.0", + "social-auth-core<5.0.0", + "pip<26.2.1", ] [tool.edx_lint] @@ -142,3 +163,9 @@ constraint-dependencies = [ uv_constraints = [ "Django<7.0", # this repo tests against Django 6.x; overrides the global Django<6.0 constraint ] + +[tool.isort] +include_trailing_comma = true +line_length = 120 +multi_line_output = 3 +skip = ["migrations"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index da0309e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[isort] -include_trailing_comma = True -indent = ' ' -line_length = 120 -multi_line_output = 3 -skip= - migrations diff --git a/src/openedx_search/__init__.py b/src/openedx_search/__init__.py index 20b9e8a..a2c0039 100644 --- a/src/openedx_search/__init__.py +++ b/src/openedx_search/__init__.py @@ -1,5 +1,5 @@ """ -A sample backend plugin for the Open edX Platform. +A search library for the Open edX platform, supporting Typesense and Meilisearch. """ from importlib.metadata import version as get_version diff --git a/src/openedx_search/apps.py b/src/openedx_search/apps.py index 0ea12be..32a4744 100644 --- a/src/openedx_search/apps.py +++ b/src/openedx_search/apps.py @@ -7,8 +7,9 @@ class OpenEdxSearchConfig(AppConfig): """ - Django App Plugin configuration for Open edX platform integration. + Django app configuration for openedx_search. """ default_auto_field = "django.db.models.BigAutoField" name = "openedx_search" + verbose_name = "Open edX Search" diff --git a/src/openedx_search/conf/locale/config.yaml b/src/openedx_search/conf/locale/config.yaml deleted file mode 100644 index bf2539e..0000000 --- a/src/openedx_search/conf/locale/config.yaml +++ /dev/null @@ -1,85 +0,0 @@ -# Configuration for i18n workflow. - -locales: - - en # English - Source Language - - am # Amharic - - ar # Arabic - - az # Azerbaijani - - bg_BG # Bulgarian (Bulgaria) - - bn_BD # Bengali (Bangladesh) - - bn_IN # Bengali (India) - - bs # Bosnian - - ca # Catalan - - ca@valencia # Catalan (Valencia) - - cs # Czech - - cy # Welsh - - da # Danish - - de_DE # German (Germany) - - el # Greek - - en # English - - en_GB # English (United Kingdom) - # Don't pull these until we figure out why pages randomly display in these locales, - # when the user's browser is in English and the user is not logged in. - # - en@lolcat # LOLCAT English - # - en@pirate # Pirate English - - es_419 # Spanish (Latin America) - - es_AR # Spanish (Argentina) - - es_EC # Spanish (Ecuador) - - es_ES # Spanish (Spain) - - es_MX # Spanish (Mexico) - - es_PE # Spanish (Peru) - - et_EE # Estonian (Estonia) - - eu_ES # Basque (Spain) - - fa # Persian - - fa_IR # Persian (Iran) - - fi_FI # Finnish (Finland) - - fil # Filipino - - fr # French - - gl # Galician - - gu # Gujarati - - he # Hebrew - - hi # Hindi - - hr # Croatian - - hu # Hungarian - - hy_AM # Armenian (Armenia) - - id # Indonesian - - it_IT # Italian (Italy) - - ja_JP # Japanese (Japan) - - kk_KZ # Kazakh (Kazakhstan) - - km_KH # Khmer (Cambodia) - - kn # Kannada - - ko_KR # Korean (Korea) - - lt_LT # Lithuanian (Lithuania) - - ml # Malayalam - - mn # Mongolian - - mr # Marathi - - ms # Malay - - nb # Norwegian Bokmål - - ne # Nepali - - nl_NL # Dutch (Netherlands) - - or # Oriya - - pl # Polish - - pt_BR # Portuguese (Brazil) - - pt_PT # Portuguese (Portugal) - - ro # Romanian - - ru # Russian - - si # Sinhala - - sk # Slovak - - sl # Slovenian - - sq # Albanian - - sr # Serbian - - ta # Tamil - - te # Telugu - - th # Thai - - tr_TR # Turkish (Turkey) - - uk # Ukrainian - - ur # Urdu - - uz # Uzbek - - vi # Vietnamese - - zh_CN # Chinese (China) - - zh_HK # Chinese (Hong Kong) - - zh_TW # Chinese (Taiwan) - -# The locales used for fake-accented English, for testing. -dummy_locales: - - eo diff --git a/src/openedx_search/migrations/0001_initial.py b/src/openedx_search/migrations/0001_initial.py deleted file mode 100644 index 2f4d98d..0000000 --- a/src/openedx_search/migrations/0001_initial.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 5.2.13 on 2026-05-14 01:13 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('openedx_catalog', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='CourseArchiveStatus', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_archived', models.BooleanField(db_index=True, default=False, help_text='Whether the course is archived.')), - ('archive_date', models.DateTimeField(blank=True, help_text='The date and time when the course was archived.', null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('course_run', models.ForeignKey(help_text='The course run that this archive status is for.', on_delete=django.db.models.deletion.CASCADE, related_name='archive_statuses', to='openedx_catalog.courserun')), - ('user', models.ForeignKey(help_text='The user who this archive status is for.', on_delete=django.db.models.deletion.CASCADE, related_name='course_archive_statuses', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Course Archive Status', - 'verbose_name_plural': 'Course Archive Statuses', - 'ordering': ['-updated_at'], - 'constraints': [models.UniqueConstraint(fields=('course_run', 'user'), name='unique_user_course_archive_status')], - }, - ), - ] diff --git a/src/openedx_search/models.py b/src/openedx_search/models.py index e69de29..199c104 100644 --- a/src/openedx_search/models.py +++ b/src/openedx_search/models.py @@ -0,0 +1,3 @@ +""" +Database models for openedx_search. +""" diff --git a/src/openedx_search/settings/common.py b/src/openedx_search/settings/common.py deleted file mode 100644 index 9f6cf6a..0000000 --- a/src/openedx_search/settings/common.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Common settings for the openedx_search application. - -This module demonstrates how Django App Plugins integrate with the platform's -settings system. Plugin settings are merged with the main settings during -platform initialization. - -Plugin Settings Integration: -The plugin_settings function is called during Django startup and receives -the main settings object. You can modify this object to add plugin-specific -configuration that integrates seamlessly with the platform. - -Official Documentation: -- Plugin Settings: - https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html#plugin-settings -- Django Settings: https://docs.djangoproject.com/en/stable/topics/settings/ - -Settings Organization: -- common.py: Settings for all environments -- production.py: Production-specific overrides -- test.py: Test environment optimizations - -Integration Points: -- OPEN_EDX_FILTERS_CONFIG: Register filters with the platform -- API rate limiting and throttling configuration -- Database connection settings for plugin models -- External service integration parameters -- Feature flags and environment-specific toggles -""" # noqa: E501 - -import logging - -logger = logging.getLogger(__name__) - - -def plugin_settings(settings): - """ - Configure plugin-specific Django settings. - - This function is called during Django startup to merge plugin settings - with the main platform configuration. All settings added here become - available throughout the Django application. - - Args: - settings (dict): Main Django settings object to modify - - Common Settings Patterns: - - # Plugin-specific configuration - settings.SAMPLE_PLUGIN_API_RATE_LIMIT = "60/minute" - settings.SAMPLE_PLUGIN_ARCHIVE_RETENTION_DAYS = 365 - - # External service integration - settings.SAMPLE_PLUGIN_EXTERNAL_API_URL = "https://api.example.com" - settings.SAMPLE_PLUGIN_API_KEY = "your-api-key" - - # Feature flags - settings.SAMPLE_PLUGIN_ENABLE_ARCHIVING = True - settings.SAMPLE_PLUGIN_ENABLE_NOTIFICATIONS = False - - Environment-Specific Settings: - Different environment files can override these settings: - - production.py: Stricter rate limits, external API endpoints - - test.py: Faster timeouts, mock services, in-memory databases - - development.py: Debug logging, local service endpoints - - Security Considerations: - - Never commit API keys or secrets to version control - - Use environment variables for sensitive configuration - - Validate setting values to prevent configuration errors - """ - # Plugin is configured but no additional settings needed for this basic example - # Uncomment and modify the examples below for your use case: - - # Plugin-specific configuration - # settings.SAMPLE_PLUGIN_API_RATE_LIMIT = "60/minute" - # settings.SAMPLE_PLUGIN_ARCHIVE_RETENTION_DAYS = 365 - - # Register Open edX Filters (additive approach) - _configure_openedx_filters(settings) - - -def _configure_openedx_filters(settings): - """ - Configure Open edX Filters for the sample plugin. - - This function demonstrates the proper way to register filters by: - 1. Preserving existing filter configuration from other plugins - 2. Adding our filter configuration additively - 3. Avoiding duplicate pipeline steps - 4. Logging configuration state for debugging - - Args: - settings (dict): Django settings object - """ - # Get existing filter configuration (may be from other plugins or platform) - filters_config = getattr(settings, 'OPEN_EDX_FILTERS_CONFIG', {}) - - # Filter we want to register - filter_name = "org.openedx.learning.home.courserun.api.rendered.started.v1" - our_pipeline_step = "openedx_search.pipeline.AddArchiveStatusToLearnerHomeCourseRun" - - # Check if this filter already has configuration - if filter_name in filters_config: - logger.debug(f"Filter {filter_name} already configured, adding our pipeline step") - - # Get existing pipeline steps - existing_pipeline = filters_config[filter_name].get("pipeline", []) - - # Check if our pipeline step is already registered - if our_pipeline_step in existing_pipeline: - logger.info( - f"Pipeline step {our_pipeline_step} already registered for filter {filter_name}. " - "This may indicate the plugin is being loaded multiple times or another plugin " - "has registered the same pipeline step." - ) - else: - # Add our pipeline step to existing configuration - existing_pipeline.append(our_pipeline_step) - filters_config[filter_name]["pipeline"] = existing_pipeline - logger.debug(f"Added {our_pipeline_step} to existing filter configuration") - else: - # Create new filter configuration - logger.debug(f"Creating new filter configuration for {filter_name}") - filters_config[filter_name] = { - "pipeline": [our_pipeline_step], - "fail_silently": False, - } - - # Update the settings object - settings.OPEN_EDX_FILTERS_CONFIG = filters_config - - logger.debug( - f"Final filter configuration for {filter_name}: " - f"{filters_config.get(filter_name, {})}" - ) diff --git a/src/openedx_search/settings/production.py b/src/openedx_search/settings/production.py deleted file mode 100644 index dcd9e96..0000000 --- a/src/openedx_search/settings/production.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Production settings for the openedx_search application. -""" - -from openedx_search.settings.common import plugin_settings as common_settings - - -def plugin_settings(settings): - """ - Set up production-specific settings. - - Args: - settings (dict): Django settings object - """ - # Apply common settings - common_settings(settings) diff --git a/src/openedx_search/settings/test.py b/src/openedx_search/settings/test.py deleted file mode 100644 index 635f5d1..0000000 --- a/src/openedx_search/settings/test.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Test settings for the openedx_search application. -""" - -from openedx_search.settings.common import plugin_settings as common_settings - - -def plugin_settings(settings): - """ - Set up test-specific settings. - - Args: - settings (dict): Django settings object - """ - - # Apply common settings - common_settings(settings) diff --git a/src/openedx_search/views.py b/src/openedx_search/views.py deleted file mode 100644 index eb1c6b9..0000000 --- a/src/openedx_search/views.py +++ /dev/null @@ -1,268 +0,0 @@ -""" -Views for the openedx_search app. -""" - -import logging - -from django.utils import timezone -from django_filters import rest_framework as django_filters -from django_filters.rest_framework import DjangoFilterBackend -from opaque_keys import InvalidKeyError -from opaque_keys.edx.keys import CourseKey -from rest_framework import filters, permissions, viewsets -from rest_framework.exceptions import PermissionDenied, ValidationError -from rest_framework.pagination import PageNumberPagination -from rest_framework.throttling import UserRateThrottle - -from openedx_search.models import CourseArchiveStatus -from openedx_search.serializers import CourseArchiveStatusSerializer - -logger = logging.getLogger(__name__) - - -class IsOwnerOrStaffSuperuser(permissions.BasePermission): - """ - Custom permission to only allow owners of an object or staff/superusers to view or edit it. - """ - - def has_permission(self, request, view): - """ - Return True if permission is granted to the view. - """ - # Allow authenticated users to list and create - return request.user and request.user.is_authenticated - - def has_object_permission(self, request, view, obj): - """ - Return True if permission is granted to the object. - """ - # Allow if the object belongs to the requesting user - if obj.user == request.user: - return True - - # Allow staff users and superusers - if request.user.is_staff or request.user.is_superuser: - return True - - return False - - -class CourseArchiveStatusPagination(PageNumberPagination): - """ - Pagination class for CourseArchiveStatus. - """ - - page_size = 20 - page_size_query_param = "page_size" - max_page_size = 100 - - -class CourseArchiveStatusThrottle(UserRateThrottle): - """ - Throttle for the CourseArchiveStatus API. - """ - - rate = "60/minute" - - -class CourseArchiveStatusFilterSet(django_filters.FilterSet): - """ - FilterSet for CourseArchiveStatus. - - The model stores a FK to CourseRun, but the public API filters and orders - by the course_key string (never by the internal CourseRun PK). - """ - - # Map ?course_id=course-v1:... onto the FK's course_key column. - course_id = django_filters.CharFilter(field_name="course_run__course_key") - - # Expose ?ordering=course_id (and other fields) without leaking the - # double-underscore FK lookup path. - ordering = django_filters.OrderingFilter( - fields=( - ("course_run__course_key", "course_id"), - ("user", "user"), - ("is_archived", "is_archived"), - ("archive_date", "archive_date"), - ("created_at", "created_at"), - ("updated_at", "updated_at"), - ) - ) - - class Meta: - """ - FilterSet Meta options for CourseArchiveStatus. - """ - - model = CourseArchiveStatus - fields = ["course_id", "user", "is_archived"] - - -class CourseArchiveStatusViewSet(viewsets.ModelViewSet): - """ - API viewset for CourseArchiveStatus. - - Allows users to view their own course archive statuses and staff/superusers to view all. - Pagination is applied with a default page size of 20 (max 100). - Filtering is available on course_id, user, and is_archived fields. - Ordering is available on all fields. - """ - - serializer_class = CourseArchiveStatusSerializer - permission_classes = [IsOwnerOrStaffSuperuser] - pagination_class = CourseArchiveStatusPagination - throttle_classes = [ - CourseArchiveStatusThrottle, - ] - filter_backends = [DjangoFilterBackend, filters.OrderingFilter] - filterset_class = CourseArchiveStatusFilterSet - ordering = ["-updated_at"] - - def get_queryset(self): - """ - Return the queryset for this viewset. - - Regular users can only see their own records. - Staff and superusers can see all records but with optimized queries. - """ - user = self.request.user - - # Validate query parameters to prevent injection - self._validate_query_params() - - # Always use select_related to avoid N+1 queries when accessing - # related user and course_run (for course_key) fields. - base_queryset = CourseArchiveStatus.objects.select_related("user", "course_run") - - if user.is_staff or user.is_superuser: - return base_queryset - - # Regular users only see their own records - return base_queryset.filter(user=user) - - def _validate_query_params(self): - """ - Validate query parameters to prevent injection. - """ - # Example validation for course_id format - course_id = self.request.query_params.get("course_id") - if course_id and not self._is_valid_course_id(course_id): - logger.warning( - "Invalid course_id in request: %s, user: %s", - course_id, - self.request.user.username, - ) - raise ValidationError({"course_id": "Invalid course ID format."}) - - def _is_valid_course_id(self, course_id): - """ - Check if the course_id is in a valid format. - - This is a basic implementation - in production, you might use a more - sophisticated validator from the edx-platform. - """ - try: - CourseKey.from_string(course_id) - return True - except InvalidKeyError: - return False - - def perform_create(self, serializer): - """ - Perform creation of a new CourseArchiveStatus. - - Validates permission for user override and sets archive_date if needed. - """ - # Check if user was explicitly provided and differs from current user - if "user" in self.request.data: - requested_user_id = self.request.data["user"] - if requested_user_id != self.request.user.id and not ( - self.request.user.is_staff or self.request.user.is_superuser - ): - logger.warning( - "Permission denied: User %s tried to create a record for user %s", - self.request.user.username, - requested_user_id, - ) - raise PermissionDenied( - "You do not have permission to create records for other users." - ) - - # Set archive_date if is_archived is True - data = {} - if serializer.validated_data.get("is_archived", False): - data["archive_date"] = timezone.now() - - # Create the record - instance = serializer.save(**data) - - # Log at debug level for normal operation - logger.debug( - "CourseArchiveStatus created: course_id=%s, user=%s, is_archived=%s", - instance.course_run.course_key, - instance.user.username, - instance.is_archived, - ) - - return instance - - def perform_update(self, serializer): - """ - Perform update of an existing CourseArchiveStatus. - - Validates permission for user override and updates archive_date if needed. - """ - instance = serializer.instance - - # Check if user was explicitly provided and differs from current user - if "user" in self.request.data: - requested_user_id = self.request.data["user"] - if requested_user_id != self.request.user.id and not ( - self.request.user.is_staff or self.request.user.is_superuser - ): - logger.warning( - "Permission denied: User %s tried to update a record for user %s", - self.request.user.username, - requested_user_id, - ) - raise PermissionDenied( - "You do not have permission to update records for other users." - ) - - # Handle archive_date if is_archived changes - data = {} - if "is_archived" in serializer.validated_data: - # If changing from not archived to archived - if serializer.validated_data["is_archived"] and not instance.is_archived: - data["archive_date"] = timezone.now() - # If changing from archived to not archived - elif not serializer.validated_data["is_archived"] and instance.is_archived: - data["archive_date"] = None - - # Update the record - updated_instance = serializer.save(**data) - - # Log at debug level - logger.debug( - "CourseArchiveStatus updated: course_id=%s, user=%s, is_archived=%s", - updated_instance.course_run.course_key, - updated_instance.user.username, - updated_instance.is_archived, - ) - - return updated_instance - - def perform_destroy(self, instance): - """ - Perform deletion of an existing CourseArchiveStatus. - """ - # Log at debug level before deletion - logger.debug( - "CourseArchiveStatus deleted: course_id=%s, user=%s, by=%s", - instance.course_run.course_key, - instance.user.username, - self.request.user.username, - ) - - # Delete the instance - return super().perform_destroy(instance) diff --git a/test_settings.py b/test_settings.py index c11e30b..351e5cc 100644 --- a/test_settings.py +++ b/test_settings.py @@ -5,16 +5,6 @@ Django applications, so these settings will not be used. """ -from os.path import abspath, dirname, join - - -def root(*args): - """ - Get the absolute path of the given path relative to the project root. - """ - return join(abspath(dirname(__file__)), *args) - - DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", @@ -32,19 +22,15 @@ def root(*args): "django.contrib.contenttypes", "django.contrib.messages", "django.contrib.sessions", - "rest_framework", - "django_filters", - "django_extensions", -] - -LOCALE_PATHS = [ - root("openedx_search", "conf", "locale"), + "openedx_search", ] ROOT_URLCONF = "tests.urls" SECRET_KEY = "insecure-secret-key" +USE_TZ = True + MIDDLEWARE = ( "django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -65,27 +51,6 @@ def root(*args): } ] -REST_FRAMEWORK = { - "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.IsAuthenticated", - ], - "DEFAULT_AUTHENTICATION_CLASSES": [ - "rest_framework.authentication.SessionAuthentication", - ], - "DEFAULT_FILTER_BACKENDS": [ - "django_filters.rest_framework.DjangoFilterBackend", - "rest_framework.filters.OrderingFilter", - ], - "DEFAULT_THROTTLE_CLASSES": [ - "rest_framework.throttling.AnonRateThrottle", - "rest_framework.throttling.UserRateThrottle", - ], - "DEFAULT_THROTTLE_RATES": { - "anon": "20/hour", - "user": "100/hour", - }, -} - LOGGING = { "version": 1, "disable_existing_loggers": False, diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..2d84c0c --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,18 @@ +""" +Smoke tests for the openedx_search package and its Django app. +""" + +from django.apps import apps + +import openedx_search + + +def test_version_is_set(): + """The package exposes the version it was installed with.""" + assert openedx_search.__version__ + + +def test_app_is_installed(): + """The Django app loads under its expected label.""" + app_config = apps.get_app_config("openedx_search") + assert app_config.verbose_name == "Open edX Search" diff --git a/tests/urls.py b/tests/urls.py index e69de29..fcfd7f7 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -0,0 +1,8 @@ +""" +URL configuration for tests. + +This app has no views yet, but Django's system checks require ROOT_URLCONF to +point at a module that defines ``urlpatterns``. +""" + +urlpatterns = [] diff --git a/tox.ini b/tox.ini index a8b1612..57ade01 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py312-django{52,60},docs,quality,pii_check +envlist = django{52,60},docs,quality,pii_check requires = tox-uv>=1 @@ -34,7 +34,7 @@ match-dir = (?!migrations) [pytest] DJANGO_SETTINGS_MODULE = test_settings addopts = --cov openedx_search --cov tests --cov-report term-missing --cov-report xml -norecursedirs = .* docs requirements site-packages +norecursedirs = .* docs site-packages [testenv] runner = uv-venv-lock-runner @@ -60,6 +60,7 @@ allowlist_externals = commands = doc8 --ignore-path docs/_build docs rm -f docs/openedx_search.rst + rm -f docs/openedx_search.*.rst rm -f docs/modules.rst make -e -C docs clean make -e -C docs html @@ -76,12 +77,15 @@ allowlist_externals = touch commands = touch tests/__init__.py - pylint src/openedx_search tests test_utils manage.py + pylint src/openedx_search tests test_utils manage.py test_settings.py rm tests/__init__.py pycodestyle src/openedx_search tests manage.py pydocstyle src/openedx_search tests manage.py isort --check-only --diff tests test_utils src/openedx_search manage.py test_settings.py make selfcheck + # Renovate cannot run `edx_lint write_uv_constraints` itself, so instead of + # regenerating the constraints on a schedule we fail here when they drift. + make check-constraints [testenv:pii_check] runner = uv-venv-lock-runner diff --git a/uv.lock b/uv.lock index db856af..b1b428d 100644 --- a/uv.lock +++ b/uv.lock @@ -15,10 +15,17 @@ conflicts = [[ { package = "openedx-search", group = "django60" }, ]] +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P7D" + [manifest] constraints = [ { name = "django", specifier = "<7.0" }, { name = "elasticsearch", specifier = "<7.14.0" }, + { name = "pip", specifier = "<26.2.1" }, + { name = "social-auth-app-django", specifier = "<6.0.0" }, + { name = "social-auth-core", specifier = "<5.0.0" }, ] [[package]] @@ -119,7 +126,7 @@ name = "cffi" version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy' or (extra == 'group-14-openedx-search-dev' and extra == 'group-14-openedx-search-django60') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-doc') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-quality') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-test')" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } wheels = [ @@ -604,8 +611,8 @@ name = "django" version = "5.2.17" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asgiref", marker = "extra == 'group-14-openedx-search-dev' or extra != 'group-14-openedx-search-django60' or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-doc') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-quality') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-test')" }, - { name = "sqlparse", marker = "extra == 'group-14-openedx-search-dev' or extra != 'group-14-openedx-search-django60' or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-doc') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-quality') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-test')" }, + { name = "asgiref" }, + { name = "sqlparse" }, { name = "tzdata", marker = "(sys_platform == 'win32' and extra == 'group-14-openedx-search-dev') or (sys_platform == 'win32' and extra != 'group-14-openedx-search-django60') or (extra == 'group-14-openedx-search-dev' and extra == 'group-14-openedx-search-django60') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-doc') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-quality') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-test')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/d8/43e9d000519adceb189620b6869ff88031e046df91c2e9da72f8f6918399/django-5.2.17.tar.gz", hash = "sha256:9d4d93be539a18ab80d058eb515900e10951e04c537c5a6b394fc49528d3251f", size = 10889740, upload-time = "2026-08-04T15:04:03.173Z" } @@ -627,45 +634,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/9c/ce847620134cfab903e75690c498af73b46abbede2912ea89bd76d5c1e76/django-6.1-py3-none-any.whl", hash = "sha256:6c132cd980c9392b06807d4ca52d72530d631dc65a85d9dacede00a780cefbbe", size = 8417399, upload-time = "2026-08-05T19:21:47.285Z" }, ] -[[package]] -name = "django-crum" -version = "0.7.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django", version = "5.2.17", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-14-openedx-search-dev' or extra != 'group-14-openedx-search-django60' or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-doc') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-quality') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-test')" }, - { name = "django", version = "6.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-14-openedx-search-django60'" }, -] -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-extensions" -version = "4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django", version = "5.2.17", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-14-openedx-search-dev' or extra != 'group-14-openedx-search-django60' or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-doc') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-quality') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-test')" }, - { name = "django", version = "6.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-14-openedx-search-django60'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/b3/ed0f54ed706ec0b54fd251cc0364a249c6cd6c6ec97f04dc34be5e929eac/django_extensions-4.1.tar.gz", hash = "sha256:7b70a4d28e9b840f44694e3f7feb54f55d495f8b3fa6c5c0e5e12bcb2aa3cdeb", size = 283078, upload-time = "2025-04-11T01:15:39.617Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/96/d967ca440d6a8e3861120f51985d8e5aec79b9a8bdda16041206adfe7adc/django_extensions-4.1-py3-none-any.whl", hash = "sha256:0699a7af28f2523bf8db309a80278519362cd4b6e1fd0a8cd4bf063e1e023336", size = 232980, upload-time = "2025-04-11T01:15:37.701Z" }, -] - -[[package]] -name = "django-waffle" -version = "5.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django", version = "5.2.17", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-14-openedx-search-dev' or extra != 'group-14-openedx-search-django60' or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-doc') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-quality') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-test')" }, - { name = "django", version = "6.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-14-openedx-search-django60'" }, -] -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 = "doc8" version = "2.0.0" @@ -690,41 +658,6 @@ 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-django-utils" -version = "8.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "django", version = "5.2.17", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-14-openedx-search-dev' or extra != 'group-14-openedx-search-django60' or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-doc') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-quality') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-test')" }, - { name = "django", version = "6.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-14-openedx-search-django60'" }, - { name = "django-crum" }, - { name = "django-waffle" }, - { name = "psutil" }, - { name = "pynacl" }, - { name = "stevedore" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/56/d3d55bc595d5b67770e31223c126dd0eddbadd1971ddc763eac82d042a69/edx_django_utils-8.0.2.tar.gz", hash = "sha256:9176d91a588332807453cb4b80815c57eca847e4765384238f397a4dd25ed424", size = 110152, upload-time = "2026-08-28T20:31:15.16Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/9c/90cc19dce1a20cbadbcd32696f55d54a021042d8bc02410ac2b8ac6ccfa2/edx_django_utils-8.0.2-py2.py3-none-any.whl", hash = "sha256:7c77c0a3b97086a928b4989d7cfea0f366e32364dfd356a09e102e2132855c2e", size = 121553, upload-time = "2026-08-28T20:31:13.769Z" }, -] - -[[package]] -name = "edx-i18n-tools" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django", version = "5.2.17", source = { registry = "https://pypi.org/simple" } }, - { name = "lxml", extra = ["html-clean"], marker = "extra == 'group-14-openedx-search-dev' or extra != 'group-14-openedx-search-django60' or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-doc') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-quality') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-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.2.0" @@ -902,133 +835,6 @@ 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 = "lxml" -version = "6.1.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/ad/28ecd7cb894d172f3c9c80a075eeeb2017ac62e3632cee05a5f9493547eb/lxml-6.1.3.tar.gz", hash = "sha256:45222d94ddd511536f3b2f7d9deae3b2339b4ce0f075f1ca25703b07cad9dd21", size = 4211198, upload-time = "2026-09-02T14:48:02.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/1f/a180b57d9eeabaab77f9d5aa30356898ea749c4795596a8f66d1eb6bef2e/lxml-6.1.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c0710ac085a157b593c38fbcacd950f15c4afa8e2057527185875ab302752bc", size = 8602094, upload-time = "2026-09-02T14:47:26.054Z" }, - { url = "https://files.pythonhosted.org/packages/a8/25/070c92013a1c029a602b03560d68772313d918268667fa993da7961759c9/lxml-6.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:623c8799c17128753c65699f1c3aa32402657393a9ad6db09ed8b98ddf76611d", size = 4638308, upload-time = "2026-09-02T14:47:29.587Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1c/722e88883173097a1a375153e3c2447eba3060d0231522cf6596e99f4195/lxml-6.1.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f683dc6300317700025e41d89a43e0276692ded16113a3c43eab704d605c58e5", size = 4939696, upload-time = "2026-09-02T14:47:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/db/36/aa413bc214dc4f785ad2b2ddd8cc99aae7062d49ab155e91e6011af00daf/lxml-6.1.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379f8a75cf6eb7eef0af074b55f49ab73b868388a98de14646abcdfa4564bb11", size = 5105247, upload-time = "2026-09-02T14:47:36.734Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a0/a1f7f1313795bfec67b77f01ef3b1128d49f2d7f66a8413fa55d47f4e25f/lxml-6.1.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b37772102d44bb6628186accca3a121b1fa3a6b3d97518a8c29a5229ca4c0d0a", size = 5011915, upload-time = "2026-09-02T14:47:39.846Z" }, - { url = "https://files.pythonhosted.org/packages/b9/78/840e7e3f1d0cc7a5cfac5d8505b97e25b6427fd774ac4bae672aaebfb4b5/lxml-6.1.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddcf547bea2aee967d6a77779376a45e77e610e8465147a1f3d7e20d539d6e32", size = 5638175, upload-time = "2026-09-02T14:47:43.644Z" }, - { url = "https://files.pythonhosted.org/packages/0a/20/e022dbc6b4753a9bc9fc5fb28a27163430c1731b9913997f6544c1b2518c/lxml-6.1.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:909f4e927bb051f7740d6367285fc60cdcfdaf0258c2dba4ff5ba7eadadc250c", size = 5244675, upload-time = "2026-09-02T14:47:47.635Z" }, - { url = "https://files.pythonhosted.org/packages/99/83/82cde81d2b5eb38d1539fdfdf318abdd014a7e604f4df01c9cd3deb18f2a/lxml-6.1.3-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:a5c18810318303ce9afb3f95e2ddb54834f96fa699a8600433fd5a93dcf44c56", size = 5358205, upload-time = "2026-09-02T14:47:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/d2/a1/f3b057371c8cb29f2a9c9c44ea320592446e40b74a4b0af68c3d8e65bc73/lxml-6.1.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:3e42265103fb385d8642a78672edf376c6f7e1d3598a7a4f9cb1278f2f6b5f6f", size = 4704495, upload-time = "2026-09-02T14:47:53.251Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a4/230eb28be5d412152ffc3c679b51fe1aeede5a53f3a8eb6e9748f2f4754f/lxml-6.1.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:21402998e4b78e7cce237d2788841aaa21ac9a4d1574d04dc2d12ee41ae807b5", size = 5255117, upload-time = "2026-09-02T14:47:55.963Z" }, - { url = "https://files.pythonhosted.org/packages/a3/18/1969f56763af24ce42ea156007b0b2d73fddea552e283b2010416394f0f4/lxml-6.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:38fc4e4e4e084e0bd491949482527d406788045c546d4f8789e93fc527b91385", size = 5054424, upload-time = "2026-09-02T14:47:58.131Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d4/2a90acc1f6fabaa3a8db9340437822bd8d041b205d626a4b3e8621aaa390/lxml-6.1.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5609efdb0d3c95499c00046bc53648b3482ec2175b5503d6e611b3f0555dc71d", size = 4785572, upload-time = "2026-09-02T14:48:01.029Z" }, - { url = "https://files.pythonhosted.org/packages/a5/1e/b90e845b1dcd0f2f3f26b98283d857f25909223aacd265eee032c34ab8b1/lxml-6.1.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:97ce49699d87ebf8aad631b55d65b33219a4f1bfefbbf5bff19dc9af160aeaf9", size = 5656516, upload-time = "2026-09-02T14:48:03.419Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ab/0a1b802c57f3fba5c4efd77d5c6b78adaa8f7b681f0c90456b140fe8bf6c/lxml-6.1.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:48542c9acba9ff9450bd18d871d2c2c8787fdb283572b623d206f1b927cd7d9e", size = 5245982, upload-time = "2026-09-02T14:48:06.109Z" }, - { url = "https://files.pythonhosted.org/packages/da/ee/2c016fbceb3778137459292538d9dfa7e3ad9070fe409c15254ddd90d2cc/lxml-6.1.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c55e71a9b1db1f107efb60da49c093689b74c5c31a708e5379e2fd9439d4fbb5", size = 5267340, upload-time = "2026-09-02T14:48:08.374Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b1/736d18fd6f0835761923b7bac1f0c27d60c1200384e9093f05d8c5100525/lxml-6.1.3-cp312-cp312-win32.whl", hash = "sha256:b3ff39654f0ce6ebd4db154211136dbe7e8157bcc3bed2344c87f32c7c6ecb6c", size = 3602606, upload-time = "2026-09-02T14:48:10.384Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5b/6ed903e4e6278a020c8a6f0dbbe78030d041840a6b4a64ea441a1e414077/lxml-6.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:3e9a00d1c2c30936f7add097c41afc5da6556c580909104aafd382cac92a855c", size = 4005999, upload-time = "2026-09-02T14:48:12.51Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1b/7bcebb7b6332cb3ae85e9c13b139adb6f23f75c71d84041c56a5005d9a29/lxml-6.1.3-cp312-cp312-win_arm64.whl", hash = "sha256:1aeca87830c4fe649dcf93fe2b059525b71c72587f21be4ae4af7103082a79fa", size = 3666631, upload-time = "2026-09-02T14:48:14.567Z" }, - { url = "https://files.pythonhosted.org/packages/52/05/3ef45db776baea068044c799bbba68f3ca00a440c0e930a17c572f3d9639/lxml-6.1.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3a48093cdb058a93af842ede9703520e810b05dcd0fc6d7190a06376c3bfb6bd", size = 8590357, upload-time = "2026-09-02T14:48:17.413Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a5/eee2fc77eee5ea68e4a4334b1def1781a3beaeefd3d98e81b4a38dc447b7/lxml-6.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:887c021d9a977cff89cb273047c1352997b772a8908a25c21836861f69b92be1", size = 4632616, upload-time = "2026-09-02T14:48:20.745Z" }, - { url = "https://files.pythonhosted.org/packages/35/42/df27b56848acd29d8a720acc28977911aab36f2a09df4208d5502e887415/lxml-6.1.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:611a51e61c92f62345a50b0035df6fc0d678f9299f33728826d831598862f59d", size = 4936186, upload-time = "2026-09-02T14:48:22.94Z" }, - { url = "https://files.pythonhosted.org/packages/ab/8d/8a7b91df0b54d09d25f5f44885d6b3e0a6d6643a8c070191580318d20c42/lxml-6.1.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b477912f42c5c33405a10c759d22f80cf5af043ae02d95b9d8e5e5bc555739ed", size = 5093324, upload-time = "2026-09-02T14:48:25.132Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7e/8f340ddcd43790332fb0de8a26628d571a492da3300cd191821698407c96/lxml-6.1.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cffe18571ccc51d742cd08cbb3f8b756de9311d18c7ea98f5d92f37b8fb60c2", size = 4998850, upload-time = "2026-09-02T14:48:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c1/9c5bb572f1f09ec9e4322bd4a4e9f4ad48347fc56ef94cf4df58a5279dc8/lxml-6.1.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75cc6569e86be5785b6188ef1642670c6adbc984e81ec35e224842ecd9eefcc8", size = 5626813, upload-time = "2026-09-02T14:48:29.61Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7d/8bf1fd8bae8247743968bb76d027a1ac5bd2c4b44495fba6a71b30d10706/lxml-6.1.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d85dfab42dd672f87a7f76e9de7172962aee69fa12044f0d6e1a23cbd53fb80e", size = 5232385, upload-time = "2026-09-02T14:48:31.969Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2e/6cef69ed81cb7df0d03b0dd09d08e6e2cf5061a743ff6f42f0b741548e9b/lxml-6.1.3-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:42632b4024ab24a6b488f559ac851312509888b6b80ae2aa11cf29a646a0d245", size = 5347088, upload-time = "2026-09-02T14:48:34.13Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e1/8e5fd8ddc8c7d685badb0f2db149e3c9da84eefc2827c01c658df2c4e3cb/lxml-6.1.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:febd35ef45f603c2d74b74655efdbf45e14f55fc0aef4ac82b663ca829b283e0", size = 4707227, upload-time = "2026-09-02T14:48:36.62Z" }, - { url = "https://files.pythonhosted.org/packages/7a/7e/00041382a11be40a88bf405ebff11c8efabd3de79f2691e1638b1c47a8a0/lxml-6.1.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a43b3bdf11e477dc7770609d3477316f974354dfc8425d596f64f471cc8daf6e", size = 5240208, upload-time = "2026-09-02T14:48:38.893Z" }, - { url = "https://files.pythonhosted.org/packages/fd/fe/316538b5cff0936fa63d45d421c655730fcbb5a28dcac728c175083002bc/lxml-6.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d582042c69857c364e8153de6e18e0da9b7b515a6a8113caf69a6ec8e0520f2", size = 5050271, upload-time = "2026-09-02T14:48:41.213Z" }, - { url = "https://files.pythonhosted.org/packages/c9/91/455bcccb3ac725373007344d351151810cd19762d1673b64b811f4359a42/lxml-6.1.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e49a646acfab83c68974f4aa1d0a2acca9e88d7d627ae0fc13201b14b76d310", size = 4780433, upload-time = "2026-09-02T14:48:43.779Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f6/580440e2f52cf00bba5c5e1080bfa88cdfcde73be71a11d95170ddbb663f/lxml-6.1.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0dee106e9aa97fb00541b1ed7827070564d0549c3d3fba8920e6b20fd980f748", size = 5645928, upload-time = "2026-09-02T14:48:46.187Z" }, - { url = "https://files.pythonhosted.org/packages/f6/dc/d123c1f244306543d545f62443f794959e4f1ea709fe100f8740d514e74a/lxml-6.1.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dd5e90f34cffcfed97f36cf066325773d2b6021c60c29942e53a18b028501b1d", size = 5231184, upload-time = "2026-09-02T14:48:48.691Z" }, - { url = "https://files.pythonhosted.org/packages/c3/3c/fe55b2bd5c6113c906511cd88f6a470195c5fbff1124f19970ab706c3477/lxml-6.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9b3e7d71bf6acff341233417abbdface29c647e3113892d9aaedc02eb4aa2bc", size = 5255814, upload-time = "2026-09-02T14:48:50.948Z" }, - { url = "https://files.pythonhosted.org/packages/e7/a7/485df55acf55dc35e4ca89d2f48f03889e5a3241826b18b85102b32ce9d8/lxml-6.1.3-cp313-cp313-win32.whl", hash = "sha256:160fcf381f76c3aeac28a756bec44f48942a8f7245a87aa28e3a523b4d90cd87", size = 3602214, upload-time = "2026-09-02T14:48:53.236Z" }, - { url = "https://files.pythonhosted.org/packages/c0/28/e46a7702bd95e9043291f7c3539b6184cba66f96cea9936f20939b284eeb/lxml-6.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:e477aca0bc0d19f3b4ae9e4f2a1cfd687c31bf772d78734910658186b40b2477", size = 4004091, upload-time = "2026-09-02T14:48:55.699Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1d/154c78e20479a43916e63f19cb720d83f44f024b03228be44c92d9a97b24/lxml-6.1.3-cp313-cp313-win_arm64.whl", hash = "sha256:b1cc980905221a5d8b3c476330730b3adb40ff80add71ffbdb6215ba055656f1", size = 3665468, upload-time = "2026-09-02T14:48:57.703Z" }, - { url = "https://files.pythonhosted.org/packages/0c/15/fc75a70b0af6021d0ea16811f1fc71cc42cd06ce90fe10f007a69b2eed84/lxml-6.1.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2bec13085dc8ef48a3fe62f7dfcacfeda2c785cdf19cc8eeda2bb9ed081da165", size = 8609725, upload-time = "2026-09-02T14:49:00.156Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/398fcf9018f881ec9aeaafae1ddd6586dfb13314a35d35e899de373dcae0/lxml-6.1.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f4db7c7e954d289d71878938348b3d91b904a3e8210a11939359fb758a58e7d", size = 4639629, upload-time = "2026-09-02T14:49:02.81Z" }, - { url = "https://files.pythonhosted.org/packages/a7/2d/49b6a6ad7ce8f64b07b9fe852ff0c6d3fcbb26db61bee4f63d4120180a1c/lxml-6.1.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2cae5d5c90a62d9139c512a0cb1aad1d182b022b5740daea2617eb5bf7fc658e", size = 4965074, upload-time = "2026-09-02T14:49:05.133Z" }, - { url = "https://files.pythonhosted.org/packages/66/bc/6230cf80e4331c33383b0b6b73dc31a393dd76edd4cb73d761de5123034d/lxml-6.1.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c6c0c13128a32eb04a51357e56a094e13aa8e6d3d1884de2e9ae923f6915e1a8", size = 5099355, upload-time = "2026-09-02T14:49:07.343Z" }, - { url = "https://files.pythonhosted.org/packages/ac/cf/d1143d9b7717e07a82f158a1fc9ce6e581fdad1226734950af869e3ffde4/lxml-6.1.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2221e88679d1351e9a40aaee54bc65679b9795bbd0160bc3d5e36b163344eb75", size = 5036795, upload-time = "2026-09-02T14:49:09.65Z" }, - { url = "https://files.pythonhosted.org/packages/31/6f/194bb00ffb89712c30f5a7e1b8e685590e140fad6c8261fec172c09a3dc0/lxml-6.1.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfb398886a7eb4c719161c3efcff2a1248febc53a4d8e5072d2d8a87fed84ac9", size = 5658740, upload-time = "2026-09-02T14:49:11.9Z" }, - { url = "https://files.pythonhosted.org/packages/e9/44/27e3cee3dcdb3b7bc09727b642bdbfcd098490ea77df04611db9060d7722/lxml-6.1.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7eb78ba28b187e1e9203a55c60fcf70df2d22cb205fe6d51b9383d6097419f0", size = 5245991, upload-time = "2026-09-02T14:49:14.154Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e9/8312560579fc980bbd2233a8a673cc46f7d613d3633f2bf08a21e8f4ad13/lxml-6.1.3-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:ea6b1e9105b4b24a34c722432d9fb578f9ed83af21fa1abda639011e0f22bbb6", size = 5354136, upload-time = "2026-09-02T14:49:16.459Z" }, - { url = "https://files.pythonhosted.org/packages/74/d8/eda60f4f73a9c780b5d6e1175484f66e6c81a2c93346e2906a1fec9c7a02/lxml-6.1.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:e8b17e23df3e827a69d25af70990ca2420e92668aaffaeeb3cd2351d7916a023", size = 4704379, upload-time = "2026-09-02T14:49:19.032Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c8/c9cc60057be78ac34bd2b842e45e6e88edbfe5e532e82c3b82381b7aab49/lxml-6.1.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b7c37339d7e75cab9a123a04248e243cefefb302ad6db566ea0c77cbcde421e", size = 5258676, upload-time = "2026-09-02T14:49:21.306Z" }, - { url = "https://files.pythonhosted.org/packages/41/7b/66894008fee8d1785b8db129747ae963fd427b68f456918df7f2f24a8b98/lxml-6.1.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:83e3a51e7933db700a0da0db31849db3a24022d9970da9bb73001e1d0326fd92", size = 5090069, upload-time = "2026-09-02T14:49:23.562Z" }, - { url = "https://files.pythonhosted.org/packages/8b/31/c1b60404859f4c3cd1f41f29c65a24e25cea78fde822d9574a21f66810be/lxml-6.1.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9bde9ae026a55b9a192078dfa6e27dd0ca4a050171ab6272e92f97b757dfdf48", size = 4741958, upload-time = "2026-09-02T14:49:26.037Z" }, - { url = "https://files.pythonhosted.org/packages/23/b8/6285f0cf546f14da2554cabdeaf7c2c2ff3190c74807f0de2e8810a786f9/lxml-6.1.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a635e837b50a1819bebfedaac5916498ea024120969da8790500148fb0a894d", size = 5683245, upload-time = "2026-09-02T14:49:28.438Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f6/2168cab44336dcb15fed0f0b78577225b83297cdf0dee349c95420c3dcb0/lxml-6.1.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d0c5c362bc94f1929dc7e96e715bbe7bd17037f802e6d8f0d1545df9133c0559", size = 5246087, upload-time = "2026-09-02T14:49:30.955Z" }, - { url = "https://files.pythonhosted.org/packages/f5/89/32f5de69a0a31f30e6164981851f87b37ecb2c4ee838e504b88d49d4818e/lxml-6.1.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c59e4265608da6a041f54646ecc0c9ecdbb19aaf14c4c684bb6c2114998cc415", size = 5269352, upload-time = "2026-09-02T14:49:33.502Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a1/741d952ed3a7ef7a50055c6415aec3f067015e97f72f4389ce77b09657ba/lxml-6.1.3-cp314-cp314-win32.whl", hash = "sha256:2e62c569ec7531b679b184cbfe335c501c1d13c4b363560013019962eb630e6d", size = 3662783, upload-time = "2026-09-02T14:50:23.751Z" }, - { url = "https://files.pythonhosted.org/packages/0f/bc/5811cc73cac05e324e05ba9b0924e1a163a317a167ede8a9c748b11db30a/lxml-6.1.3-cp314-cp314-win_amd64.whl", hash = "sha256:66299564c046bc7e0cc5de5106601eae907e9fa5904cd68a323380a8502f7861", size = 4073951, upload-time = "2026-09-02T14:50:26.348Z" }, - { url = "https://files.pythonhosted.org/packages/92/18/3768c8b01ac3a9bed1914715e6011711b00e2a11628ffa6f7fa37f8e0269/lxml-6.1.3-cp314-cp314-win_arm64.whl", hash = "sha256:ebd054ad1737a68fb7c5c073d405cef2b88bb824e294de3b4a4e995b47f0e376", size = 3749279, upload-time = "2026-09-02T14:50:28.749Z" }, - { url = "https://files.pythonhosted.org/packages/72/38/84684784738d9451db2b330de2483f496690c3a5c642071df24135739b37/lxml-6.1.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5a143e6207579de8baeded4eaac9134413200359f1969d636f0bfb98ee8c3c8f", size = 8860296, upload-time = "2026-09-02T14:49:36.346Z" }, - { url = "https://files.pythonhosted.org/packages/24/b7/fc4c50bb1b38e864010ea396046cabe85129bf9e65b11edcfbc37d356241/lxml-6.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a1cec0f99b9b914d39176347a93b7610dc09324491aee1cbc57cd291a41a1d55", size = 4755190, upload-time = "2026-09-02T14:49:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/94/e2/ee9aa6ed2b666b2db1f6f7fd48964ff9da39ebe827ef5eac0ab881f639d9/lxml-6.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6b9d2aad499c769ee8287609ab0e6de99d8bcea99c6e6c2e64945259fd52fb2", size = 4979517, upload-time = "2026-09-02T14:49:42.153Z" }, - { url = "https://files.pythonhosted.org/packages/29/e3/e7763d1661b283ddd4fa36f91b9a497db6b8d2aff55028b16c7f642e0755/lxml-6.1.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a23fefdb345b2d4d0ff2860571b5ff9a89a28b6a120f720e8fb0324d346626", size = 5115270, upload-time = "2026-09-02T14:49:44.493Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cd/22205d5b4d177e3f4156f780412426ee7c7f8107809f119f0dcc40fa51e3/lxml-6.1.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:545ccc14fb05485f48b4439ec35beb16d5b5280eb6c81c658bd4707a2a119414", size = 5032449, upload-time = "2026-09-02T14:49:46.841Z" }, - { url = "https://files.pythonhosted.org/packages/da/43/06a4626c3bb79ef8c501b674afab8100d64e798665bb2a97d1c960636a49/lxml-6.1.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:93476b6514b373fc6ca67d26c442784f7807c86f00635bfe79f935c3eab2af17", size = 5603325, upload-time = "2026-09-02T14:49:49.664Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9c/733682a0c2de9f5779ba207bbb3f3f6be8c6bda863fc01739b186b38783a/lxml-6.1.3-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8db38ff3fb7aee7d6a82ae4da2eef1178656fe1216841fbd24870062a9d60473", size = 5229023, upload-time = "2026-09-02T14:49:52.447Z" }, - { url = "https://files.pythonhosted.org/packages/c6/8a/e69cdaca3fd33a647942925664f01b20908d41a6968c182305be9c38fb11/lxml-6.1.3-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:25f4118c438f96bb466e83108506d03d5c31b1bd2387e83e5b070bda6ded9c37", size = 5317811, upload-time = "2026-09-02T14:49:55.25Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b2/0c397588174403c2ab68fc464abf97e03e7324f9c6cb6a99023104707195/lxml-6.1.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:1beb0f9909b26cee938df9ba56b15252a84429b1fc30ce6fca161390b9789a70", size = 4646516, upload-time = "2026-09-02T14:49:57.761Z" }, - { url = "https://files.pythonhosted.org/packages/56/7e/cfea25afafbe49db8b225764f7f74bb37c2a7f5e717d917d3d4a5e098ed4/lxml-6.1.3-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3a27ac6c780c8b8a1cd231b58407634cafc1c4cc28cd6c7141362df0f36351e7", size = 5240626, upload-time = "2026-09-02T14:50:00.279Z" }, - { url = "https://files.pythonhosted.org/packages/a1/75/7a587771bb52ebb0e2c57b6dbe9fd96a70fbb54d72ddd97d54c5f8ec18d5/lxml-6.1.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a1932d7ce78a561367512c594fe66eac2b2ec9b9264cfd9b5f950622f4a116e2", size = 5086619, upload-time = "2026-09-02T14:50:03.245Z" }, - { url = "https://files.pythonhosted.org/packages/1e/01/94c0ebe6d831861542d251e038052e52bf6d33f1d18f1cfffdc82851065a/lxml-6.1.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7d0f5976aa2701996f759b30172925829867547bb073af0ae67d1307a0f0262c", size = 4758828, upload-time = "2026-09-02T14:50:05.873Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f1/938d67bd0e5b1fdfa52be28aefdffbad57e1f6b8e921c2aab88542c75f40/lxml-6.1.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e7ce578aa8a80910a72a8ca0bbea3baae10100827249001999726a788456d8", size = 5627083, upload-time = "2026-09-02T14:50:08.555Z" }, - { url = "https://files.pythonhosted.org/packages/d8/65/4e51522f6c214650db0abb7b16ccd11b1238b8a05a8d59aa4ebed59c9f67/lxml-6.1.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d97c5227621af74b111882a290b10f371780a38eef9d9e730408fba2259b52fb", size = 5235170, upload-time = "2026-09-02T14:50:11.255Z" }, - { url = "https://files.pythonhosted.org/packages/92/c2/e73d19365665f6b16ef84df21199befc3b06e4c539046ad2d9595f6fb9ea/lxml-6.1.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:da707f14ea3c35ee463d50acd596d6488e4b2b4ae7cf77a5bf93f55c023d63e8", size = 5252273, upload-time = "2026-09-02T14:50:13.782Z" }, - { url = "https://files.pythonhosted.org/packages/48/a9/7f386c84c9fe2854e1ca6e231c285e1c8f392971ac353c6865e6ec49faff/lxml-6.1.3-cp314-cp314t-win32.whl", hash = "sha256:9efe56a68179f3adc4de41861c9358931db03837c48dd5e1c78077b84dd07f3a", size = 3902712, upload-time = "2026-09-02T14:50:16.171Z" }, - { url = "https://files.pythonhosted.org/packages/82/a6/8a3eb793f7900ef01c7f99e6f5fcbcfbdff35251cfaef66b32a4c16352d6/lxml-6.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c9389b3784b56c58d933b5e0aecdf28f901b073ff385358d8a7d40907f6e14b2", size = 4400979, upload-time = "2026-09-02T14:50:18.621Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c4/3807bea283b4fe9e9d9f5dde46a73df91178472b335d2778e10b2a37aa22/lxml-6.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32a409be3190b088f960ac92bfedfbef2f86c49ff940765e1548177592d20026", size = 3823401, upload-time = "2026-09-02T14:50:21.119Z" }, - { url = "https://files.pythonhosted.org/packages/e1/8e/4614fcd65496054cfb7172662f3576a59200278739506433b8c241ea422a/lxml-6.1.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:6ea2f13dce778ca072ccee598bca46a092ce192e8fd907b6c1f0e52c800529a0", size = 8609378, upload-time = "2026-09-02T14:50:31.772Z" }, - { url = "https://files.pythonhosted.org/packages/f2/51/2cdce3c65fa99a6195dd8fbd512d33407c1000ad99f63e0a285b63d7a8eb/lxml-6.1.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c581b1d68b3845fb86c6b2983e755b29bf001461c59fa411d2c26a911b6559a9", size = 4640022, upload-time = "2026-09-02T14:50:34.41Z" }, - { url = "https://files.pythonhosted.org/packages/52/09/0b30084e9eb1c546a4be3d9c56df70058d116b1a320400a59b0f7da87bf0/lxml-6.1.3-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e01125896585139453cab8cb235893644d8815d7509520da95ae3ee8d1c1f79", size = 5037928, upload-time = "2026-09-02T14:50:37.007Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0e/5c37275a3e361f6138dc06db748ea565c1fe8a5f4ee5e2ddd80047c81a89/lxml-6.1.3-cp315-cp315-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:290f66b97ede0e552e1cb44a0fd8a74f9753ee635b50830a0b122fb72788d015", size = 5661932, upload-time = "2026-09-02T14:50:39.777Z" }, - { url = "https://files.pythonhosted.org/packages/70/c5/b71ffb289b15e2642e2a3cf6d468c44da39ea119061a99e5b05e3d10f217/lxml-6.1.3-cp315-cp315-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73fc05988ed20809450474ba760a87c8ad4e455fc09783c02195e56ec634b41a", size = 5249209, upload-time = "2026-09-02T14:50:42.141Z" }, - { url = "https://files.pythonhosted.org/packages/81/ea/9910da149a23932f9301652e57661cd9e42b0df18f12be21159b7255f92b/lxml-6.1.3-cp315-cp315-manylinux_2_31_armv7l.whl", hash = "sha256:dc3a44689eea43eab836e5c98a8ab015dc2419987d1ea6eafc7c590cdff86bed", size = 4704543, upload-time = "2026-09-02T14:50:44.634Z" }, - { url = "https://files.pythonhosted.org/packages/76/07/9290329cd188c62e22021f79df04ee0cc33d9a93b0d38bd65ccd452ad9d0/lxml-6.1.3-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:209c3ccbfe35a04ac6d24f0611f9d1cbf8025d49991b14acd935236234d6c156", size = 5261298, upload-time = "2026-09-02T14:50:47.301Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0c/aba78bd3401cd99b73a0aed8e2b9b43e14be94fab3603d4bbc8a62365f2a/lxml-6.1.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2f5b2a2b9811b853b39bfa41367c6d78747b8e3e80e07fc5a24aae295c1a4d7d", size = 5090453, upload-time = "2026-09-02T14:50:49.952Z" }, - { url = "https://files.pythonhosted.org/packages/8d/dc/fa4426c3355aa0216cbeb3911495b5f65a26e0df85859a89928fe28f0396/lxml-6.1.3-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6a406d0b3cb207b0fa460ed4dc93e866f44f105da0169361cb18ff998a44c7f0", size = 4744709, upload-time = "2026-09-02T14:50:52.394Z" }, - { url = "https://files.pythonhosted.org/packages/be/2b/224fe7918658ab7c532ac2412f3c1eb28f71e6364fb07566262d0cc6a7b6/lxml-6.1.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:53258656846f5c48996b882fb4b135885e088a3ad3d96b4bc0530f95124d1f69", size = 5685802, upload-time = "2026-09-02T14:50:55.043Z" }, - { url = "https://files.pythonhosted.org/packages/21/44/7d480819b9adcae5f84dd8ac529132c6b7a578544398225cd20321adcd91/lxml-6.1.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:aa633613ff907ea91b9b0489a1f0da1b8725d8c6ccec6b77e8a1c9c235044bb0", size = 5249019, upload-time = "2026-09-02T14:50:57.985Z" }, - { url = "https://files.pythonhosted.org/packages/72/83/385a267ea1b6b283f2249dd827ef360a295e9db14e13ef4665a120c60d64/lxml-6.1.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:90f709b9accab6b2e4d14f5c8718203877a0486bcb3afd74d8b539ecd1e961d4", size = 5271886, upload-time = "2026-09-02T14:51:01.667Z" }, - { url = "https://files.pythonhosted.org/packages/d8/0d/f967b0eb172ae876855a402d6d9b11fa86e3e0c89ca9bbfeadf7ffbfa719/lxml-6.1.3-cp315-cp315-win32.whl", hash = "sha256:b4fc6b03b9d9d90557274f571ab30e7fbbfc527955536935d96f98b6817a86e4", size = 3662894, upload-time = "2026-09-02T14:51:45.173Z" }, - { url = "https://files.pythonhosted.org/packages/f4/48/d8a8c4160a29e663109ad520bac2deb37fcd014756d024561e8bc3e611ec/lxml-6.1.3-cp315-cp315-win_amd64.whl", hash = "sha256:33cadd956b667997e4de1635fce9541f2e8ede2038fcde8cf55aa14d571d1bad", size = 4074626, upload-time = "2026-09-02T14:51:47.77Z" }, - { url = "https://files.pythonhosted.org/packages/25/20/3e1395d34d19f9254625d0b567b81cf70d37d3417be074f4d63b94a2be3c/lxml-6.1.3-cp315-cp315-win_arm64.whl", hash = "sha256:8a330c0ee5fa318c7b5cbbaad882baeca3f570357e7eb25ab34bf31008150758", size = 3749495, upload-time = "2026-09-02T14:51:50.663Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c6/7465ffd9c43883526a382df6fa4846c9d8d419214f7effbf65270e795471/lxml-6.1.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:0bf5a3e397df2ec4258eb5eea4c1ac6cf013ca1abd04a176903bff20a70021fe", size = 8857677, upload-time = "2026-09-02T14:51:05.109Z" }, - { url = "https://files.pythonhosted.org/packages/ed/eb/1f3a917e299df43c8162c3e6f64fc2cea3bcf277910f35bff5b8e5d39901/lxml-6.1.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:13d22c0d57355366b393936acf6b98a5e0edeadddd3fccbc6a846c50a76b8741", size = 4754522, upload-time = "2026-09-02T14:51:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f9/f81b4bdb6efb7a596be29603d8758154d00a5f545db9f3cef9d9041c8f64/lxml-6.1.3-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad7617727a96d189bd6f979d0fadf765198c7934e85f4edaba9bf3ad919a300", size = 5033744, upload-time = "2026-09-02T14:51:10.633Z" }, - { url = "https://files.pythonhosted.org/packages/c8/0f/26d9bfaacb319c86e0eca8a1a0bf1130d36a7afbd318883e23caea63763d/lxml-6.1.3-cp315-cp315t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cae82b5ca24b0c2beedb269f6e2a96f466acd926879ab00ae19f1a65cbf9ffb0", size = 5615269, upload-time = "2026-09-02T14:51:13.357Z" }, - { url = "https://files.pythonhosted.org/packages/5d/90/73675f3f4141350ed65d6fec533b107d4e802c5caa340cf111771edd86e0/lxml-6.1.3-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69cafd61aea04ebb3502c93c2aaa568b12931ca0802231e0b5de76bf8b6e74bd", size = 5236280, upload-time = "2026-09-02T14:51:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/fd/be/ed260767e7977de463a0f91f3f4fffcab85c0a2a024a21ffe1fa442c2c79/lxml-6.1.3-cp315-cp315t-manylinux_2_31_armv7l.whl", hash = "sha256:dc205732d593118cf701d986f40e9de7801bb2e371cb189ddbda9b7348f4d97e", size = 4650718, upload-time = "2026-09-02T14:51:19.102Z" }, - { url = "https://files.pythonhosted.org/packages/d0/fd/e9839d03b1e767f2725cf7d7d81b80d5f3f9fdc10ad8827e2479311b046e/lxml-6.1.3-cp315-cp315t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88e719b9437f148f7e1465df845c758dd1598618cbea3a2fd1e61a715542f2b2", size = 5243376, upload-time = "2026-09-02T14:51:21.606Z" }, - { url = "https://files.pythonhosted.org/packages/34/a5/4606e347e2788c301f677004aa83e28d24da9fe663a24380122af57be6fc/lxml-6.1.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40983eabefd13da003e68170928c7acc011f0d095eefce5871a3c71c9385fb9a", size = 5092340, upload-time = "2026-09-02T14:51:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/ea/99/3314a8661cdf30f493c55a87db283961dfaae08451976a2ca418958e1804/lxml-6.1.3-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:fad67b12ffe0f71e02b4932b04883cbc76a9072bbd30731409d3523cf058b011", size = 4758768, upload-time = "2026-09-02T14:51:26.813Z" }, - { url = "https://files.pythonhosted.org/packages/30/58/3bdc577f78ea8b7d72d39a84506f7001d5b28728f43e5b84891e3b7d9a4a/lxml-6.1.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd11e7550d89e551a87dcec30f04b1fca32e86b68708aa01a4daa455d8605e5", size = 5649546, upload-time = "2026-09-02T14:51:29.453Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e4/652633de1a2395949ebb7a8fc7d089aba12a2b45f0fefbc9d29e3e3ab3cf/lxml-6.1.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ca0ec532ad2f5ba1e5ec120ac157769c57f01855b3d8bf37213f5d88abd9ba0a", size = 5234874, upload-time = "2026-09-02T14:51:32.262Z" }, - { url = "https://files.pythonhosted.org/packages/65/a6/c4581d171de30449304b4859bbd3607e9b40da13c0f88b68e6097c8d785e/lxml-6.1.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e99e09ab7741f1281e2677f4c0058c7f5267d182530b09c87e4f6aa26adf3887", size = 5260043, upload-time = "2026-09-02T14:51:34.841Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d7/ed6ee6186a89e69ca4ea9658b2a278f46a5efe8b5d4db56c7197f18653fe/lxml-6.1.3-cp315-cp315t-win32.whl", hash = "sha256:ace1d2c83b2bd24db5940600541140e87a325e119cb32d5fa9ad720d7e76648e", size = 3901093, upload-time = "2026-09-02T14:51:37.234Z" }, - { url = "https://files.pythonhosted.org/packages/67/9d/11d10257a4a048d04195d638bb61f0246ce2448eb05f682bcbab25a257a8/lxml-6.1.3-cp315-cp315t-win_amd64.whl", hash = "sha256:b49638355ea3bebba70da783ccbc630fd72afa16bc46c54474bfa1f9a915bbc6", size = 4395446, upload-time = "2026-09-02T14:51:39.884Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b7/44edd7de434181c582892e68d1ffe6775ca403ce14aea07cb5a218a936cf/lxml-6.1.3-cp315-cp315t-win_arm64.whl", hash = "sha256:5a721a98c649855963811b59b55755b30566e7f7fc40bdc9803d66dee9f811cf", size = 3822836, upload-time = "2026-09-02T14:51:42.471Z" }, -] - -[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" @@ -1174,22 +980,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/70/e140dffff6e808dc6343598df76e7e2407fd0f581de3524c75fba2e0cf24/nh3-0.3.7-cp38-abi3-win_arm64.whl", hash = "sha256:f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a", size = 621867, upload-time = "2026-08-23T14:26:29.547Z" }, ] -[[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-search" source = { editable = "." } dependencies = [ { name = "django", version = "5.2.17", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-14-openedx-search-dev' or extra != 'group-14-openedx-search-django60' or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-doc') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-quality') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-test')" }, { name = "django", version = "6.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-14-openedx-search-django60'" }, - { name = "openedx-atlas" }, ] [package.dev-dependencies] @@ -1201,9 +997,6 @@ dev = [ { name = "code-annotations" }, { name = "diff-cover" }, { name = "django", version = "5.2.17", source = { registry = "https://pypi.org/simple" } }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, - { name = "edx-i18n-tools" }, { name = "edx-lint" }, { name = "isort" }, { name = "pycodestyle" }, @@ -1216,8 +1009,6 @@ dev = [ django60 = [ { name = "code-annotations" }, { name = "django", version = "6.1", source = { registry = "https://pypi.org/simple" } }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, { name = "pytest-cov" }, { name = "pytest-django" }, ] @@ -1225,9 +1016,7 @@ doc = [ { name = "build" }, { name = "code-annotations" }, { name = "django", version = "5.2.17", source = { registry = "https://pypi.org/simple" } }, - { name = "django-extensions" }, { name = "doc8" }, - { name = "edx-django-utils" }, { name = "pytest-cov" }, { name = "pytest-django" }, { name = "sphinx" }, @@ -1237,8 +1026,6 @@ doc = [ quality = [ { name = "code-annotations" }, { name = "django", version = "5.2.17", source = { registry = "https://pypi.org/simple" } }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, { name = "edx-lint" }, { name = "isort" }, { name = "pycodestyle" }, @@ -1249,93 +1036,73 @@ quality = [ test = [ { name = "code-annotations" }, { name = "django", version = "5.2.17", source = { registry = "https://pypi.org/simple" } }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, { name = "pytest-cov" }, { name = "pytest-django" }, ] test-base = [ { name = "code-annotations" }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, { name = "pytest-cov" }, { name = "pytest-django" }, ] [package.metadata] -requires-dist = [ - { name = "django" }, - { name = "openedx-atlas" }, -] +requires-dist = [{ name = "django", specifier = ">=0" }] [package.metadata.requires-dev] ci = [ - { name = "tox" }, - { name = "tox-uv" }, + { name = "tox", specifier = ">=0" }, + { name = "tox-uv", specifier = ">=1" }, ] dev = [ - { name = "code-annotations" }, - { name = "diff-cover" }, - { name = "django", specifier = ">=5.0,<6.0" }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, - { name = "edx-i18n-tools" }, - { name = "edx-lint" }, - { name = "isort" }, - { name = "pycodestyle" }, - { name = "pydocstyle" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, - { name = "tox" }, - { name = "tox-uv" }, + { name = "code-annotations", specifier = ">=0" }, + { name = "diff-cover", specifier = ">=0" }, + { name = "django", specifier = ">=5.2,<6.0" }, + { name = "edx-lint", specifier = ">=6.2" }, + { name = "isort", specifier = ">=0" }, + { name = "pycodestyle", specifier = ">=0" }, + { name = "pydocstyle", specifier = ">=0" }, + { name = "pytest-cov", specifier = ">=0" }, + { name = "pytest-django", specifier = ">=0" }, + { name = "tox", specifier = ">=0" }, + { name = "tox-uv", specifier = ">=1" }, ] django60 = [ - { name = "code-annotations" }, + { name = "code-annotations", specifier = ">=0" }, { name = "django", specifier = ">=6.0,<7.0" }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, + { name = "pytest-cov", specifier = ">=0" }, + { name = "pytest-django", specifier = ">=0" }, ] doc = [ - { name = "build" }, - { name = "code-annotations" }, - { name = "django", specifier = ">=5.0,<6.0" }, - { name = "django-extensions" }, - { name = "doc8" }, - { name = "edx-django-utils" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, - { name = "sphinx" }, - { name = "sphinx-book-theme" }, - { name = "twine" }, + { name = "build", specifier = ">=0" }, + { name = "code-annotations", specifier = ">=0" }, + { name = "django", specifier = ">=5.2,<6.0" }, + { name = "doc8", specifier = ">=0" }, + { name = "pytest-cov", specifier = ">=0" }, + { name = "pytest-django", specifier = ">=0" }, + { name = "sphinx", specifier = ">=0" }, + { name = "sphinx-book-theme", specifier = ">=0" }, + { name = "twine", specifier = ">=0" }, ] quality = [ - { name = "code-annotations" }, - { name = "django", specifier = ">=5.0,<6.0" }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, - { name = "edx-lint" }, - { name = "isort" }, - { name = "pycodestyle" }, - { name = "pydocstyle" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, + { name = "code-annotations", specifier = ">=0" }, + { name = "django", specifier = ">=5.2,<6.0" }, + { name = "edx-lint", specifier = ">=6.2" }, + { name = "isort", specifier = ">=0" }, + { name = "pycodestyle", specifier = ">=0" }, + { name = "pydocstyle", specifier = ">=0" }, + { name = "pytest-cov", specifier = ">=0" }, + { name = "pytest-django", specifier = ">=0" }, ] test = [ - { name = "code-annotations" }, - { name = "django", specifier = ">=5.0,<6.0" }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, + { name = "code-annotations", specifier = ">=0" }, + { name = "django", specifier = ">=5.2,<6.0" }, + { name = "pytest-cov", specifier = ">=0" }, + { name = "pytest-django", specifier = ">=0" }, ] test-base = [ - { name = "code-annotations" }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, + { name = "code-annotations", specifier = ">=0" }, + { name = "pytest-cov", specifier = ">=0" }, + { name = "pytest-django", specifier = ">=0" }, ] [[package]] @@ -1347,15 +1114,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] -[[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 = "platformdirs" version = "4.11.7" @@ -1374,43 +1132,6 @@ 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 = "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 = "pycodestyle" version = "2.14.0" @@ -1523,41 +1244,6 @@ 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 = "pynacl" -version = "1.6.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy' or (extra == 'group-14-openedx-search-dev' and extra == 'group-14-openedx-search-django60') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-doc') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-quality') or (extra == 'group-14-openedx-search-django60' and extra == 'group-14-openedx-search-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.11.0"