pax_global_header00006660000000000000000000000064151317362350014520gustar00rootroot0000000000000052 comment=0e5e659a33e33a1b2c694c7e77046d598ca89ec0 django-polymorphic-4.10.2/000077500000000000000000000000001513173623500154115ustar00rootroot00000000000000django-polymorphic-4.10.2/.github/000077500000000000000000000000001513173623500167515ustar00rootroot00000000000000django-polymorphic-4.10.2/.github/workflows/000077500000000000000000000000001513173623500210065ustar00rootroot00000000000000django-polymorphic-4.10.2/.github/workflows/lint.yml000066400000000000000000000047201513173623500225020ustar00rootroot00000000000000name: Lint permissions: contents: read on: push: tags-ignore: - '*' branches: - '*' pull_request: workflow_call: workflow_dispatch: inputs: debug: description: 'Open ssh debug session.' required: true default: false type: boolean jobs: linting: runs-on: ubuntu-latest strategy: matrix: # run static analysis on bleeding and trailing edges python-version: [ '3.10', '3.12', '3.14' ] django-version: - '4.2' # LTS April 2026 - '5.2' # LTS April December 2027 - '6.0' # exclude: - python-version: '3.12' django-version: '4.2' - python-version: '3.14' django-version: '4.2' - python-version: '3.10' django-version: '5.2' - python-version: '3.14' django-version: '5.2' - python-version: '3.10' django-version: '6.0' - python-version: '3.12' django-version: '6.0' env: TEST_PYTHON_VERSION: ${{ matrix.python-version }} TEST_DJANGO_VERSION: ${{ matrix.django-version }} steps: - uses: actions/checkout@v5 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 id: sp with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v6 with: enable-cache: true - name: Install Just uses: extractions/setup-just@v3 - name: Install Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} if [[ "${{ matrix.django-version }}" =~ (a|b|rc) ]]; then just test-lock Django==${{ matrix.django-version }} else just test-lock Django~=${{ matrix.django-version }}.0 fi just install-docs - name: Install Emacs if: ${{ github.event.inputs.debug == 'true' }} run: | sudo apt install emacs - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@v3.22 with: detached: true timeout-minutes: 60 - name: Run Static Analysis run: | just check-lint just check-format just check-types just check-package just check-readme django-polymorphic-4.10.2/.github/workflows/release.yml000066400000000000000000000105541513173623500231560ustar00rootroot00000000000000name: Publish Release permissions: read-all concurrency: # stop previous release runs if tag is recreated group: release-${{ github.ref }} cancel-in-progress: true on: push: tags: - 'v*' # only publish on version tags (e.g. v1.0.0) jobs: lint: if: github.repository == 'jazzband/django-polymorphic' name: Lint permissions: contents: read actions: write uses: ./.github/workflows/lint.yml secrets: inherit test: if: github.repository == 'jazzband/django-polymorphic' name: Test permissions: contents: read actions: write uses: ./.github/workflows/test.yml secrets: inherit build: if: github.repository == 'jazzband/django-polymorphic' name: Build Package runs-on: ubuntu-latest permissions: contents: read actions: write outputs: PACKAGE_NAME: ${{ steps.set-package.outputs.package_name }} RELEASE_VERSION: ${{ steps.set-package.outputs.release_version }} steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 id: sp with: python-version: "3.13" # for tomlib - name: Install uv uses: astral-sh/setup-uv@v7 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: Install Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} sudo apt-get install -y gettext - name: Verify Tag run: | TAG_NAME=${GITHUB_REF#refs/tags/} echo "Verifying tag $TAG_NAME..." # if a tag was deleted and recreated we may have the old one cached # be sure that we're publishing the current tag! git fetch --force origin refs/tags/$TAG_NAME:refs/tags/$TAG_NAME # verify signature curl -sL https://github.com/${{ github.actor }}.gpg | gpg --import git tag -v "$TAG_NAME" # verify version RELEASE_VERSION=$(just validate_version $TAG_NAME) # export the release version echo "RELEASE_VERSION=${RELEASE_VERSION}" >> $GITHUB_ENV - name: Build the binary wheel and a source tarball run: just build - name: Store the distribution packages uses: actions/upload-artifact@v5 with: name: python-package-distributions path: dist/ - name: Set Package Name id: set-package run: PACKAGE_NAME=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['name'])") echo "PACKAGE_NAME=${PACKAGE_NAME}" >> $GITHUB_ENV publish-to-jazzband: name: Publish to Jazzband needs: - lint - test - build runs-on: ubuntu-latest steps: - name: Download all the dists uses: actions/download-artifact@v6 with: name: python-package-distributions path: dist/ - name: Upload Package to Jazzband uses: pypa/gh-action-pypi-publish@release/v1.13 with: user: jazzband password: ${{ secrets.JAZZBAND_RELEASE_KEY }} attestations: false repository-url: https://jazzband.co/projects/django-polymorphic/upload verbose: true github-release: name: Publish GitHub Release runs-on: ubuntu-latest needs: - lint - test - build permissions: contents: write # IMPORTANT: mandatory for making GitHub Releases id-token: write # IMPORTANT: mandatory for sigstore steps: - name: Download all the dists uses: actions/download-artifact@v6 with: name: python-package-distributions path: dist/ - name: Sign the dists with Sigstore uses: sigstore/gh-action-sigstore-python@v3.1.0 with: inputs: >- ./dist/*.tar.gz ./dist/*.whl - name: Create GitHub Release env: GITHUB_TOKEN: ${{ github.token }} run: >- gh release create '${{ github.ref_name }}' --repo '${{ github.repository }}' --generate-notes --prerelease - name: Upload artifact signatures to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} # Upload to GitHub Release using the `gh` CLI. # `dist/` contains the built packages, and the # sigstore-produced signatures and certificates. run: >- gh release upload '${{ github.ref_name }}' dist/** --repo '${{ github.repository }}' django-polymorphic-4.10.2/.github/workflows/test.yml000066400000000000000000000642001513173623500225120ustar00rootroot00000000000000name: Test permissions: contents: read on: push: tags-ignore: - '*' branches: - '*' pull_request: workflow_call: workflow_dispatch: inputs: debug: description: 'Open ssh debug session.' required: true default: false type: boolean schedule: - cron: '0 13 * * *' # Runs at 6 am pacific every day jobs: postgres: runs-on: ubuntu-latest permissions: contents: read actions: write # Service containers to run with `container-job` strategy: fail-fast: false matrix: python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] postgres-version: ['12', '14', 'latest'] psycopg-version: ['psycopg2', 'psycopg3'] django-version: - '4.2' # LTS April 2026 - '5.1' # December 2025 - '5.2' # LTS April 2028 - '6.0' # exclude: - python-version: '3.10' django-version: '6.0' - python-version: '3.11' django-version: '6.0' - python-version: '3.13' django-version: '4.2' - python-version: '3.14' django-version: '4.2' - python-version: '3.14' django-version: '5.1' - python-version: '3.14' django-version: '5.2' - postgres-version: '12' django-version: '5.1' - postgres-version: '12' django-version: '5.2' - postgres-version: '12' django-version: '6.0' - postgres-version: '14' django-version: '4.2' - postgres-version: '14' django-version: '5.2' - postgres-version: '14' django-version: '6.0' - postgres-version: 'latest' django-version: '4.2' - postgres-version: 'latest' django-version: '5.1' - postgres-version: '12' psycopg-version: 'psycopg3' - postgres-version: 'latest' psycopg-version: 'psycopg2' # https://github.com/psycopg/psycopg2/pull/1695 - python-version: '3.13' psycopg-version: 'psycopg2' - python-version: '3.14' psycopg-version: 'psycopg2' env: RDBMS: postgres POSTGRES_PASSWORD: postgres PGPASSWORD: postgres POSTGRES_USER: postgres POSTGRES_HOST: localhost POSTGRES_PORT: 5432 COVERAGE_FILE: linux-py${{ matrix.python-version }}-dj${{ matrix.django-version }}-${{ matrix.psycopg-version }}-pg${{ matrix.postgres-version }}.coverage TEST_PYTHON_VERSION: ${{ matrix.python-version }} TEST_DJANGO_VERSION: ${{ matrix.django-version }} TEST_DATABASE_CLIENT_VERSION: ${{ matrix.psycopg-version }} TEST_DATABASE_VERSION: ${{ matrix.postgres-version }} # Service containers to run with `runner-job` services: # Label used to access the service container postgres: # Docker Hub image image: postgres:${{ matrix.postgres-version }} # Provide the password for postgres env: POSTGRES_PASSWORD: postgres # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: # Maps tcp port 5432 on service container to the host - 5432:5432 steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} id: sp uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Create test databases run: | psql -h localhost -p 5432 -U postgres -d postgres -c "CREATE DATABASE test1;" psql -h localhost -p 5432 -U postgres -d postgres -c "CREATE DATABASE test2;" - name: Install uv uses: astral-sh/setup-uv@v6 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: Install Emacs if: ${{ github.event.inputs.debug == 'true' }} run: | sudo apt install emacs - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@v3.22 with: detached: true timeout-minutes: 60 - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} if [[ "${{ matrix.django-version }}" =~ (a|b|rc) ]]; then just test-lock Django==${{ matrix.django-version }} else just test-lock Django~=${{ matrix.django-version }}.0 fi - name: Run Unit Tests run: | just test-db ${{ matrix.psycopg-version }} just test-integrations ${{ matrix.psycopg-version }} - name: Store coverage files uses: actions/upload-artifact@v4 with: name: ${{ env.COVERAGE_FILE }} path: ${{ env.COVERAGE_FILE }} sqlite: runs-on: ubuntu-latest permissions: contents: read actions: write env: RDBMS: sqlite COVERAGE_FILE: linux-py${{ matrix.python-version }}-dj${{ matrix.django-version }}-sqlite.coverage TEST_PYTHON_VERSION: ${{ matrix.python-version }} TEST_DJANGO_VERSION: ${{ matrix.django-version }} TEST_DATABASE_VERSION: "sqlite" strategy: fail-fast: false matrix: python-version: [ '3.10', '3.12', '3.14'] django-version: - '4.2' # LTS April 2026 - '5.2' # LTS April 2028 - '6.0' # LTS April 2028 exclude: - python-version: '3.10' django-version: '5.2' - python-version: '3.10' django-version: '6.0' - python-version: '3.12' django-version: '4.2' - python-version: '3.12' django-version: '6.0' - python-version: '3.14' django-version: '4.2' - python-version: '3.14' django-version: '5.2' steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 id: sp with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v6 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: Install Emacs if: ${{ github.event.inputs.debug == 'true' }} run: | sudo apt install emacs - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@v3.22 with: detached: true timeout-minutes: 60 - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} just install if [[ "${{ matrix.django-version }}" =~ (a|b|rc) ]]; then just test-lock Django==${{ matrix.django-version }} else just test-lock Django~=${{ matrix.django-version }}.0 fi - name: Run Unit Tests run: | just test-db just test-integrations - name: Store coverage files uses: actions/upload-artifact@v4 with: name: ${{ env.COVERAGE_FILE }} path: ${{ env.COVERAGE_FILE }} mysql: runs-on: ubuntu-latest permissions: contents: read actions: write strategy: fail-fast: false matrix: python-version: ['3.10', '3.12', '3.14'] mysql-version: ['8.0', 'latest'] mysqlclient-version: ['1.4.3', ''] django-version: - '4.2' # LTS April 2024 - '5.2' # LTS April 2028 - '6.0' # April 2027 exclude: - django-version: '4.2' mysql-version: 'latest' - django-version: '6.0' mysql-version: '8.0' - mysql-version: 'latest' mysqlclient-version: '1.4.3' - python-version: '3.12' django-version: '4.2' - python-version: '3.14' django-version: '4.2' - python-version: '3.10' django-version: '5.2' - python-version: '3.14' django-version: '5.2' - django-version: '5.2' mysqlclient-version: '1.4.3' - python-version: '3.10' django-version: '6.0' - python-version: '3.12' django-version: '6.0' env: RDBMS: mysql MYSQL_VERSION: ${{ matrix.mysql-version }} COVERAGE_FILE: linux-py${{ matrix.python-version }}-dj${{ matrix.django-version }}-${{ matrix.mysqlclient-version }}-mysql${{ matrix.mysql-version }}.coverage TEST_PYTHON_VERSION: ${{ matrix.python-version }} TEST_DJANGO_VERSION: ${{ matrix.django-version }} TEST_DATABASE_CLIENT_VERSION: ${{ matrix.mysqlclient-version }} TEST_DATABASE_VERSION: ${{ matrix.mysql-version }} services: mysql: # Docker Hub image image: mysql:${{ matrix.mysql-version }} # Provide the password for mysql env: MYSQL_ROOT_PASSWORD: root MYSQL_MULTIPLE_DATABASES: test1,test2 # Set health checks to wait until mysql has started options: >- --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 5 ports: # Maps tcp port 3306 on service container to the host - 3306:3306 steps: # make text comparisons case sensitive (some tests) - name: Set default collation for MySQL run: | mysql -h 127.0.0.1 -u root -proot <- --health-cmd="${{ matrix.mariadb-healthcheck }}" --health-interval 10s --health-timeout 5s --health-retries 5 ports: # Maps tcp port 3306 on service container to the host - 3306:3306 steps: # make text comparisons case sensitive (some tests) - name: Set default collation for MariaDB run: | mysql -h 127.0.0.1 -u root -proot <- --health-cmd healthcheck.sh --health-interval 10s --health-timeout 5s --health-retries 10 oracle2: image: gvenzl/${{ matrix.oracle-version }} env: ORACLE_PASSWORD: password ORACLE_DATABASE: test2 # Forward Oracle port ports: - 1522:1521 # Provide healthcheck script options for startup options: >- --health-cmd healthcheck.sh --health-interval 10s --health-timeout 5s --health-retries 10 steps: - name: Set coverage file run: | ORACLE_TAG=$(echo "${{ matrix.oracle-version }}" | cut -d':' -f2) echo "COVERAGE_FILE=linux-py${{ matrix.python-version }}-dj${{ matrix.django-version }}-oracle${ORACLE_TAG}.coverage" >> $GITHUB_ENV - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} id: sp uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Emacs if: ${{ github.event.inputs.debug == 'true' }} run: | sudo apt install emacs - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@v3.22 with: detached: true timeout-minutes: 60 - name: Install uv uses: astral-sh/setup-uv@v6 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: Install Oracle Client # https://askubuntu.com/questions/1512196/libaio1-on-noble run: | sudo apt install alien libaio1t64 sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/x86_64-linux-gnu/libaio.so.1 if [[ "${{ matrix.oracle-version }}" == oracle-xe* ]]; then curl --output oracle-client.rpm https://download.oracle.com/otn_software/linux/instantclient/2116000/oracle-instantclient-basiclite-21.16.0.0.0-1.el8.x86_64.rpm sudo alien -i oracle-client.rpm sudo sh -c "echo /usr/lib/oracle/21/client64/lib/ > /etc/ld.so.conf.d/oracle.conf" else curl --output oracle-client.rpm https://download.oracle.com/otn_software/linux/instantclient/2326000/oracle-instantclient-basiclite-23.26.0.0.0-1.el9.x86_64.rpm sudo alien -i oracle-client.rpm sudo sh -c "echo /usr/lib/oracle/23/client64/lib/ > /etc/ld.so.conf.d/oracle.conf" fi sudo ldconfig - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} if [[ "${{ matrix.django-version }}" =~ (a|b|rc) ]]; then just test-lock Django==${{ matrix.django-version }} else just test-lock Django~=${{ matrix.django-version }}.0 fi # we don't run integration tests against Oracle in CI, these are slow enough - name: Run Full Unit Tests run: | if [[ "${{ matrix.oracle-version }}" == oracle-xe* ]]; then just test-db cx_oracle else just test-db oracledb fi - name: Store coverage files uses: actions/upload-artifact@v4 with: name: ${{ env.COVERAGE_FILE }} path: ${{ env.COVERAGE_FILE }} windows: runs-on: windows-latest permissions: contents: read actions: write env: RDBMS: sqlite COVERAGE_FILE: windows-py${{ matrix.python-version }}-dj${{ matrix.django-version }}-sqlite.coverage TEST_PYTHON_VERSION: ${{ matrix.python-version }} TEST_DJANGO_VERSION: ${{ matrix.django-version }} TEST_DATABASE_VERSION: "sqlite" strategy: fail-fast: false matrix: python-version: [ '3.10', '3.12', '3.14'] django-version: - '4.2' # LTS April 2026 - '5.2' # LTS April 2028 - '6.0' # LTS April 2028 exclude: - python-version: '3.10' django-version: '5.2' - python-version: '3.10' django-version: '6.0' - python-version: '3.12' django-version: '4.2' - python-version: '3.12' django-version: '6.0' - python-version: '3.14' django-version: '4.2' - python-version: '3.14' django-version: '5.2' steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 id: sp with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v6 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: install-vim-windows if: ${{ github.event.inputs.debug == 'true' }} uses: rhysd/action-setup-vim@v1 - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@v3.22 with: detached: true timeout-minutes: 60 - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} just install $version = "${{ matrix.django-version }}" if ($version -match "(a|b|rc)") { just test-lock "Django==$version" } else { just test-lock "Django~=$version.0" } - name: Run Unit Tests run: | just test just test-integrations - name: Store coverage files uses: actions/upload-artifact@v4 with: name: ${{ env.COVERAGE_FILE }} path: ${{ env.COVERAGE_FILE }} macos: runs-on: macos-latest permissions: contents: read actions: write env: RDBMS: sqlite COVERAGE_FILE: macos-py${{ matrix.python-version }}-dj${{ matrix.django-version }}-sqlite.coverage TEST_PYTHON_VERSION: ${{ matrix.python-version }} TEST_DJANGO_VERSION: ${{ matrix.django-version }} TEST_DATABASE_VERSION: "sqlite" strategy: fail-fast: false matrix: python-version: [ '3.10', '3.12', '3.14'] django-version: - '4.2' # LTS April 2026 - '5.2' # LTS April 2028 - '6.0' # LTS April 2028 exclude: - python-version: '3.10' django-version: '5.2' - python-version: '3.10' django-version: '6.0' - python-version: '3.12' django-version: '4.2' - python-version: '3.12' django-version: '6.0' - python-version: '3.14' django-version: '4.2' - python-version: '3.14' django-version: '5.2' steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 id: sp with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v6 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: install-emacs-macos if: ${{ github.event.inputs.debug == 'true' }} run: | brew install emacs - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@v3.22 with: detached: true timeout-minutes: 60 - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} just install if [[ "${{ matrix.django-version }}" =~ (a|b|rc) ]]; then just test-lock Django==${{ matrix.django-version }} else just test-lock Django~=${{ matrix.django-version }}.0 fi - name: Run Unit Tests run: | just test just test-integrations - name: Store coverage files uses: actions/upload-artifact@v4 with: name: ${{ env.COVERAGE_FILE }} path: ${{ env.COVERAGE_FILE }} coverage-combine: needs: [postgres, sqlite, mysql, mariadb, oracle, windows, macos] runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v5 id: sp - name: Install uv uses: astral-sh/setup-uv@v6 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} just install - name: Get coverage files uses: actions/download-artifact@v5 with: pattern: "*.coverage" merge-multiple: true - run: ls -la *.coverage - run: just coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml django-polymorphic-4.10.2/.gitignore000066400000000000000000000111501513173623500173770ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py.cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Test migrations (generated dynamically by tests) src/polymorphic/tests/test_migrations/migrations/0*.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # UV # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. #uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock #poetry.toml # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. # https://pdm-project.org/en/latest/usage/project/#working-with-version-control #pdm.lock #pdm.toml .pdm-python .pdm-build/ # pixi # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. #pixi.lock # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one # in the .venv directory. It is recommended not to include this directory in version control. .pixi # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # Redis *.rdb *.aof *.pid # RabbitMQ mnesia/ rabbitmq/ rabbitmq-data/ # ActiveMQ activemq-data/ # SageMath parsed files *.sage.py # Environments .env .envrc .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ # Abstra # Abstra is an AI-powered process automation framework. # Ignore directories containing user credentials, local state, and settings. # Learn more at https://abstra.io/docs .abstra/ # Visual Studio Code # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore # and can be added to the global gitignore or merged into this file. However, if you prefer, # you could uncomment the following to ignore the entire vscode folder # .vscode/ # Ruff stuff: .ruff_cache/ # PyPI configuration file .pypirc # Marimo marimo/_static/ marimo/_lsp/ __marimo__/ # Streamlit .streamlit/secrets.toml .DS_Store .python-version test1.db test2.db example/example.db django-polymorphic-4.10.2/.pre-commit-config.yaml000066400000000000000000000005311513173623500216710ustar00rootroot00000000000000ci: # Don't run these in pre-commit.ci at all skip: [lint, format] repos: - repo: local hooks: - id: lint name: Lint entry: just lint language: system pass_filenames: false - id: format name: Format entry: just format language: system pass_filenames: false django-polymorphic-4.10.2/.readthedocs.yaml000066400000000000000000000012101513173623500206320ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.12" apt_packages: - gettext jobs: post_install: - pip install uv - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --all-extras --group docs --link-mode=copy # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # Optionally build your docs in additional formats such as PDF and ePub formats: - pdf django-polymorphic-4.10.2/AUTHORS.md000066400000000000000000000030071513173623500170600ustar00rootroot00000000000000# Current Maintainer(s) * Brian Kohan ## Contributors * Abel Daniel * Adam Chainz * Adam Wentz * Adam Donaghy * Andrew Ingram (contributed setup.py) * Al Johri * Alex Alvarez * Andrew Dodd * Angel Velasquez * Austin Matsick * Bastien Vallet * Ben Konrath * Bert Constantin * Bertrand Bordage * Chad Shryock * Charles Leifer (python 2.4 compatibility) * Chris Barna * Chris Brantley * Christopher Glass * David Sanders * Emad Rad * Éric Araujo * Evan Borgstrom * Frankie Dintino * Gavin Wahl * Germán M. Bravo * Gonzalo Bustos * Gregory Avery-Weir * Hugo Osvaldo Barrera * Jacob Rief * James Murty * Jedediah Smith (proxy models support) * Jesús Leganés-Combarro (Auto-discover child models and inlines, #582) * John Furr * Jonas Haag * Jonas Obrist * Julian Wachholz * Kamil Bar * Kelsey Gilmore-Innis * Kevin Armenat * Krzysztof Gromadzki * Krzysztof Nazarewski * Luis Zárate * Marius Lueck * Martin Brochhaus * Martin Maillard * Michael Fladischer * Nick Ward * Oleg Myltsyn * Omer Strumpf * Paweł Adamczak * Petr Dlouhý * Sander van Leeuwen * Sobolev Nikita * Tadas Dailyda * Tai Lee * Tomas Peterka * Tony Narlock * Vail Gold ## Former authors / maintainers * Bert Constantin 2009/2010 (Original author, disappeared from the internet :( ) * Chris Glass * Diederik van der Boor * Charlie Denton * Jerome Leclanche ## django-rest-framework ### Development Lead * Denis Orehovsky ### Contributors * Jeff Hackshaw * TFranzel * Ignacio Losiggio django-polymorphic-4.10.2/CONTRIBUTING.md000066400000000000000000000150571513173623500176520ustar00rootroot00000000000000# Contributing [![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) This is a [Jazzband](https://jazzband.co) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). Contributions are encouraged! Please use the issue page to submit feature requests or bug reports. Issues with attached PRs will be given priority and have a much higher likelihood of acceptance. We are actively seeking additional maintainers. If you're interested, [contact me](https://github.com/bckohan). ## Installation ### Install Just We provide a platform independent justfile with recipes for all the development tasks. You should [install just](https://just.systems/man/en/) if it is not on your system already. [django-polymorphic](https://pypi.python.org/pypi/django-polymorphic) uses [uv](https://docs.astral.sh/uv) for environment, package, and dependency management. ``just setup`` will install the necessary build tooling if you do not already have it: ```bash just setup ``` Setup also may take a python version: ```bash just setup 3.12 ``` If you already have uv and python installed running install will just install the development dependencies: ```bash just install ``` **To run pre-commit checks you will have to install just.** ## Documentation `django-polymorphic` documentation is generated using [Sphinx](https://www.sphinx-doc.org) with the [furo](https://github.com/pradyunsg/furo) theme. Any new feature PRs must provide updated documentation for the features added. To build the docs run doc8 to check for formatting issues then run Sphinx: ```bash just install-docs # install the doc dependencies just docs # builds docs just check-docs # lint the docs just check-docs-links # check for broken links in the docs ``` Run the docs with auto rebuild using: ```bash just docs-live ``` ## Static Analysis `django-polymorphic` uses [ruff](https://docs.astral.sh/ruff/) for Python linting, header import standardization and code formatting. Before any PR is accepted the following must be run, and static analysis tools should not produce any errors or warnings. Disabling certain errors or warnings where justified is acceptable: To fix formatting and linting problems that are fixable run: ```bash just fix ``` To run all static analysis without automated fixing you can run: ```bash just check ``` To format source files you can run: ```bash just format ``` ## Running Tests `django-polymorphic` is set up to use [pytest](https://docs.pytest.org) to run unit tests. All the tests are housed in `src/polymorphic/tests`. Before a PR is accepted, all tests must be passing and the code coverage must be at 100%. A small number of exempted error handling branches are acceptable. To run the full suite: ```bash just test ``` To run a single test, or group of tests in a class: ```bash just test ::ClassName::FunctionName ``` For instance, to run all admin tests, and then just the test_admin_registration test you would do: ```bash just test src/polymorphic/tests/test_admin.py just test src/polymorphic/tests/test_admin.py::PolymorphicAdminTests::test_admin_registration ``` ### Running UI Tests Make sure you have playwright installed: ```bash just install-playwright ``` If you want to see the test step through the UI actions you can run the test like so: ```bash just debug-test -k ``` This will open a browser and the debugger at the start of the test, you can then ``next`` through and see the UI actions happen. ## Versioning [django-polymorphic](https://pypi.python.org/pypi/django-polymorphic) strictly adheres to [semantic versioning](https://semver.org). ## Issuing Releases The release workflow is triggered by tag creation. You must have [git tag signing enabled](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). Our justfile has a release shortcut: ```bash just release x.x.x ``` ## Just Recipes ```bash build # build docs and package build-docs # build the docs build-docs-html # build html documentation build-docs-pdf # build pdf documentation check # run all static checks check-docs # lint the documentation check-docs-links # check the documentation links for broken links check-format # check if the code needs formatting check-lint # lint the code check-package # run package checks check-readme # check that the readme renders check-types # run static type checking clean # remove all non repository artifacts clean-docs # remove doc build artifacts- clean-env # remove the virtual environment clean-git-ignored # remove all git ignored files coverage # generate the test coverage report debug-test *TESTS # debug a test - (break at test start/run in headed mode) docs # build and open the documentation docs-live # serve the documentation, with auto-reload fetch-refs LIB fix # fix formatting, linting issues and import sorting format # format the code and sort imports install *OPTS # update and install development dependencies install-docs # install documentation dependencies install-precommit # install git pre-commit hooks install_uv # install the uv package manager lint # sort the imports and fix linting issues make-test-migrations # regenerate test migrations using the lowest version of Django manage *COMMAND # run the django admin open-docs # open the html documentation precommit # run the pre-commit checks release VERSION # issue a release for the given semver string (e.g. 2.1.0) run +ARGS # run the command in the virtual environment setup python="python" # setup the venv, pre-commit hooks and playwright dependencies sort-imports # sort the python imports test *TESTS # run tests test-db DB_CLIENT="dev" *TESTS test-lock +PACKAGES # lock to specific python and versions of given dependencies validate_version VERSION # validate the given version string against the lib version ``` django-polymorphic-4.10.2/LICENSE000066400000000000000000000030431513173623500164160ustar00rootroot00000000000000Copyright (c) 2009 or later by the individual contributors. Please see the AUTHORS file. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-polymorphic-4.10.2/README.md000066400000000000000000000117751513173623500167030ustar00rootroot00000000000000# django-polymorphic [![License: BSD](https://img.shields.io/badge/License-BSD-blue.svg)](https://opensource.org/license/bsd-3-clause) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![PyPI version](https://badge.fury.io/py/django-polymorphic.svg)](https://pypi.python.org/pypi/django-polymorphic/) [![PyPI pyversions](https://img.shields.io/pypi/pyversions/django-polymorphic.svg)](https://pypi.python.org/pypi/django-polymorphic/) [![PyPI djversions](https://img.shields.io/pypi/djversions/django-polymorphic.svg)](https://pypi.org/project/django-polymorphic/) [![PyPI status](https://img.shields.io/pypi/status/django-polymorphic.svg)](https://pypi.python.org/pypi/django-polymorphic) [![Documentation Status](https://readthedocs.org/projects/django-polymorphic/badge/?version=latest)](http://django-polymorphic.readthedocs.io/?badge=latest/) [![Code Cov](https://img.shields.io/codecov/c/github/jazzband/django-polymorphic/master.svg)](https://codecov.io/github/jazzband/django-polymorphic?branch=master) [![Test Status](https://github.com/jazzband/django-polymorphic/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/jazzband/django-polymorphic/actions/workflows/test.yml?query=branch:master) [![Lint Status](https://github.com/jazzband/django-polymorphic/actions/workflows/lint.yml/badge.svg?branch=master)](https://github.com/jazzband/django-polymorphic/actions/workflows/lint.yml?query=branch:master) [![Published on Django Packages](https://img.shields.io/badge/Published%20on-Django%20Packages-0c3c26)](https://djangopackages.org/packages/p/django-polymorphic/) [![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) --------------------------------------------------------------------------------------------------- [![Postgres](https://img.shields.io/badge/Postgres-12%2B-blue)](https://www.postgresql.org/) [![MySQL](https://img.shields.io/badge/MySQL-8.0%2B-blue)](https://www.mysql.com/) [![MariaDB](https://img.shields.io/badge/MariaDB-10.4%2B-blue)](https://mariadb.org/) [![SQLite](https://img.shields.io/badge/SQLite-3.8%2B-blue)](https://www.sqlite.org/) [![Oracle](https://img.shields.io/badge/Oracle-21c%2B-blue)](https://www.oracle.com/database/) --------------------------------------------------------------------------------------------------- ## Polymorphic Models for Django [django-polymorphic](https://pypi.python.org/pypi/django-polymorphic) simplifies using inherited models in [Django](https://djangoproject.com) projects. When a query is made at the base model, the inherited model classes are returned. When we store models that inherit from a ``Project`` model... ```python >>> Project.objects.create(topic="Department Party") >>> ArtProject.objects.create(topic="Painting with Tim", artist="T. Turner") >>> ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") ``` ...and want to retrieve all our projects, the subclassed models are returned! ```python >>> Project.objects.all() [ , , ] ``` Using vanilla Django, we get the base class objects, which is rarely what we wanted: ```python >>> Project.objects.all() [ , , ] ``` This also works when the polymorphic model is accessed via ForeignKeys, ManyToManyFields or OneToOneFields. ### Features * Full admin integration. * ORM integration: * support for ForeignKey, ManyToManyField, OneToOneField descriptors. * Filtering/ordering of inherited models (``ArtProject___artist``). * Filtering model types: ``instance_of(...)`` and ``not_instance_of(...)`` * Combining querysets of different models (``qs3 = qs1 | qs2``) * Support for custom user-defined managers. * Uses the minimum amount of queries needed to fetch the inherited models. * Disabling polymorphic behavior when needed. **Note:** While [django-polymorphic](https://pypi.python.org/pypi/django-polymorphic) makes subclassed models easy to use in Django, we still encourage to use them with caution. Each subclassed model will require Django to perform an ``INNER JOIN`` to fetch the model fields from the database. While taking this in mind, there are valid reasons for using subclassed models. That's what this library is designed for! For more information, see the [documentation at Read the Docs](https://django-polymorphic.readthedocs.io). ### Installation ```bash $ pip install django-polymorphic ``` ```python INSTALLED_APPS = [ ... "django.contrib.contenttypes", # we rely on the contenttypes framework "polymorphic" ] ``` ## License [django-polymorphic](https://pypi.python.org/pypi/django-polymorphic) uses the same license as Django (BSD-like). django-polymorphic-4.10.2/conftest.py000066400000000000000000000031101513173623500176030ustar00rootroot00000000000000import inspect def pytest_configure(config): # stash it somewhere global-ish from polymorphic import tests tests.HEADLESS = not config.getoption("--headed") def first_breakable_line(obj) -> tuple[str, int]: """ Return the absolute line number of the first executable statement in a function or bound method. """ import ast import textwrap func = obj.__func__ if inspect.ismethod(obj) else obj source = inspect.getsource(func) source = textwrap.dedent(source) filename = inspect.getsourcefile(func) assert filename _, start_lineno = inspect.getsourcelines(func) tree = ast.parse(source) for node in tree.body[0].body: if ( isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str) ): continue return filename, start_lineno + node.lineno - 1 # fallback: just return the line after the def return filename, start_lineno + 1 def pytest_runtest_call(item): # --trace cli option does not work for unittest style tests so we implement it here test = getattr(item, "obj", None) if item.config.option.trace and inspect.ismethod(test): from IPython.terminal.debugger import TerminalPdb try: file = inspect.getsourcefile(test) assert file dbg = TerminalPdb() dbg.set_break(*first_breakable_line(test)) dbg.cmdqueue.append("continue") dbg.set_trace() except (OSError, AssertionError): pass django-polymorphic-4.10.2/docs/000077500000000000000000000000001513173623500163415ustar00rootroot00000000000000django-polymorphic-4.10.2/docs/_ext/000077500000000000000000000000001513173623500173005ustar00rootroot00000000000000django-polymorphic-4.10.2/docs/_ext/djangodummy/000077500000000000000000000000001513173623500216165ustar00rootroot00000000000000django-polymorphic-4.10.2/docs/_ext/djangodummy/__init__.py000066400000000000000000000000001513173623500237150ustar00rootroot00000000000000django-polymorphic-4.10.2/docs/_ext/djangodummy/requirements.txt000066400000000000000000000002431513173623500251010ustar00rootroot00000000000000# for readthedocs # Remaining requirements are picked up from setup.py Django>=4.2.20 django-extra-views>=0.14.0 sphinxcontrib-django>=2.5 sphinx_rtd_theme>=2.0.0 django-polymorphic-4.10.2/docs/_ext/djangodummy/settings.py000066400000000000000000000006071513173623500240330ustar00rootroot00000000000000# Settings file to allow parsing API documentation of Django modules, # and provide defaults to use in the documentation. # # This file is placed in a subdirectory, # so the docs root won't be detected by find_packages() # Display sane URLs in the docs: STATIC_URL = "/static/" # Avoid error for missing the secret key SECRET_KEY = "docs" INSTALLED_APPS = ["django.contrib.contenttypes"] django-polymorphic-4.10.2/docs/_static/000077500000000000000000000000001513173623500177675ustar00rootroot00000000000000django-polymorphic-4.10.2/docs/_static/.gitkeep000066400000000000000000000000001513173623500214060ustar00rootroot00000000000000django-polymorphic-4.10.2/docs/_static/style.css000066400000000000000000000022121513173623500216360ustar00rootroot00000000000000section#api-documentation div.highlight pre { color: #b30000; display: block; /* ensures it's treated as a block */ margin-left: auto; /* auto margins center block elements */ margin-right: auto; width: fit-content; } body[data-theme="light"] section#api-documentation div.highlight, body[data-theme="light"] section#api-documentation div.highlight pre { background-color: #f8f8f8; } body[data-theme="dark"] section#api-documentation div.highlight, body[data-theme="dark"] section#api-documentation div.highlight pre { background-color: #202020; } /* AUTO → system prefers DARK (acts like dark unless user forced light) */ @media (prefers-color-scheme: dark) { body:not([data-theme="light"]) #api-documentation .highlight, body:not([data-theme="light"]) #api-documentation .highlight pre { background-color: #202020; } } /* AUTO → system prefers LIGHT (acts like light unless user forced dark) */ @media (prefers-color-scheme: light) { body:not([data-theme="dark"]) #api-documentation .highlight, body:not([data-theme="dark"]) #api-documentation .highlight pre { background-color: #f8f8f8; } } django-polymorphic-4.10.2/docs/admin.rst000066400000000000000000000247031513173623500201710ustar00rootroot00000000000000Admin Integration ================= Of course, it's possible to register individual polymorphic models in the :doc:`Django admin interface `. However, to use these models in a single cohesive interface, some extra base classes are available. Setup ----- Both the parent model and child model need to have a :class:`~django.contrib.admin.ModelAdmin` class. The shared base model should use the :class:`~polymorphic.admin.PolymorphicParentModelAdmin` as base class. * :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.base_model` should be set * :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.child_models` or :meth:`~polymorphic.admin.PolymorphicParentModelAdmin.get_child_models` should return an iterable of Model classes. The admin class for every child model should inherit from :class:`~polymorphic.admin.PolymorphicChildModelAdmin` * :attr:`~polymorphic.admin.PolymorphicChildModelAdmin.base_model` should be set. Although the child models are registered too, they won't be shown in the admin index page. This only happens when :attr:`~polymorphic.admin.PolymorphicChildModelAdmin.show_in_index` is set to ``True``. Fieldset configuration ~~~~~~~~~~~~~~~~~~~~~~ The parent admin is only used for the list display of models, and for the edit/delete view of non-subclassed models. All other model types are redirected to the edit/delete/history view of the child model admin. Hence, the fieldset configuration should be placed on the child admin. .. tip:: When the child admin is used as base class for various derived classes, avoid using the standard ``ModelAdmin`` attributes ``form`` and ``fieldsets``. Instead, use the ``base_form`` and ``base_fieldsets`` attributes. This allows the :class:`~polymorphic.admin.PolymorphicChildModelAdmin` class to detect any additional fields in case the child model is overwritten. .. versionchanged:: 1.0 It's now needed to register the child model classes too. In :pypi:`django-polymorphic` 0.9 and below, the :meth:`~polymorphic.admin.PolymorphicParentModelAdmin.child_models` was a tuple of a (:class:`~django.db.models.Model`, :class:`~polymorphic.admin.PolymorphicChildModelAdmin`). The admin classes were registered in an internal class, and kept away from the main admin site. This caused various subtle problems with the :class:`~django.db.models.ManyToManyField` and related field wrappers, which are fixed by registering the child admin classes too. Note that they are hidden from the main view, unless :attr:`~polymorphic.admin.PolymorphicChildModelAdmin.show_in_index` is set. .. _admin-example: Example ------- The models are taken from :ref:`advanced-features`. .. code-block:: python from django.contrib import admin from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter from .models import ModelA, ModelB, ModelC, StandardModel class ModelAChildAdmin(PolymorphicChildModelAdmin): """ Base admin class for all child models """ base_model = ModelA # Optional, explicitly set here. # By using these `base_...` attributes instead of the regular ModelAdmin `form` and `fieldsets`, # the additional fields of the child models are automatically added to the admin form. base_form = ... base_fieldsets = ( ... ) @admin.register(ModelB) class ModelBAdmin(ModelAChildAdmin): base_model = ModelB # Explicitly set here! # define custom features here @admin.register(ModelC) class ModelCAdmin(ModelBAdmin): base_model = ModelC # Explicitly set here! show_in_index = True # makes child model admin visible in main admin site # define custom features here @admin.register(ModelA) class ModelAParentAdmin(PolymorphicParentModelAdmin): """ The parent model admin """ base_model = ModelA # Optional, explicitly set here. child_models = (ModelB, ModelC) list_filter = (PolymorphicChildModelFilter,) # This is optional. Filtering child types --------------------- Child model types can be filtered by adding a :class:`~polymorphic.admin.PolymorphicChildModelFilter` to the :attr:`~django.contrib.admin.ModelAdmin.list_filter` attribute. See the example above. Inline models ------------- .. versionadded:: 1.0 Inline models are handled via a special :class:`~polymorphic.admin.StackedPolymorphicInline` class. For models with a generic foreign key, there is a :class:`~polymorphic.admin.GenericStackedPolymorphicInline` class available. When the inline is included to a normal :class:`~django.contrib.admin.ModelAdmin`, make sure the :class:`~polymorphic.admin.PolymorphicInlineSupportMixin` is included. This is not needed when the admin inherits from the :class:`~polymorphic.admin.PolymorphicParentModelAdmin` or :class:`~polymorphic.admin.PolymorphicChildModelAdmin` classes. In the following example, the ``PaymentInline`` supports several types. These are defined as separate inline classes. The child classes can be nested for clarity, but this is not a requirement. .. code-block:: python from django.contrib import admin from polymorphic.admin import PolymorphicInlineSupportMixin, StackedPolymorphicInline from .models import Order, Payment, CreditCardPayment, BankPayment, SepaPayment class PaymentInline(StackedPolymorphicInline): """ An inline for a polymorphic model. The actual form appearance of each row is determined by the child inline that corresponds with the actual model type. """ class CreditCardPaymentInline(StackedPolymorphicInline.Child): model = CreditCardPayment class BankPaymentInline(StackedPolymorphicInline.Child): model = BankPayment class SepaPaymentInline(StackedPolymorphicInline.Child): model = SepaPayment model = Payment child_inlines = ( CreditCardPaymentInline, BankPaymentInline, SepaPaymentInline, ) @admin.register(Order) class OrderAdmin(PolymorphicInlineSupportMixin, admin.ModelAdmin): """ Admin for orders. The inline is polymorphic. To make sure the inlines are properly handled, the ``PolymorphicInlineSupportMixin`` is needed to """ inlines = (PaymentInline,) Using polymorphic models in standard inlines ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To add a polymorphic child model as an Inline for another model, add a field to the inline's :attr:`~django.contrib.admin.ModelAdmin.readonly_fields` list formed by the lowercased name of the polymorphic parent model with the string ``_ptr`` appended to it. Otherwise, trying to save that model in the admin will raise an :exc:`AttributeError` with the message "can't set attribute". .. code-block:: python from django.contrib import admin from .models import StandardModel class ModelBInline(admin.StackedInline): model = ModelB fk_name = 'modelb' readonly_fields = ['modela_ptr'] @admin.register(StandardModel) class StandardModelAdmin(admin.ModelAdmin): inlines = [ModelBInline] Internal details ---------------- The polymorphic admin interface works in a simple way: * The add screen gains an additional step where the desired child model is selected. * The edit screen displays the admin interface of the child model. * The list screen still displays all objects of the base class. The polymorphic admin is implemented via a parent admin that redirects the ``edit`` and ``delete`` views to the :class:`~django.contrib.admin.ModelAdmin` of the derived child model. The ``list`` page is still implemented by the parent model admin. The parent model ~~~~~~~~~~~~~~~~ The parent model needs to inherit :class:`~polymorphic.admin.PolymorphicParentModelAdmin`, and implement the following: * :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.base_model` should be set * :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.child_models` or :meth:`~polymorphic.admin.PolymorphicParentModelAdmin.get_child_models` should return an iterable of Model classes. The exact implementation can depend on the way your module is structured. For simple inheritance situations, :meth:`~polymorphic.admin.PolymorphicParentModelAdmin.child_models` is the best solution. For large applications, :meth:`~polymorphic.admin.PolymorphicParentModelAdmin.get_child_models` can be used to query a plugin registration system. By default, the :meth:`~polymorphic.managers.PolymorphicQuerySet.non_polymorphic` method will be called on the queryset, so only the Parent model will be provided to the list template. This is to avoid the performance hit of retrieving child models. This can be controlled by setting the :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.polymorphic_list` property on the parent admin. Setting it to True will provide child models to the list template. If you use other applications such as django-reversion_ or django-mptt_, please check :ref:`integrations`. Note: If you are using non-integer primary keys in your model, you have to edit :attr:`~polymorphic.admin.PolymorphicParentModelAdmin.pk_regex`, for example ``pk_regex = '([\w-]+)'`` if you use :class:`~uuid.UUID` primary keys. Otherwise you cannot change model entries. The child models ~~~~~~~~~~~~~~~~ The admin interface of the derived models should inherit from :class:`~polymorphic.admin.PolymorphicChildModelAdmin`. Again, :attr:`~polymorphic.admin.PolymorphicChildModelAdmin.base_model` should be set in this class as well. This class implements the following features: * It corrects the breadcrumbs in the admin pages. * It extends the template lookup paths, to look for both the parent model and child model in the ``admin/app/model/change_form.html`` path. * It allows to set :attr:`~polymorphic.admin.PolymorphicChildModelAdmin.base_form` so the derived class will automatically include other fields in the form. * It allows to set :attr:`~polymorphic.admin.PolymorphicChildModelAdmin.base_fieldsets` so the derived class will automatically display any extra fields. * Although it must be registered with admin site, by default it's hidden from admin site index page. This can be overridden by adding :attr:`~polymorphic.admin.PolymorphicChildModelAdmin.show_in_index` = ``True`` in admin class. .. _django-reversion: https://github.com/etianen/django-reversion .. _django-mptt: https://github.com/django-mptt/django-mptt django-polymorphic-4.10.2/docs/advanced.rst000066400000000000000000000436121513173623500206460ustar00rootroot00000000000000.. _advanced-features: Advanced Features ================= In the examples below, these models are being used: .. code-block:: python from django.db import models from polymorphic.models import PolymorphicModel class ModelA(PolymorphicModel): field1 = models.CharField(max_length=10) class ModelB(ModelA): field2 = models.CharField(max_length=10) class ModelC(ModelB): field3 = models.CharField(max_length=10) Filtering for classes (equivalent to python's :func:`isinstance`): ------------------------------------------------------------------ .. code-block:: python >>> ModelA.objects.instance_of(ModelB) [ , ] In general, including or excluding parts of the inheritance tree: .. code-block:: python ModelA.objects.instance_of(ModelB [, ModelC ...]) ModelA.objects.not_instance_of(ModelB [, ModelC ...]) You can also use this feature in Q-objects (with the same result as above): .. code-block:: python >>> ModelA.objects.filter( Q(instance_of=ModelB) ) Polymorphic filtering (for fields in inherited classes) ------------------------------------------------------- For example, cherry-picking objects from multiple derived classes anywhere in the inheritance tree, using Q objects (with the syntax: ``exact model name + three _ + field name``): .. code-block:: python >>> ModelA.objects.filter( Q(ModelB___field2 = 'B2') | Q(ModelC___field3 = 'C3') ) [ , ] Combining Querysets ------------------- Querysets could now be regarded as object containers that allow the aggregation of different object types, very similar to python lists - as long as the objects are accessed through the manager of a common base class: .. code-block:: python >>> Base.objects.instance_of(ModelX) | Base.objects.instance_of(ModelY) [ , ] ManyToManyField, ForeignKey, OneToOneField ------------------------------------------ Relationship fields referring to polymorphic models work as expected: like polymorphic querysets they now always return the referred objects with the same type/class these were created and saved as. E.g., if in your model you define: .. code-block:: python field1 = OneToOneField(ModelA) then field1 may now also refer to objects of type ``ModelB`` or ``ModelC``. A :class:`~django.db.models.ManyToManyField` example: .. code-block:: python # The model holding the relation may be any kind of model, polymorphic or not class RelatingModel(models.Model): # ManyToMany relation to a polymorphic model many2many = models.ManyToManyField('ModelA') >>> o=RelatingModel.objects.create() >>> o.many2many.add(ModelA.objects.get(id=1)) >>> o.many2many.add(ModelB.objects.get(id=2)) >>> o.many2many.add(ModelC.objects.get(id=3)) >>> o.many2many.all() [ , , ] Copying Polymorphic objects --------------------------- **Copying polymorphic models is no different than copying regular multi-table models.** You have two options: 1. Use :meth:`~django.db.models.query.QuerySet.create` and provide all field values from the original instance except the primary key(s). 2. Set the primary key attribute, and parent table pointers at all levels of inheritance to ``None`` and call :meth:`~django.db.models.Model.save`. The Django documentation :ref:`offers some discussion on copying `, including the complexity around related fields and multi-table inheritance. :pypi:`django-polymorphic` offers a utility function :func:`~polymorphic.utils.prepare_for_copy` that resets all necessary fields on a model instance to prepare it for copying: .. code-block:: python from polymorphic.utils import prepare_for_copy obj = ModelB.objects.first() prepare_for_copy(obj) obj.save() # obj is now a copy of the original ModelB instance Working with Fixtures --------------------- Polymorphic models work with Django's :django-admin:`dumpdata` and :django-admin:`loaddata` commands just as regular models do. There are two important considerations: 1. Polymorphic models are multi-table models and :django-admin:`dumpdata` serializes each table separately. :pypi:`django-polymorphic` `does it's best `_ to ensure non-polymorphic managers are used when creating fixtures but there may be edge cases where this fails. If you override :django-admin:`dumpdata` you must make sure any polymorphic managers encountered :meth:`toggle polymorphism off `. Other usual multi-table model caveats apply. If you serialize a subset of tables in the model inheritance you may generate corrupt data or "upcast" your models if child tables were omitted. 2. Polymorphic models rely on the :class:`~django.contrib.contenttypes.models.ContentType` framework. When serializing and deserializing polymorphic models, the ``polymorphic_ctype`` field must be handled correctly. If there is any question about if the content type primary keys are or will be different between the source and target database you should use the :option:`--natural-foreign ` flag to serialize those relations by-value. Polymorphism introduces no special consideration here - any model using contenttypes, polymorphic or not, must handle this correctly. .. note:: Prior documentation urged users to use both :option:`--natural-primary ` and :option:`--natural-foreign ` flags when dumping polymorphic models. This is not necessary and only needs to be done when the primary keys are not guaranteed to match or be available at the target database. Loading Fixtures (loaddata) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Fixtures should be loadable as normal with :django-admin:`loaddata`. However, if there are problems with the ``polymorphic_ctype`` references, you may fix them using :func:`~polymorphic.utils.reset_polymorphic_ctype`: .. code-block:: python from polymorphic.utils import reset_polymorphic_ctype from myapp.models import Animal, Dog, Cat # Reset polymorphic_ctype for all models in the inheritance tree reset_polymorphic_ctype(Animal, Dog, Cat) Using Third Party Models (without modifying them) ------------------------------------------------- Third party models can be used as polymorphic models without restrictions by subclassing them. E.g. using a third party model as the root of a polymorphic inheritance tree: .. code-block:: python from thirdparty import ThirdPartyModel class MyThirdPartyBaseModel(PolymorphicModel, ThirdPartyModel): pass # or add fields Or instead integrating the third party model anywhere into an existing polymorphic inheritance tree: .. code-block:: python class MyBaseModel(SomePolymorphicModel): my_field = models.CharField(max_length=10) class MyModelWithThirdParty(MyBaseModel, ThirdPartyModel): pass # or add fields Non-Polymorphic Queries ----------------------- If you insert :meth:`~polymorphic.managers.PolymorphicQuerySet.non_polymorphic` anywhere into the query chain, then :pypi:`django-polymorphic` will simply leave out the final step of retrieving the real objects, and the manager/queryset will return objects of the type of the base class you used for the query, like vanilla Django would (``ModelA`` in this example). .. code-block:: python >>> qs=ModelA.objects.non_polymorphic().all() >>> qs [ , , ] There are no other changes in the behaviour of the queryset. For example, enhancements for ``filter()`` or ``instance_of()`` etc. still work as expected. If you do the final step yourself, you get the usual polymorphic result: .. code-block:: python >>> ModelA.objects.get_real_instances(qs) [ , , ] About Queryset Methods ---------------------- * :meth:`~django.db.models.query.QuerySet.annotate` and :meth:`~django.db.models.query.QuerySet.aggregate` work just as usual, with the addition that the ``ModelX___field`` syntax can be used for the keyword arguments (but not for the non-keyword arguments). * :meth:`~django.db.models.query.QuerySet.order_by` similarly supports the ``ModelX___field`` syntax for specifying ordering through a field in a submodel. * :meth:`~django.db.models.query.QuerySet.distinct` works as expected. It only regards the fields of the base class, but this should never make a difference. * :meth:`~django.db.models.query.QuerySet.select_related` works just as usual, but it can not (yet) be used to select relations in inherited models (like ``ModelA.objects.select_related('ModelC___fieldxy')`` ) * :meth:`~django.db.models.query.QuerySet.extra` works as expected (it returns polymorphic results) but currently has one restriction: The resulting objects are required to have a unique primary key within the result set - otherwise an error is thrown (this case could be made to work, however it may be mostly unneeded).. The keyword-argument "polymorphic" is no longer supported. You can get back the old non-polymorphic behaviour by using ``ModelA.objects.non_polymorphic().extra(...)``. * :meth:`~polymorphic.managers.PolymorphicQuerySet.get_real_instances` allows you to turn a queryset or list of base model objects efficiently into the real objects. For example, you could do ``base_objects_queryset=ModelA.extra(...).non_polymorphic()`` and then call ``real_objects=base_objects_queryset.get_real_instances()``. Or alternatively ``real_objects=ModelA.objects.get_real_instances(base_objects_queryset_or_object_list)`` * :meth:`~django.db.models.query.QuerySet.values` & :meth:`~django.db.models.query.QuerySet.values_list` currently do not return polymorphic results. This may change in the future however. If you want to use these methods now, it's best if you use ``Model.base_objects.values...`` as this is guaranteed to not change. * :meth:`~django.db.models.query.QuerySet.defer` and :meth:`~django.db.models.query.QuerySet.only` work as expected. On Django 1.5+ they support the ``ModelX___field`` syntax, but on Django 1.4 it is only possible to pass fields on the base model into these methods. Using enhanced Q-objects in any Places -------------------------------------- The queryset enhancements (e.g. :meth:`~polymorphic.managers.PolymorphicQuerySet.instance_of`) only work as arguments to the member functions of a polymorphic queryset. Occasionally it may be useful to be able to use Q objects with these enhancements in other places. As Django doesn't understand these enhanced Q objects, you need to transform them manually into normal Q objects before you can feed them to a Django queryset or function: .. code-block:: python normal_q_object = ModelA.translate_polymorphic_Q_object( Q(instance_of=Model2B) ) This function cannot be used at model creation time however (in models.py), as it may need to access the ContentTypes database table. Nicely Displaying Polymorphic Querysets --------------------------------------- In order to get the output as seen in all examples here, you need to use the :class:`~polymorphic.showfields.ShowFieldType` class mixin: .. code-block:: python from polymorphic.models import PolymorphicModel from polymorphic.showfields import ShowFieldType class ModelA(ShowFieldType, PolymorphicModel): field1 = models.CharField(max_length=10) You may also use :class:`~polymorphic.showfields.ShowFieldContent` or :class:`~polymorphic.showfields.ShowFieldTypeAndContent` to display additional information when printing querysets (or converting them to text). When showing field contents, they will be truncated to 20 characters. You can modify this behavior by setting a class variable in your model like this: .. code-block:: python class ModelA(ShowFieldType, PolymorphicModel): polymorphic_showfield_max_field_width = 20 ... Similarly, pre-V1.0 output formatting can be re-estated by using ``polymorphic_showfield_old_format = True``. Create Children from Parents (Downcasting) ------------------------------------------ You can create an instance of a subclass from an existing instance of a superclass using the :meth:`~polymorphic.managers.PolymorphicManager.create_from_super` method of the subclass's manager. For example: .. code-block:: python super_instance = ModelA.objects.get(id=1) sub_instance = ModelB.objects.create_from_super(super_instance, field2='value2') The restriction is that ``super_instance`` must be an instance of the direct superclass of ``ModelB``, and any required fields of ``ModelB`` must be provided as keyword arguments. If multiple levels of subclassing are involved, you must call this method multiple times to "promote" each level. Delete Children, Leaving Parents (Upcasting) -------------------------------------------- The reverse operation of :meth:`~polymorphic.managers.PolymorphicManager.create_from_super` is to delete the subclass instance while keeping the superclass instance. This can be done using the ``keep_parents=True`` argument to :meth:`~django.db.models.Model.delete`. :pypi:`django-polymorphic` ensures that the ``polymorphic_ctype`` fields of the superclass instances are updated accordingly when doing this. .. _restrictions: Restrictions & Caveats ---------------------- * Database Performance regarding concrete Model inheritance in general. Please see :ref:`performance`. * Queryset methods :meth:`~django.db.models.query.QuerySet.values`, :meth:`~django.db.models.query.QuerySet.values_list`, and :meth:`~django.db.models.query.QuerySet.select_related` are not yet fully supported (see above). :meth:`~django.db.models.query.QuerySet.extra` has one restriction: the resulting objects are required to have a unique primary key within the result set. * Diamond shaped inheritance: There seems to be a general problem with diamond shaped multiple model inheritance with Django models (tested with V1.1 - V1.3). An example `is here `_. This problem is aggravated when trying to enhance :class:`~django.db.models.Model` by subclassing it instead of modifying Django core (as we do here with :class:`~polymorphic.models.PolymorphicModel`). * The enhanced filter-definitions/Q-objects only work as arguments for the methods of the polymorphic querysets. Please see above for ``translate_polymorphic_Q_object``. * When using the :django-admin:`dumpdata` management command on polymorphic tables (or any table that has a reference to :class:`~django.contrib.contenttypes.models.ContentType`), include the :option:`--natural-primary ` and :option:`--natural-foreign ` flags in the arguments. * If the ``polymorphic_ctype_id`` on the base table points to the wrong :class:`~django.contrib.contenttypes.models.ContentType` (this can happen if you delete child rows manually with raw SQL, ``DELETE FROM table``), then polymorphic queries will elide the corresponding model objects: * ``BaseClass.objects.all()`` will **exclude** these rows (it filters for existing child types). * ``BaseClass.objects.non_polymorphic().all()`` will behave as normal - but polymorphic behavior for the affected rows will be undefined - for instance, :meth:`~polymorphic.managers.PolymorphicQuerySet.get_real_instances` will raise an exception. Always use ``instance.delete()`` or ``QuerySet.delete()`` to ensure cascading deletion of the base row. If you must delete manually, ensure you also delete the corresponding row from the base table. .. old links: - http://code.djangoproject.com/wiki/ModelInheritance - http://lazypython.blogspot.com/2009/02/second-look-at-inheritance-and.html - http://www.djangosnippets.org/snippets/1031/ - http://www.djangosnippets.org/snippets/1034/ - http://groups.google.com/group/django-developers/browse_frm/thread/7d40ad373ebfa912/a20fabc661b7035d?lnk=gst&q=model+inheritance+CORBA#a20fabc661b7035d - http://groups.google.com/group/django-developers/browse_thread/thread/9bc2aaec0796f4e0/0b92971ffc0aa6f8?lnk=gst&q=inheritance#0b92971ffc0aa6f8 - http://groups.google.com/group/django-developers/browse_thread/thread/3947c594100c4adb/d8c0af3dacad412d?lnk=gst&q=inheritance#d8c0af3dacad412d - http://groups.google.com/group/django-users/browse_thread/thread/52f72cffebb705e/b76c9d8c89a5574f - http://peterbraden.co.uk/article/django-inheritance - http://www.hopelessgeek.com/2009/11/25/a-hack-for-multi-table-inheritance-in-django - http://stackoverflow.com/questions/929029/how-do-i-access-the-child-classes-of-an-object-in-django-without-knowing-the-name/929982#929982 - http://stackoverflow.com/questions/1581024/django-inheritance-how-to-have-one-method-for-all-subclasses - http://groups.google.com/group/django-users/browse_thread/thread/cbdaf2273781ccab/e676a537d735d9ef?lnk=gst&q=polymorphic#e676a537d735d9ef - http://groups.google.com/group/django-users/browse_thread/thread/52f72cffebb705e/bc18c18b2e83881e?lnk=gst&q=model+inheritance#bc18c18b2e83881e - http://code.djangoproject.com/ticket/10808 - http://code.djangoproject.com/ticket/7270 django-polymorphic-4.10.2/docs/api/000077500000000000000000000000001513173623500171125ustar00rootroot00000000000000django-polymorphic-4.10.2/docs/api/index.rst000066400000000000000000000006131513173623500207530ustar00rootroot00000000000000API Documentation ================= .. _base: .. automodule:: polymorphic :members: :show-inheritance: :inherited-members: .. toctree:: polymorphic.admin polymorphic.contrib/index polymorphic.formsets polymorphic.managers polymorphic.models polymorphic.deletion polymorphic.query polymorphic.showfields polymorphic.templatetags/index polymorphic.utils django-polymorphic-4.10.2/docs/api/polymorphic.admin.rst000066400000000000000000000036371513173623500233110ustar00rootroot00000000000000polymorphic.admin ================= ModelAdmin classes ------------------ The ``PolymorphicParentModelAdmin`` class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: polymorphic.admin.PolymorphicParentModelAdmin :members: :undoc-members: :show-inheritance: The ``PolymorphicChildModelAdmin`` class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: polymorphic.admin.PolymorphicChildModelAdmin :members: :undoc-members: :show-inheritance: List filtering -------------- The ``PolymorphicChildModelFilter`` class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: polymorphic.admin.PolymorphicChildModelFilter :show-inheritance: Inlines support --------------- The ``StackedPolymorphicInline`` class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: polymorphic.admin.StackedPolymorphicInline :show-inheritance: The ``GenericStackedPolymorphicInline`` class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: polymorphic.admin.GenericStackedPolymorphicInline :members: :undoc-members: :show-inheritance: The ``PolymorphicInlineSupportMixin`` class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: polymorphic.admin.PolymorphicInlineSupportMixin :members: :undoc-members: :show-inheritance: Low-level classes ----------------- These classes are useful when existing parts of the admin classes. .. autoclass:: polymorphic.admin.PolymorphicModelChoiceForm :members: :undoc-members: :show-inheritance: .. autoclass:: polymorphic.admin.PolymorphicInlineModelAdmin :members: :undoc-members: :show-inheritance: .. autoclass:: polymorphic.admin.GenericPolymorphicInlineModelAdmin :members: :undoc-members: :show-inheritance: .. autoclass:: polymorphic.admin.PolymorphicInlineAdminForm :show-inheritance: .. autoclass:: polymorphic.admin.PolymorphicInlineAdminFormSet :show-inheritance: django-polymorphic-4.10.2/docs/api/polymorphic.contrib/000077500000000000000000000000001513173623500231165ustar00rootroot00000000000000django-polymorphic-4.10.2/docs/api/polymorphic.contrib/drf.rst000066400000000000000000000002161513173623500244220ustar00rootroot00000000000000drf.serializers =============== .. automodule:: polymorphic.contrib.drf.serializers :members: :undoc-members: :show-inheritance: django-polymorphic-4.10.2/docs/api/polymorphic.contrib/extra_views.rst000066400000000000000000000002021513173623500262020ustar00rootroot00000000000000extra_views =========== .. automodule:: polymorphic.contrib.extra_views :members: :undoc-members: :show-inheritance: django-polymorphic-4.10.2/docs/api/polymorphic.contrib/guardian.rst000066400000000000000000000001711513173623500254410ustar00rootroot00000000000000guardian ======== .. automodule:: polymorphic.contrib.guardian :members: :undoc-members: :show-inheritance: django-polymorphic-4.10.2/docs/api/polymorphic.contrib/index.rst000066400000000000000000000003061513173623500247560ustar00rootroot00000000000000polymorphic.contrib =================== .. _contrib: .. automodule:: polymorphic.contrib :members: :show-inheritance: :inherited-members: .. toctree:: extra_views guardian drf django-polymorphic-4.10.2/docs/api/polymorphic.deletion.rst000066400000000000000000000004311513173623500240110ustar00rootroot00000000000000polymorphic.deletion ==================== .. automodule:: polymorphic.deletion .. autoclass:: polymorphic.deletion.PolymorphicGuard :members: __call__ :show-inheritance: .. autoclass:: polymorphic.deletion.PolymorphicGuardSerializer :members: :show-inheritance: django-polymorphic-4.10.2/docs/api/polymorphic.formsets.rst000066400000000000000000000016531513173623500240570ustar00rootroot00000000000000polymorphic.formsets ==================== .. automodule:: polymorphic.formsets Model formsets -------------- .. autofunction:: polymorphic.formsets.polymorphic_modelformset_factory .. autoclass:: polymorphic.formsets.PolymorphicFormSetChild Inline formsets --------------- .. autofunction:: polymorphic.formsets.polymorphic_inlineformset_factory Generic formsets ---------------- .. autofunction:: polymorphic.formsets.generic_polymorphic_inlineformset_factory Low-level features ------------------ The internal machinery can be used to extend the formset classes. This includes: .. autofunction:: polymorphic.formsets.polymorphic_child_forms_factory .. autoclass:: polymorphic.formsets.BasePolymorphicModelFormSet :show-inheritance: .. autoclass:: polymorphic.formsets.BasePolymorphicInlineFormSet :show-inheritance: .. autoclass:: polymorphic.formsets.BaseGenericPolymorphicInlineFormSet :show-inheritance: django-polymorphic-4.10.2/docs/api/polymorphic.managers.rst000066400000000000000000000006261513173623500240110ustar00rootroot00000000000000polymorphic.managers ==================== .. automodule:: polymorphic.managers The ``PolymorphicManager`` class -------------------------------- .. autoclass:: polymorphic.managers.PolymorphicManager :members: :show-inheritance: The ``PolymorphicQuerySet`` class --------------------------------- .. autoclass:: polymorphic.managers.PolymorphicQuerySet :members: :show-inheritance: django-polymorphic-4.10.2/docs/api/polymorphic.models.rst000066400000000000000000000002061513173623500234710ustar00rootroot00000000000000polymorphic.models ================== .. automodule:: polymorphic.models :members: :show-inheritance: :private-members: django-polymorphic-4.10.2/docs/api/polymorphic.query.rst000066400000000000000000000001551513173623500233560ustar00rootroot00000000000000polymorphic.query ================= .. automodule:: polymorphic.query :members: :show-inheritance: django-polymorphic-4.10.2/docs/api/polymorphic.showfields.rst000066400000000000000000000002171513173623500243570ustar00rootroot00000000000000polymorphic.showfields ====================== .. automodule:: polymorphic.showfields :members: :show-inheritance: :inherited-members:django-polymorphic-4.10.2/docs/api/polymorphic.templatetags/000077500000000000000000000000001513173623500241505ustar00rootroot00000000000000django-polymorphic-4.10.2/docs/api/polymorphic.templatetags/index.rst000066400000000000000000000002641513173623500260130ustar00rootroot00000000000000polymorphic.templatetags ======================== .. _templatetags: .. automodule:: polymorphic.templatetags .. toctree:: polymorphic_admin_tags polymorphic_formset_tags django-polymorphic-4.10.2/docs/api/polymorphic.templatetags/polymorphic_admin_tags.rst000066400000000000000000000001751513173623500314400ustar00rootroot00000000000000polymorphic_admin_tags ====================== .. automodule:: polymorphic.templatetags.polymorphic_admin_tags :members: django-polymorphic-4.10.2/docs/api/polymorphic.templatetags/polymorphic_formset_tags.rst000066400000000000000000000002031513173623500320170ustar00rootroot00000000000000polymorphic_formset_tags ======================== .. automodule:: polymorphic.templatetags.polymorphic_formset_tags :members: django-polymorphic-4.10.2/docs/api/polymorphic.utils.rst000066400000000000000000000001251513173623500233460ustar00rootroot00000000000000polymorphic.utils ================= .. automodule:: polymorphic.utils :members: django-polymorphic-4.10.2/docs/changelog/000077500000000000000000000000001513173623500202705ustar00rootroot00000000000000django-polymorphic-4.10.2/docs/changelog/drf.rst000066400000000000000000000017031513173623500215760ustar00rootroot00000000000000.. :drf_changelog: drf --- Below is the changelog for the :pypi:`django-rest-polymorphic` before it was included into :pypi:`django-polymorphic`. 0.1.10 (2022-07-17) +++++++++++++++++++ * Allow partial updates without resourcetype. 0.1.9 (2020-04-02) ++++++++++++++++++ * Fix validation error and update versions. 0.1.8 (2018-10-11) ++++++++++++++++++ * Add Python 3.7 and Django 2.1 support. 0.1.7 (2018-06-06) ++++++++++++++++++ 0.1.6 (2018-06-03) ++++++++++++++++++ * Add Django REST Framework 3.8 support. 0.1.5 (2018-03-29) ++++++++++++++++++ * Fix validation for nested serializers. 0.1.4 (2018-02-10) ++++++++++++++++++ * Fix serializer instantiation. 0.1.3 (2018-02-09) ++++++++++++++++++ * Add Python 2.7 support. 0.1.2 (2018-01-30) ++++++++++++++++++ * Add Django 2.0 support. * Drop Django 1.10 support. 0.1.1 (2017-12-08) ++++++++++++++++++ * Add example project. 0.1.0 (2017-12-07) ++++++++++++++++++ * First release on PyPI. django-polymorphic-4.10.2/docs/changelog/index.rst000066400000000000000000001160031513173623500221320ustar00rootroot00000000000000Changelog ========= v4.10.2 (2026-01-14) -------------------- * Fixed `changelog url on pypi is broken `_ v4.10.1 (2026-01-13) -------------------- * Fixed `Django-polymorphic does not update the PolymorphicSerializer's validated_data after running the child's validation method `_ v4.10.0 (2026-01-13) -------------------- This release is primarily an integrations release. Many tests were added for the documented intergrations and a few bugs were fixed. Some warnings were converted to system checks. The two most notable changes are: 1. The :pypi:`django-rest-polymorphic` package is now part of :pypi:`django-polymorphic`. 2. In queries that need content types that are not cached those fetches are `now embedded as subqueries `_. This reduces the total number of queries needed but most importantly it eliminates `a class of errors `_ that could happen if Manager/QuerySet methods that should not but do generate database queries are called before models are loaded (e.g. get_queryset()). Issues ~~~~~~ * Implemented `Bring django-rest-polymorphic into this package. `_ .. tip:: :pypi:`django-rest-polymorphic` is now part of :pypi:`django-polymorphic`. You must update your import paths from ``rest_polymorphic.serializers`` to :mod:`polymorphic.contrib.drf.serializers`. * Fixed `Guardian contenttype integraton hook is extremely brittle `_ * Fixed `Models defined after load break inheritance utility caches `_ * Fixed `get_queryset() results in db queries for proxy models `_ * Implemented `System check error for PolymorphicManager marked as use_in_migrations `_ * Implemented `Manager warnings should be system checks instead. `_ v4.9.0 (2026-01-09) ------------------- .. note:: This update may generate new migrations for your polymorphic models, similar to the following. This is ok and an expected side effect of fixing `#815 `_ .. code-block:: python migrations.AlterModelOptions( name='modelname', options={}, ) * Fixed `PolymorphicModel.base_manager is the same as default_manager when custom default manager is supplied. `_ * Fixed `Use non-polymorphic managers for all invocations of dumpdata `_ * Documented `Fixture usage `_ v4.8.0 (2026-01-08) ------------------- * Fixed `PolymorphicFormSetChild overrides form exclude `_ * Fixed `Issue with polymorphic_ctype when populating polymorphic inline formsets. `_ * Fixed `Nested polymorphic_inline_formsets gives AttributeError: 'NoneType' object has no attribute 'get_real_instance_class' `_ v4.7.0 (2026-01-07) ------------------- Fixed a few outstanding admin bugs, updated documentation and added more admin tests for things like m2m relationships. * Documented `How to handle non-admin polymorphic forms? `_ * Fixed `Admin: add view popup breaks if initial submit has validation error `_ * Fixed `Filters are not preserved in polymorphic parent admin `_ * Fixed `Admin change form doesn't preserve changelist filter `_ v4.6.0 (2026-01-05) ------------------- The release fixes longstanding bugs with respect to expected ORM behavior. For a complete list of changes see the `v4.6.0 release `_. * Fixed `get_real_instance() should also gracefully retry parents on failure (best effort) `_ * Fixed `polymorphic_primary_key_name no longer points to base classes polymorphic field `_ * Fixed `create_from_super needs to run atomically. `_ * Fixed `Support polymorphic models that have different pk fields/values at different levels of the hierarchy. `_ * Fixed `Issue with .delete(keep_parents=True) `_ * Fixed `PolymorphicChildModelAdmin with show_in_index=False in Django Admin with nav_sidebar `_ * Fixed `ForeignKeyViolation when trying to save an entity when using a non-default db `_ * Fixed `Content types pulled from wrong database when using database routers `_ * Fixed `Copying Polymorphic objects for below the first level not working `_ * Fixed `Getting a specific element from a queryset in an post_delete returns None `_ * Fixed `Reverse related object descriptor is overwritten on class `_ v4.3.1, v4.4.2, v4.5.2 (2026-01-01) ----------------------------------- * Fixed `Significant performance regression on polymorphic queryset iteration `_ v4.5.1 (2025-12-24) ------------------- * Fixed `4.5.0 generates a lot of migrations on my project `_ * Fixed `Annotations with F `_ * Fixed `show_in_index=False visibility in admin sites and sidebar `_ v4.5.0 (2025-12-22) ------------------- The release fixes longstanding bugs with respect to deletion of polymorphic models. .. warning:: This version has a bug that generates unnecessary migrations - use 4.5.1 instead! * Implemented `Deletion fixes `_ **This release fixes the longstanding polymorphic deletion bug.** The fix should be transparent and not generate new migrations files. If you experience any issues, please report them. * Fixed `AttributeError Using .alias() On Polymorphic Querysets `_ v4.4.1 (2025-12-15) ------------------- * `Fix infinite recursion bug when using only() `_ v4.4.0 (2025-12-14) ------------------- * Implemented `Add create_from_super method and test `_ * Implemented `Move model definition errors to system checks `_ * Fixed `Fix ordering of stacked inline admin forms on add. `_ * Fixed `Change model definition errors to be system checks `_ * Fixed `Replace deepcopy of the Q object `_ v4.3.0 (2025-12-09) ------------------- * Fixed `Resolve primary key name correctly. `_ * Implemented `Include get_child_inlines() hook in stacked inline admin forms. `_ * Fixed `multi-database support in inheritance accessors. `_ * Fixed `Caching in inheritance accessor functions `_ * Fixed `Foreign key resolves to parent class when using abstract models `_ * Fixed `Support Q expressions that contain subquery expressions `_ v4.2.0 (2025-12-04) ------------------- * Fixed `The objects which were transmogrified aren't initialized correctly if they implement __init__ method. `_ * Implemented `Defer to chunk_size parameter on .iterators for fetching get_real_instances() `_ * Fixed `Show full admin context (breadcrumb and logout nav) in model type selection admin form `_ * Fixed `Issue with Autocomplete Fields in StackedPolymorphicInline.Child Inline `_ * Support Python 3.14 and Django 6.0, drop support for EOL python 3.9, Django 3.2, 4.0, 4.1 and 5.0. * `Modernized package management with new build, test, docs tooling and improved CI `_. v4.1.0 (2025-05-20) ------------------- * `Fixed a bug on Django 5 `_ where `aggregation queries could result in None-type errors `_ * `Use css variables in the admin css `_ v4.0.0 (2025-05-20) ------------------- **There were no breaking changes in this major release** *This was the first release under* `Jazzband `_. There were many updates modernizing the package and incorporating Jazzband standards: * Updates to documentation * Formatting and linting with ruff * Moving to GHA from Travis CI * Switch to pytest Changes that touched the core package code were: * Remove `legacy Django/python version checks `_ * Replace `string formats with f-strings `_ * Removed `deprecated usage of package_resources `_ - as of Python 3.12 package_resources was removed. To get prior releases to work on >3.12 you would also need to install `setuptools `_. * Fixed `multi field lines do not render in the admin `_ * Fixed `dark mode rendering in the polymorphic admin `_ v3.1.0 (2021-11-18) ------------------- * Added support for Django 4.0. * Fixed crash when the admin "add type" view has no choices; will show a permission denied. * Fixed missing ``locale`` folder in sdist. * Fixed missing ``QuerySet.bulk_create(.., ignore_conflicts=True)`` parameter support. * Fixed ``FilteredRelation`` support. * Fixed supporting class keyword arguments in model definitions for ``__init_subclass__()``. * Fixed including ``polymorphic.tests.migrations`` in the sdist. * Fixed non-polymorphic parent handling, which has no ``_base_objects``. * Fixed missing ``widgets`` support for ``modelform_factory()``. * Fixed ``has_changed`` handling for ``polymorphic_ctype_id`` due to implicit str to int conversions. * Fixed ``Q`` object handling when lists are used (e.g. in django-advanced-filters_). * Fixed Django Admin support when using a script-prefix. Many thanks to everyone providing clear pull requests! v3.0.0 (2020-08-21) ------------------- * Support for Django 3.X * Dropped support for python 2.X * A lot of various fixes and improvements by various authors. Thanks a lot! v2.1.2 (2019-07-15) ------------------- * Fix ``PolymorphicInlineModelAdmin`` media jQuery include for Django 2.0+ v2.1.1 (2019-07-15) ------------------- * Fixed admin import error due to ``isort`` changes. v2.1 (2019-07-15) ----------------- * Added Django 2.2 support. * Changed ``.non_polymorphic()``, to use a different iterable class that completely circumvent polymorphic. * Changed SQL for ``instance_of`` filter: use ``IN`` statement instead of ``OR`` clauses. * Changed queryset iteration to implement ``prefetch_related()`` support. * Fixed Django 3.0 alpha compatibility. * Fixed compatibility with current django-extra-views_ in ``polymorphic.contrib.extra_views``. * Fixed ``prefetch_related()`` support on polymorphic M2M relations. * Fixed model subclass ``___`` selector for abstract/proxy models. * Fixed model subclass ``___`` selector for models with a custom ``OneToOneField(parent_link=True)``. * Fixed unwanted results on calling ``queryset.get_real_instances([])``. * Fixed unwanted ``TypeError`` exception when ``PolymorphicTypeInvalid`` should have raised. * Fixed hiding the add-button of polymorphic lines in the Django admin. * Reformatted all files with black v2.0.3 (2018-08-24) ------------------- * Fixed admin crash for Django 2.1 with missing ``use_required_attribute``. v2.0.2 (2018-02-05) ------------------- * Fixed manager inheritance behavior for Django 1.11, by automatically enabling ``Meta.manager_inheritance_from_future`` if it's not defined. This restores the manager inheritance behavior that *django-polymorphic 1.3* provided for Django 1.x projects. * Fixed internal ``base_objects`` usage. v2.0.1 (2018-02-05) ------------------- * Fixed manager inheritance detection for Django 1.11. It's recommended to use ``Meta.manager_inheritance_from_future`` so Django 1.x code also inherit the ``PolymorphicManager`` in all subclasses. Django 2.0 already does this by default. * Deprecated the ``base_objects`` manager. Use ``objects.non_polymorphic()`` instead. * Optimized detection for dumpdata behavior, avoiding the performance hit of ``__getattribute__()``. * Fixed test management commands v2.0.0 (2018-01-22) ------------------- * **BACKWARDS INCOMPATIBILITY:** Dropped Django 1.8 and 1.10 support. * **BACKWARDS INCOMPATIBILITY:** Removed old deprecated code from 1.0, thus: * Import managers from ``polymorphic.managers`` (plural), not ``polymorphic.manager``. * Register child models to the admin as well using ``@admin.register()`` or ``admin.site.register()``, as this is no longer done automatically. * Added Django 2.0 support. Also backported into 1.3.1: * Added ``PolymorphicTypeUndefined`` exception for incomplete imported models. When a data migration or import creates an polymorphic model, the ``polymorphic_ctype_id`` field should be filled in manually too. The ``polymorphic.utils.reset_polymorphic_ctype`` function can be used for that. * Added ``PolymorphicTypeInvalid`` exception when database was incorrectly imported. * Added ``polymorphic.utils.get_base_polymorphic_model()`` to find the base model for types. * Using ``base_model`` on the polymorphic admins is no longer required, as this can be autodetected. * Fixed manager errors for swappable models. * Fixed ``deleteText`` of ``|as_script_options`` template filter. * Fixed ``.filter(applabel__ModelName___field=...)`` lookups. * Fixed proxy model support in formsets. * Fixed error with .defer and child models that use the same parent. * Fixed error message when ``polymorphic_ctype_id`` is null. * Fixed fieldsets recursion in the admin. * Improved ``polymorphic.utils.reset_polymorphic_ctype()`` to accept models in random ordering. * Fix fieldsets handling in the admin (``declared_fieldsets`` is removed since Django 1.9) v1.3.1 (2018-04-16) ------------------- Backported various fixes from 2.x to support older Django versions: * Added ``PolymorphicTypeUndefined`` exception for incomplete imported models. When a data migration or import creates an polymorphic model, the ``polymorphic_ctype_id`` field should be filled in manually too. The ``polymorphic.utils.reset_polymorphic_ctype`` function can be used for that. * Added ``PolymorphicTypeInvalid`` exception when database was incorrectly imported. * Added ``polymorphic.utils.get_base_polymorphic_model()`` to find the base model for types. * Using ``base_model`` on the polymorphic admins is no longer required, as this can be autodetected. * Fixed manager errors for swappable models. * Fixed ``deleteText`` of ``|as_script_options`` template filter. * Fixed ``.filter(applabel__ModelName___field=...)`` lookups. * Fixed proxy model support in formsets. * Fixed error with .defer and child models that use the same parent. * Fixed error message when ``polymorphic_ctype_id`` is null. * Fixed fieldsets recursion in the admin. * Improved ``polymorphic.utils.reset_polymorphic_ctype()`` to accept models in random ordering. * Fix fieldsets handling in the admin (``declared_fieldsets`` is removed since Django 1.9) v1.3.0 (2017-08-01) ------------------- * **BACKWARDS INCOMPATIBILITY:** Dropped Django 1.4, 1.5, 1.6, 1.7, 1.9 and Python 2.6 support. Only official Django releases (1.8, 1.10, 1.11) are supported now. * Allow expressions to pass unchanged in ``.order_by()`` * Fixed Django 1.11 accessor checks (to support subclasses of ``ForwardManyToOneDescriptor``, like ``ForwardOneToOneDescriptor``) * Fixed polib syntax error messages in translations. v1.2.0 (2017-05-01) ------------------- * Django 1.11 support. * Fixed ``PolymorphicInlineModelAdmin`` to explictly exclude ``polymorphic_ctype``. * Fixed Python 3 TypeError in the admin when preserving the query string. * Fixed Python 3 issue due to ``force_unicode()`` usage instead of ``force_text()``. * Fixed ``z-index`` attribute for admin menu appearance. v1.1.0 (2017-02-03) ------------------- * Added class based formset views in ``polymorphic/contrib/extra_views``. * Added helper function ``polymorphic.utils.reset_polymorphic_ctype()``. This eases the migration old existing models to polymorphic. * Fixed Python 2.6 issue. * Fixed Django 1.6 support. v1.0.2 (2016-10-14) ------------------- * Added helper function for django-guardian_; add ``GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_content_type'`` to the project settings to let guardian handles inherited models properly. * Fixed ``polymorphic_modelformset_factory()`` usage. * Fixed Python 3 bug for inline formsets. * Fixed CSS for Grappelli, so model choice menu properly overlaps. * Fixed ``ParentAdminNotRegistered`` exception for models that are registered via a proxy model instead of the real base model. v1.0.1 (2016-09-11) ------------------- * Fixed compatibility with manager changes in Django 1.10.1 v1.0.0 (2016-09-02) ------------------- * Added Django 1.10 support. * Added **admin inline** support for polymorphic models. * Added **formset** support for polymorphic models. * Added support for polymorphic queryset limiting effects on *proxy models*. * Added support for multiple databases with the ``.using()`` method and ``using=..`` keyword argument. * Fixed modifying passed ``Q()`` objects in place. .. note:: This version provides a new method for registering the admin models. While the old method is still supported, we recommend to upgrade your code. The new registration style improves the compatibility in the Django admin. * Register each ``PolymorphicChildModelAdmin`` with the admin site too. * The ``child_models`` attribute of the ``PolymorphicParentModelAdmin`` should be a flat list of all child models. The ``(model, admin)`` tuple is obsolete. Also note that proxy models will now limit the queryset too. Fixed since 1.0b1 (2016-08-10) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Fix formset empty-form display when there are form errors. * Fix formset empty-form hiding for Grappelli_. * Fixed packing ``admin/polymorphic/edit_inline/stacked.html`` in the wheel format. v0.9.2 (2016-05-04) ------------------- * Fix error when using ``date_hierarchy`` field in the admin * Fixed Django 1.10 warning in admin add-type view. v0.9.1 (2016-02-18) ------------------- * Fixed support for ``PolymorphicManager.from_queryset()`` for custom query sets. * Fixed Django 1.7 ``changeform_view()`` redirection to the child admin site. This fixes custom admin code that uses these views, such as django-reversion_'s ``revision_view()`` / ``recover_view()``. * Fixed ``.only('pk')`` field support. * Fixed ``object_history_template`` breadcrumb. **NOTE:** when using django-reversion_ / django-reversion-compare_, make sure to implement a ``admin/polymorphic/object_history.html`` template in your project that extends from ``reversion/object_history.html`` or ``reversion-compare/object_history.html`` respectively. v0.9.0 (2016-02-17) ------------------- * Added ``.only()`` and ``.defer()`` support. * Added support for Django 1.8 complex expressions in ``.annotate()`` / ``.aggregate()``. * Fix Django 1.9 handling of custom URLs. The new change-URL redirect overlapped any custom URLs defined in the child admin. * Fix Django 1.9 support in the admin. * Fix setting an extra custom manager without overriding the ``_default_manager``. * Fix missing ``history_view()`` redirection to the child admin, which is important for django-reversion_ support. See the documentation for hints for `django-reversion-compare support _`. v0.8.1 (2015-12-29) ------------------- * Fixed support for reverse relations for ``relname___field`` when the field starts with an ``_`` character. Otherwise, the query will be interpreted as subclass lookup (``ClassName___field``). v0.8.0 (2015-12-28) ------------------- * Added Django 1.9 compatibility. * Renamed ``polymorphic.manager`` => ``polymorphic.managers`` for consistentcy. * **BACKWARDS INCOMPATIBILITY:** The import paths have changed to support Django 1.9. Instead of ``from polymorphic import X``, you'll have to import from the proper package. For example: .. code-block:: python from polymorphic.models import PolymorphicModel from polymorphic.managers import PolymorphicManager, PolymorphicQuerySet from polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent * **BACKWARDS INCOMPATIBILITY:** Removed ``__version__.py`` in favor of a standard ``__version__`` in ``polymorphic/__init__.py``. * **BACKWARDS INCOMPATIBILITY:** Removed automatic proxying of method calls to the queryset class. Use the standard Django methods instead: .. code-block:: python # In model code: objects = PolymorphicQuerySet.as_manager() # For manager code: MyCustomManager = PolymorphicManager.from_queryset(MyCustomQuerySet) v0.7.2 (2015-10-01) ------------------- * Added ``queryset.as_manager()`` support for Django 1.7/1.8 * Optimize model access for non-dumpdata usage; avoid ``__getattribute__()`` call each time to access the manager. * Fixed 500 error when using invalid PK's in the admin URL, return 404 instead. * Fixed possible issues when using an custom ``AdminSite`` class for the parent object. * Fixed Pickle exception when polymorphic model is cached. v0.7.1 (2015-04-30) ------------------- * Fixed Django 1.8 support for related field widgets. v0.7.0 (2015-04-08) ------------------- * Added Django 1.8 support * Added support for custom primary key defined using ``mybase_ptr = models.OneToOneField(BaseClass, parent_link=True, related_name="...")``. * Fixed Python 3 issue in the admin * Fixed ``_default_manager`` to be consistent with Django, it's now assigned directly instead of using ``add_to_class()`` * Fixed 500 error for admin URLs without a '/', e.g. ``admin/app/parentmodel/id``. * Fixed preserved filter for Django admin in delete views * Removed test noise for diamond inheritance problem (which Django 1.7 detects) v0.6.1 (2014-12-30) ------------------- * Remove Django 1.7 warnings * Fix Django 1.4/1.5 queryset calls on related objects for unknown methods. The ``RelatedManager`` code overrides ``get_query_set()`` while ``__getattr__()`` used the new-style ``get_queryset()``. * Fix validate_model_fields(), caused errors when metaclass raises errors v0.6.0 (2014-10-14) ------------------- * Added Django 1.7 support. * Added permission check for all child types. * **BACKWARDS INCOMPATIBILITY:** the ``get_child_type_choices()`` method receives 2 arguments now (request, action). If you have overwritten this method in your code, make sure the method signature is updated accordingly. v0.5.6 (2014-07-21) ------------------- * Added ``pk_regex`` to the ``PolymorphicParentModelAdmin`` to support non-integer primary keys. * Fixed passing ``?ct_id=`` to the add view for Django 1.6 (fixes compatibility with django-parler_). v0.5.5 (2014-04-29) ------------------- * Fixed ``get_real_instance_class()`` for proxy models (broke in 0.5.4). v0.5.4 (2014-04-09) ------------------- * Fix ``.non_polymorphic()`` to returns a clone of the queryset, instead of effecting the existing queryset. * Fix missing ``alters_data = True`` annotations on the overwritten ``save()`` methods. * Fix infinite recursion bug in the admin with Django 1.6+ * Added detection of bad ``ContentType`` table data. v0.5.3 (2013-09-17) ------------------- * Fix TypeError when ``base_form`` was not defined. * Fix passing ``/admin/app/model/id/XYZ`` urls to the correct admin backend. There is no need to include a ``?ct_id=..`` field, as the ID already provides enough information. v0.5.2 (2013-09-05) ------------------- * Fix Grappelli_ breadcrumb support in the views. * Fix unwanted ``___`` handling in the ORM when a field name starts with an underscore; this detects you meant ``relatedfield__ _underscorefield`` instead of ``ClassName___field``. * Fix missing permission check in the "add type" view. This was caught however in the next step. * Fix admin validation errors related to additional non-model form fields. v0.5.1 (2013-07-05) ------------------- * Add Django 1.6 support. * Fix Grappelli_ theme support in the "Add type" view. v0.5.0 (2013-04-20) ------------------- * Add Python 3.2 and 3.3 support * Fix errors with ContentType objects that don't refer to an existing model. v0.4.2 (2013-04-10) ------------------- * Used proper ``__version__`` marker. v0.4.1 (2013-04-10) ------------------- * Add Django 1.5 and 1.6 support * Add proxy model support * Add default admin ``list_filter`` for polymorphic model type. * Fix queryset support of related objects. * Performed an overall cleanup of the project * **Deprecated** the ``queryset_class`` argument of the ``PolymorphicManager`` constructor, use the class attribute instead. * **Dropped** Django 1.1, 1.2 and 1.3 support v0.4.0 (2013-03-25) ------------------- * Update example project for Django 1.4 * Added tox and Travis configuration v0.3.1 (2013-02-28) ------------------- * SQL optimization, avoid query in pre_save_polymorphic() v0.3.0 (2013-02-28) ------------------- Many changes to the codebase happened, but no new version was released to pypi for years. 0.3 contains fixes submitted by many contributors, huge thanks to everyone! * Added a polymorphic admin interface. * PEP8 and code cleanups by various authors v0.2.0 (2011-04-27) ------------------- The 0.2 release serves as legacy release. It supports Django 1.1 up till 1.4 and Python 2.4 up till 2.7. .. _Grappelli: http://grappelliproject.com/ .. _django-advanced-filters: https://github.com/modlinltd/django-advanced-filters .. _django-extra-views: https://github.com/AndrewIngram/django-extra-views .. _django-guardian: https://github.com/django-guardian/django-guardian .. _django-parler: https://github.com/django-parler/django-parler .. _django-reversion: https://github.com/etianen/django-reversion .. _django-reversion-compare: https://github.com/jedie/django-reversion-compare V1.0 Release Candidate 1 (2011-01-24) ------------------------------------- * Fixed GitHub issue 15 (query result incomplete with inheritance). Thanks to John Debs for reporting and the test case. Renaming, refactoring, new maintainer (2011-12-20) -------------------------------------------------- Since the original author disappeared from the internet, we undertook to maintain and upgrade this piece of software. The latest "legacy" tag should be V1.0-RC-1. Anything above that should be considered experimental and unstable until further notice (there be dragons). New features, bug fixes and other improvements will be added to trunk from now on. V1.0 Beta 2 (2010-11-11) ------------------------ Beta 2 accumulated somewhat more changes than intended, and also has been delayed by DBMS benchmark testing I wanted to do on model inheritance. These benchmarks show that there are considerable problems with concrete model inheritance and contemporary DBM systems. The results will be forthcoming on the google discussion forum. Please also see: http://www.jacobian.org/writing/concrete-inheritance/ The API should be stable now with Beta 2, so it's just about potential bugfixes from now on regarding V1.0. Beta 2 is still intended for testing and development environments and not for production. No complaints have been heard regarding Beta 1 however, and Beta 1 is used on a few production sites by some enterprising users. There will be a release candidate for V1.0 in the very near future. New Features and changes ~~~~~~~~~~~~~~~~~~~~~~~~ * API CHANGE: ``.extra()`` has been re-implemented. Now it's polymorphic by default and works (nearly) without restrictions (please see docs). This is a (very) incompatible API change regarding previous versions of django_polymorphic. Support for the ``polymorphic`` keyword parameter has been removed. You can get back the non-polymorphic behaviour by using ``ModelA.objects.non_polymorphic().extra(...)``. * API CHANGE: ``ShowFieldContent`` and ``ShowFieldTypeAndContent`` now use a slightly different output format. If this causes too much trouble for your test cases, you can get the old behaviour back (mostly) by adding ``polymorphic_showfield_old_format = True`` to your model definitions. ``ShowField...`` now also produces more informative output for custom primary keys. * ``.non_polymorphic()`` queryset member function added. This is preferable to using ``.base_objects...``, as it just makes the resulting queryset non-polymorphic and does not change anything else in the behaviour of the manager used (while ``.base_objects`` is just a different manager). * ``.get_real_instances()``: implementation modified to allow the following more simple and intuitive use:: >>> qs = ModelA.objects.all().non_polymorphic() >>> qs.get_real_instances() which is equivalent to:: >>> ModelA.objects.all() * added member function: ``normal_q_object = ModelA.translate_polymorphic_Q_object(enhanced_q_object)`` * misc changes/improvements Bugfixes ~~~~~~~~ * Custom fields could cause problems when used as the primary key. In inherited models, Django's automatic ".pk" field does not always work correctly for such custom fields: "some_object.pk" and "some_object.id" return different results (which they shouldn't, as pk should always be just an alias for the primary key field). It's unclear yet if the problem lies in Django or the affected custom fields. Regardless, the problem resulting from this has been fixed with a small workaround. "python manage.py test polymorphic" also tests and reports on this problem now. Thanks to Mathieu Steele for reporting and the test case. V1.0 Beta 1 (2010-10-18) ------------------------ This release is mostly a cleanup and maintenance release that also improves a number of minor things and fixes one (non-critical) bug. Some pending API changes and corrections have been folded into this release in order to make the upcoming V1.0 API as stable as possible. This release is also about getting feedback from you in case you don't approve of any of these changes or would like to get additional API fixes into V1.0. The release contains a considerable amount of changes in some of the more critical parts of the software. It's intended for testing and development environments and not for production environments. For these, it's best to wait a few weeks for the proper V1.0 release, to allow some time for any potential problems to show up (if they exist). If you encounter any such problems, please post them in the discussion group or open an issue on GitHub or BitBucket (or send me an email). There also have been a number of minor API changes. Please see the README for more information. New Features ~~~~~~~~~~~~ * official Django 1.3 alpha compatibility * ``PolymorphicModel.__getattribute__`` hack removed. This improves performance considerably as python's __getattribute__ generally causes a pretty large processing overhead. It's gone now. * the ``polymorphic_dumpdata`` management command is not needed anymore and has been disabled, as the regular Django dumpdata command now automatically works correctly with polymorphic models (for all supported versions of Django). * ``.get_real_instances()`` has been elevated to an official part of the API:: real_objects = ModelA.objects.get_real_instances(base_objects_list_or_queryset) allows you to turn a queryset or list of base objects into a list of the real instances. This is useful if e.g. you use ``ModelA.base_objects.extra(...)`` and then want to transform the result to its polymorphic equivalent. * ``translate_polymorphic_Q_object`` (see DOCS) * improved testing * Changelog added: CHANGES.rst/html Bugfixes ~~~~~~~~ * Removed requirement for primary key to be an IntegerField. Thanks to Mathieu Steele and Malthe Borch. API Changes ~~~~~~~~~~~ **polymorphic_dumpdata** The management command ``polymorphic_dumpdata`` is not needed anymore and has been disabled, as the regular Django dumpdata command now automatically works correctly with polymorphic models (for all supported versions of Django). **Output of Queryset or Object Printing** In order to improve compatibility with vanilla Django, printing quersets (__repr__ and __unicode__) does not use django_polymorphic's pretty printing by default anymore. To get the old behaviour when printing querysets, you need to replace your model definition: >>> class Project(PolymorphicModel): by: >>> class Project(PolymorphicModel, ShowFieldType): The mixin classes for pretty output have been renamed: ``ShowFieldTypes, ShowFields, ShowFieldsAndTypes`` are now: ``ShowFieldType, ShowFieldContent and ShowFieldTypeAndContent`` (the old ones still exist for compatibility) **Running the Test suite with Django 1.3** Django 1.3 requires ``python manage.py test polymorphic`` instead of just ``python manage.py test``. Beta Release (2010-2-22) ------------------------ IMPORTANT: API Changed (import path changed), and Installation Note The django_polymorphic source code has been restructured and as a result needs to be installed like a normal Django App - either via copying the "polymorphic" directory into your Django project or by running setup.py. Adding 'polymorphic' to INSTALLED_APPS in settings.py is still optional, however. The file `polymorphic.py` cannot be used as a standalone extension module anymore, as is has been split into a number of smaller files. Importing works slightly different now: All relevant symbols are imported directly from 'polymorphic' instead from 'polymorphic.models':: # new way from polymorphic import PolymorphicModel, ... # old way, doesn't work anymore from polymorphic.models import PolymorphicModel, ... + minor API addition: 'from polymorphic import VERSION, get_version' New Features ~~~~~~~~~~~~ Python 2.4 compatibility, contributed by Charles Leifer. Thanks! Bugfixes ~~~~~~~~ Fix: The exception "...has no attribute 'sub_and_superclass_dict'" could be raised. (This occurred if a subclass defined __init__ and accessed class members before calling the superclass __init__). Thanks to Mattias Brändström. Fix: There could be name conflicts if field_name == model_name.lower() or similar. Now it is possible to give a field the same name as the class (like with normal Django models). (Found through the example provided by Mattias Brändström) Beta Release (2010-2-4) ----------------------- New features (and documentation) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ queryset order_by method added queryset aggregate() and extra() methods implemented queryset annotate() method implemented queryset values(), values_list(), distinct() documented; defer(), only() allowed (but not yet supported) setup.py added. Thanks to Andrew Ingram. More about these additions in the docs: http://bserve.webhop.org/wiki/django_polymorphic/doc Bugfixes ~~~~~~~~ * fix remaining potential accessor name clashes (but this only works with Django 1.2+, for 1.1 no changes). Thanks to Andrew Ingram. * fix use of 'id' model field, replaced with 'pk'. * fix select_related bug for objects from derived classes (till now sel.-r. was just ignored) "Restrictions & Caveats" updated ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Django 1.1 only - the names of polymorphic models must be unique in the whole project, even if they are in two different apps. This results from a restriction in the Django 1.1 "related_name" option (fixed in Django 1.2). * Django 1.1 only - when ContentType is used in models, Django's seralisation or fixtures cannot be used. This issue seems to be resolved for Django 1.2 (changeset 11863: Fixed #7052, Added support for natural keys in serialization). Beta Release (2010-1-30) ------------------------ Fixed ContentType related field accessor clash (an error emitted by model validation) by adding related_name to the ContentType ForeignKey. This happened if your polymorphc model used a ContentType ForeignKey. Thanks to Andrew Ingram. Beta Release (2010-1-29) ------------------------ Restructured django_polymorphic into a regular Django add-on application. This is needed for the management commands, and also seems to be a generally good idea for future enhancements as well (and it makes sure the tests are always included). The ``poly`` app - until now being used for test purposes only - has been renamed to ``polymorphic``. See DOCS.rst ("installation/testing") for more info. Beta Release (2010-1-28) ------------------------ Added the polymorphic_dumpdata management command (github issue 4), for creating fixtures, this should be used instead of the normal Django dumpdata command. Thanks to Charles Leifer. Important: Using ContentType together with dumpdata generally needs Django 1.2 (important as any polymorphic model uses ContentType). Beta Release (2010-1-26) ------------------------ IMPORTANT - database schema change (more info in change log). I hope I got this change in early enough before anyone started to use polymorphic.py in earnest. Sorry for any inconvenience. This should be the final DB schema now. Django's ContentType is now used instead of app-label and model-name This is a cleaner and more efficient solution Thanks to Ilya Semenov for the suggestion. .. toctree:: drf django-polymorphic-4.10.2/docs/conf.py000066400000000000000000000235641513173623500176520ustar00rootroot00000000000000# # django-polymorphic documentation build configuration file, created by # sphinx-quickstart on Sun May 19 12:20:47 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import shutil import sys from pathlib import Path import django # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("_ext")) sys.path.insert(0, os.path.abspath("..")) os.environ["DJANGO_SETTINGS_MODULE"] = "polymorphic.tests.settings" django.setup() import polymorphic # noqa: E402 # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.graphviz", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinxcontrib_django", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # -- Project information ----------------------------------------------------- project = polymorphic.__title__ copyright = polymorphic.__copyright__ author = polymorphic.__author__ release = polymorphic.__version__ version = ".".join(polymorphic.__version__.split(".")[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. # pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "furo" html_theme_options = { "source_repository": "https://github.com/jazzband/django-polymorphic/", "source_branch": "master", "source_directory": "docs", } # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] html_css_files = [ "style.css", ] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "django-polymorphicdoc" # -- Options for LaTeX output -------------------------------------------------- latex_engine = "xelatex" latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( "index", "django-polymorphic.tex", "django-polymorphic Documentation", "Bert Constantin, Chris Glass, Diederik van der Boor, Brian Kohan", "manual", ) ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( "index", "django-polymorphic", "django-polymorphic Documentation", ["Bert Constantin, Chris Glass, Diederik van der Boor", "Brian Kohan"], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "django-polymorphic", "django-polymorphic Documentation", "Bert Constantin, Chris Glass, Diederik van der Boor, Brian Kohan", "django-polymorphic", "One line description of project.", "Miscellaneous", ) ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' intersphinx_mapping = { "django": ( "https://docs.djangoproject.com/en/stable", "https://docs.djangoproject.com/en/stable/_objects/", ), "python": ("https://docs.python.org/3", None), "django-guardian": ("https://django-guardian.readthedocs.io/en/stable", None), "django-reversion": ("https://django-reversion.readthedocs.io/en/stable", None), "django-extra-views": ("https://django-extra-views.readthedocs.io/en/stable", None), } # autodoc settings autodoc_default_options = { "show-inheritance": True, # Add other autodoc options here if desired, e.g.: # 'members': True, # 'inherited-members': True, } # In your Sphinx conf.py autodoc_typehints = "description" autodoc_typehints_format = "short" autodoc_class_signature = "separated" autodoc_member_order = "groupwise" # "bysource" def pypi_role(name, rawtext, text, lineno, inliner, options={}, content=[]): from docutils import nodes url = f"https://pypi.org/project/{text}/" node = nodes.reference(rawtext, text, refuri=url, **options) return [node], [] def setup(app): from docutils.parsers.rst import roles # https://sphinxcontrib-typer.readthedocs.io/en/latest/howto.html#build-to-multiple-formats if Path(app.doctreedir).exists(): shutil.rmtree(app.doctreedir) app.add_crossref_type(directivename="django-admin", rolename="django-admin") roles.register_local_role("pypi", pypi_role) return app django-polymorphic-4.10.2/docs/deletion.rst000066400000000000000000000072111513173623500206770ustar00rootroot00000000000000Deletion ======== .. versionadded:: 4.5.0 There is nothing special about deleting polymorphic models. The same rules apply as to :ref:`the deletion of normal Django models ` that have parent/child relationships up and down a model inheritance hierarchy. Django must walk the model inheritance and relationship graph and collect all of the affected objects so that it can correctly order deletion SQL statements to respect database constraints and issue signals. The polymorphic deletion logic is the same as the normal Django deletion logic because Django already walks the model inheritance hierarchy. :class:`~polymorphic.query.PolymorphicQuerySet` and :class:`~polymorphic.managers.PolymorphicManager` disrupt this process by confusing Django's graph walker by returning concrete subclass instances instead of base class instances when it attempts to walk reverse relationships to polymorphic models. To prevent this confusion, :pypi:`django-polymorphic` wraps the :attr:`~django.db.models.ForeignKey.on_delete` handlers of reverse relations to polymorphic models with :class:`~polymorphic.deletion.PolymorphicGuard` which disables polymorphic behavior on the related querysets during collection. **You may define your polymorphic models as you normally would using the standard Django** :attr:`~django.db.models.ForeignKey.on_delete` **actions**. :class:`~polymorphic.models.PolymorphicModel` will automatically wrap the actions for you. actions wrapped with :class:`~polymorphic.deletion.PolymorphicGuard` serialize in migrations as the underlying wrapped action. This ensures migrations generated by versions of :pypi:`django-polymorphic` after 4.5.0 should be the same as with prior versions. The guard is also unnecessary during migrations because Django generates basic managers instead of using the default polymorphic managers. It is a design goal of :pypi:`django-polymorphic` that deletion should just work without any special treatment. However if you encounter attribute errors or database integrity errors during deletion you may manually wrap the :attr:`~django.db.models.ForeignKey.on_delete` action of reverse relations to polymorphic models with :class:`~polymorphic.deletion.PolymorphicGuard` to disable polymorphic behavior during deletion collection. If you encounter an issue like this `please report it to us `_. For example: .. code-block:: python from polymorphic.models import PolymorphicModel from polymorphic.deletion import PolymorphicGuard from django.db import models class MyModel(models.Model): # ... class RelatedModel(PolymorphicModel): my_model = models.ForeignKey( MyModel, on_delete=PolymorphicGuard(models.CASCADE), ) Deleting Children (upcasting) ----------------------------- When deleting a polymorphic model instance, you can choose to keep the parent model instances by passing the ``keep_parents=True`` argument to the :meth:`~polymorphic.models.PolymorphicModel.delete` method. This will delete only the subclass instance, and leave the parent instances intact. :pypi:`django-polymorphic` will ensure that the ``polymorphic_ctype`` fields of the parent instances are updated accordingly to reflect their new concrete model type. .. tip:: You can delete multiple levels of child rows by deleting the model from the desired parent level. For example, if you have a model inheritance hierarchy of ``Base -> ChildA -> ChildB``, and you delete a ``ChildB`` row from its parent model ``ChildA`` instance both the ``ChildA`` and ``ChildB`` rows will be deleted leaving a concrete row type of ``Base``. django-polymorphic-4.10.2/docs/formsets.rst000066400000000000000000000033231513173623500207360ustar00rootroot00000000000000Formsets ======== .. versionadded:: 1.0 Polymorphic models can be used in formsets. The implementation is almost identical to the regular Django :doc:`django:topics/forms/formsets`. As extra parameter, the factory needs to know how to display the child models. Provide a list of :class:`~polymorphic.formsets.PolymorphicFormSetChild` objects for this. .. code-block:: python from polymorphic.formsets import polymorphic_modelformset_factory, PolymorphicFormSetChild ModelAFormSet = polymorphic_modelformset_factory(ModelA, formset_children=( PolymorphicFormSetChild(ModelB), PolymorphicFormSetChild(ModelC), )) The formset can be used just like all other formsets: .. code-block:: python if request.method == "POST": formset = ModelAFormSet(request.POST, request.FILES, queryset=ModelA.objects.all()) if formset.is_valid(): formset.save() else: formset = ModelAFormSet(queryset=ModelA.objects.all()) Like standard Django :doc:`django:topics/forms/formsets`, there are 3 factory methods available: * :func:`~polymorphic.formsets.polymorphic_modelformset_factory` - create a regular model formset. * :func:`~polymorphic.formsets.polymorphic_inlineformset_factory` - create a inline model formset. * :func:`~polymorphic.formsets.generic_polymorphic_inlineformset_factory` - create an inline formset for a generic foreign key. Each one uses a different base class: * :class:`~polymorphic.formsets.BasePolymorphicModelFormSet` * :class:`~polymorphic.formsets.BasePolymorphicInlineFormSet` * :class:`~polymorphic.formsets.BaseGenericPolymorphicInlineFormSet` When needed, the base class can be overwritten and provided to the factory via the ``formset`` parameter. django-polymorphic-4.10.2/docs/index.rst000066400000000000000000000103601513173623500202020ustar00rootroot00000000000000django-polymorphic ================== .. image:: https://img.shields.io/badge/License-BSD-blue.svg :target: https://opensource.org/license/bsd-3-clause :alt: License: BSD .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff .. image:: https://badge.fury.io/py/django-polymorphic.svg :target: https://pypi.python.org/pypi/django-polymorphic/ :alt: PyPI version .. image:: https://img.shields.io/pypi/pyversions/django-polymorphic.svg :target: https://pypi.python.org/pypi/django-polymorphic/ :alt: PyPI pyversions .. image:: https://img.shields.io/pypi/djversions/django-polymorphic.svg :target: https://pypi.org/project/django-polymorphic/ :alt: PyPI Django versions .. image:: https://img.shields.io/pypi/status/django-polymorphic.svg :target: https://pypi.python.org/pypi/django-polymorphic :alt: PyPI status .. image:: https://readthedocs.org/projects/django-polymorphic/badge/?version=latest :target: http://django-polymorphic.readthedocs.io/?badge=latest/ :alt: Documentation Status .. image:: https://img.shields.io/codecov/c/github/jazzband/django-polymorphic/master.svg :target: https://codecov.io/github/jazzband/django-polymorphic?branch=master :alt: Code Coverage .. image:: https://github.com/jazzband/django-polymorphic/actions/workflows/test.yml/badge.svg?branch=master :target: https://github.com/jazzband/django-polymorphic/actions/workflows/test.yml?query=branch:master :alt: Test Status .. image:: https://github.com/jazzband/django-polymorphic/actions/workflows/lint.yml/badge.svg?branch=master :target: https://github.com/jazzband/django-polymorphic/actions/workflows/lint.yml?query=branch:master :alt: Lint Status .. image:: https://img.shields.io/badge/Published%20on-Django%20Packages-0c3c26 :target: https://djangopackages.org/packages/p/django-polymorphic/ :alt: Published on Django Packages .. image:: https://jazzband.co/static/img/badge.svg :target: https://jazzband.co/ :alt: Jazzband :pypi:`django-polymorphic` builds on top of the standard Django model inheritance. It makes using inherited models easier. When a query is made at the base model, the inherited model classes are returned. When we store models that inherit from a ``Project`` model... .. code-block:: python >>> Project.objects.create(topic="Department Party") >>> ArtProject.objects.create(topic="Painting with Tim", artist="T. Turner") >>> ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") ...and want to retrieve all our projects, the subclassed models are returned! .. code-block:: python >>> Project.objects.all() [ , , ] Using vanilla Django, we get the base class objects, which is rarely what we wanted: .. code-block:: python >>> Project.objects.all() [ , , ] Features -------- * Full admin integration. * ORM integration: - Support for ForeignKey, ManyToManyField, OneToOneField descriptors. - Support for proxy models. - Filtering/ordering of inherited models (``ArtProject___artist``). - Filtering model types: :meth:`~polymorphic.managers.PolymorphicQuerySet.instance_of` and :meth:`~polymorphic.managers.PolymorphicQuerySet.not_instance_of` - Combining querysets of different models (``qs3 = qs1 | qs2``) - Support for custom user-defined managers. * Formset support. * Uses the minimum amount of queries needed to fetch the inherited models. * Disabling polymorphic behavior when needed. Getting started --------------- .. toctree:: :maxdepth: 2 quickstart admin performance integrations/index Advanced topics --------------- .. toctree:: :maxdepth: 2 formsets views migrating managers deletion advanced changelog/index api/index Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` django-polymorphic-4.10.2/docs/integrations/000077500000000000000000000000001513173623500210475ustar00rootroot00000000000000django-polymorphic-4.10.2/docs/integrations/djangorestframework.rst000066400000000000000000000100001513173623500256460ustar00rootroot00000000000000.. _django-rest-framework-support: =================== djangorestframework =================== Polymorphic serializers for `Django REST Framework `_. The :pypi:`django-rest-polymorphic` package has been incorporated into :pypi:`django-polymorphic`. This contrib package allows you to easily define serializers for your inherited models that you have created using ``django-polymorphic`` library. To migrate from :pypi:`django-rest-polymorphic`, you need to change your import paths from ``rest_polymorphic.serializers`` to ``polymorphic.contrib.drf.serializers``. Usage ----- Define your polymorphic models: .. literalinclude:: ../../src/polymorphic/tests/examples/integrations/drf/models/example_models.py :language: python :linenos: Define serializers for each polymorphic model the way you did it when you used :pypi:`djangorestframework`: .. literalinclude:: ../../src/polymorphic/tests/examples/integrations/drf/example_serializers.py :language: python :linenos: :lines: 1-26 Note that if you extend `HyperlinkedModelSerializer `_ instead of `ModelSerializer `_ you need to define `extra_kwargs `_ to direct the URL to the appropriate view for your polymorphic serializer. Then you have to create a polymorphic serializer that serves as a mapper between models and serializers which you have defined above: .. literalinclude:: ../../src/polymorphic/tests/examples/integrations/drf/example_serializers.py :language: python :lines: 29- Create viewset with serializer_class equals to your polymorphic serializer: .. literalinclude:: ../../src/polymorphic/tests/examples/integrations/drf/views.py :language: python :linenos: Test it: .. code-block:: bash $ http GET "http://localhost:8000/projects/" .. code-block:: http HTTP/1.0 200 OK Content-Length: 227 Content-Type: application/json [ { "resourcetype": "Project", "topic": "John's gathering" }, { "artist": "T. Turner", "resourcetype": "ArtProject", "topic": "Sculpting with Tim", "url": "http://localhost:8000/projects/2/" }, { "resourcetype": "ResearchProject", "supervisor": "Dr. Winter", "topic": "Swallow Aerodynamics" } ] .. code-block:: bash $ http POST "http://localhost:8000/projects/" resourcetype="ArtProject" topic="Guernica" artist="Picasso" .. code-block:: http HTTP/1.0 201 Created Content-Length: 67 Content-Type: application/json { "artist": "Picasso", "resourcetype": "ArtProject", "topic": "Guernica", "url": "http://localhost:8000/projects/4/" } Customize resource type ----------------------- As you can see from the example above, in order to specify the type of your polymorphic model, you need to send a request with resource type field. The value of resource type should be the name of the model. If you want to change the resource type field name from ``resourcetype`` to something else, you should override ``resource_type_field_name`` attribute: .. code-block:: python class ProjectPolymorphicSerializer(PolymorphicSerializer): resource_type_field_name = 'projecttype' ... If you want to change the behavior of resource type, you should override ``to_resource_type`` method: .. code-block:: python class ProjectPolymorphicSerializer(PolymorphicSerializer): ... def to_resource_type(self, model_or_instance): return model_or_instance._meta.object_name.lower() Now, the request for creating new object will look like this: .. code-block:: bash $ http POST "http://localhost:8000/projects/" projecttype="artproject" topic="Guernica" artist="Picasso" django-polymorphic-4.10.2/docs/integrations/index.rst000066400000000000000000000124751513173623500227210ustar00rootroot00000000000000.. _integrations: Integrations ============ When integrating polymorphic models into third party apps you have three primary options: 0. Hope it just works (it might!). 1. Ensure the querysets the third party apps see are :meth:`not polymorphic `. 2. Override or extend relevant third party app code to work with polymorphic querysets. If it does not just work, option 1 is usually the easiest. We provide some integrations in :mod:`polymorphic.contrib` for popular third party apps and provide guidance for others below. This page does not exhaustively cover all integrations. If you feel your integration need is very common you may consider opening a PR to either provide support in code or documentation here. This page covers supported and tested integration advice. For all other integration advice please refer to `our integrations discussion page `_. For the integration examples on this page, we use the following polymorphic model hierarchy: .. literalinclude:: ../../src/polymorphic/tests/examples/integrations/models.py :language: python :linenos: .. _django-django-guardian-support: django-guardian --------------- .. versionadded:: 1.0.2 No special modifications are required to integrate with :pypi:`django-guardian`. However, if you would like all object level permissions to be managed at the base model level, rather than have unique permissions for each polymorphic subclass, then you can use the helper function :func:`polymorphic.contrib.guardian.get_polymorphic_base_content_type` to unify the permissions for your entire polymorphic model tree into a single namespace a the base level: .. code-block:: python GUARDIAN_GET_CONTENT_TYPE = \ "polymorphic.contrib.guardian.get_polymorphic_base_content_type" This option requires :pypi:`django-guardian` >= 1.4.6. Details about how this option works are available in the `django-guardian documentation `_. .. _django-extra-views-support: django-extra-views ------------------ .. versionadded:: 1.1 The :mod:`polymorphic.contrib.extra_views` package provides classes to display polymorphic formsets using the classes from :pypi:`django-extra-views`. See the documentation of: * :class:`~polymorphic.contrib.extra_views.PolymorphicFormSetView` * :class:`~polymorphic.contrib.extra_views.PolymorphicInlineFormSetView` * :class:`~polymorphic.contrib.extra_views.PolymorphicInlineFormSet` .. tip:: The complete working code for this example can be found `in the extra_views integration test `_. Example View ~~~~~~~~~~~~ Here's how to create a view using :class:`~polymorphic.contrib.extra_views.PolymorphicFormSetView` to handle polymorphic formsets: .. literalinclude:: ../../src/polymorphic/tests/examples/integrations/extra_views/views.py :language: python :linenos: URL Configuration ~~~~~~~~~~~~~~~~~ Configure the URL patterns to route to your formset view: .. literalinclude:: ../../src/polymorphic/tests/examples/integrations/extra_views/urls.py :language: python :linenos: Template ~~~~~~~~ The template for rendering the formset: .. literalinclude:: ../../src/polymorphic/tests/examples/integrations/extra_views/templates/extra_views/article_formset.html :language: html+django ``model_name`` is a template tag implemented like so: .. literalinclude:: ../../src/polymorphic/tests/examples/integrations/extra_views/templatetags/extra_views_tags.py :language: python :lines: 6- .. _django-reversion-support: django-reversion ---------------- Support for :pypi:`django-reversion` works as expected with polymorphic models. We just need to do two things: 1. Inherit our admin classes from both :class:`~polymorphic.admin.PolymorphicParentModelAdmin` / :class:`~polymorphic.admin.PolymorphicChildModelAdmin` and :ref:`VersionAdmin `. 2. Override the ``admin/polymorphic/object_history.html`` template. .. tip:: The complete working code for this example can be found `in the reversion integration test `_. Admin Configuration ~~~~~~~~~~~~~~~~~~~ The admin configuration combines :class:`~polymorphic.admin.PolymorphicParentModelAdmin` and :class:`~polymorphic.admin.PolymorphicChildModelAdmin` with :ref:`VersionAdmin `: .. literalinclude:: ../../src/polymorphic/tests/examples/integrations/reversion/admin.py :language: python :linenos: Custom Template ~~~~~~~~~~~~~~~ Since both :class:`~polymorphic.admin.PolymorphicParentModelAdmin` and :ref:`VersionAdmin `. define ``object_history.html`` template, you need to create a custom template that combines both: .. literalinclude:: ../../src/polymorphic/tests/examples/integrations/reversion/templates/admin/polymorphic/object_history.html :language: html+django This makes sure both the reversion template is used, and the breadcrumb is corrected for the polymorphic model using the :templatetag:`breadcrumb_scope` tag. .. toctree:: djangorestframework django-polymorphic-4.10.2/docs/managers.rst000066400000000000000000000110521513173623500206670ustar00rootroot00000000000000Managers & Querysets ==================== Using a Custom Manager ---------------------- A nice feature of Django is the possibility to define one's own custom object managers. This is fully supported with :pypi:`django-polymorphic`. For creating a custom polymorphic manager class, just derive your manager from :class:`~polymorphic.managers.PolymorphicManager` instead of :class:`~django.db.models.Manager`. As with vanilla Django, in your model class, you should explicitly add the default manager first, and then your custom manager: .. code-block:: python from polymorphic.models import PolymorphicModel from polymorphic.managers import PolymorphicManager class TimeOrderedManager(PolymorphicManager): def get_queryset(self): qs = super(TimeOrderedManager,self).get_queryset() return qs.order_by('-start_date') def most_recent(self): qs = self.get_queryset() # get my ordered queryset return qs[:10] # limit => get ten most recent entries class Project(PolymorphicModel): objects = PolymorphicManager() # add the default polymorphic manager first objects_ordered = TimeOrderedManager() # then add your own manager start_date = DateTimeField() # project start is this date/time The first manager defined (:attr:`~django.db.models.Model.objects` in the example) is used by Django as automatic manager for several purposes, including accessing related objects. It must not filter objects and it's safest to use the plain :class:`~polymorphic.managers.PolymorphicManager` here. Manager Inheritance ------------------- Polymorphic models inherit/propagate all managers from their base models, as long as these are polymorphic. This means that all managers defined in polymorphic base models continue to work as expected in models inheriting from this base model: .. code-block:: python from polymorphic.models import PolymorphicModel from polymorphic.managers import PolymorphicManager class TimeOrderedManager(PolymorphicManager): def get_queryset(self): qs = super(TimeOrderedManager,self).get_queryset() return qs.order_by('-start_date') def most_recent(self): qs = self.get_queryset() # get my ordered queryset return qs[:10] # limit => get ten most recent entries class Project(PolymorphicModel): objects = PolymorphicManager() # add the default polymorphic manager first objects_ordered = TimeOrderedManager() # then add your own manager start_date = DateTimeField() # project start is this date/time class ArtProject(Project): # inherit from Project, inheriting its fields and managers artist = models.CharField(max_length=30) ArtProject inherited the managers ``objects`` and ``objects_ordered`` from Project. ``ArtProject.objects_ordered.all()`` will return all art projects ordered regarding their start time and ``ArtProject.objects_ordered.most_recent()`` will return the ten most recent art projects. Using a Custom Queryset Class ----------------------------- The :class:`~polymorphic.managers.PolymorphicManager` class accepts one initialization argument, which is the queryset class the manager should use. Just as with vanilla Django, you may define your own custom queryset classes. Just use :class:`~polymorphic.managers.PolymorphicQuerySet` instead of Django's :class:`~django.db.models.query.QuerySet` as the base class: .. code-block:: python from polymorphic.models import PolymorphicModel from polymorphic.managers import PolymorphicManager from polymorphic.query import PolymorphicQuerySet class MyQuerySet(PolymorphicQuerySet): def my_queryset_method(self): ... class MyModel(PolymorphicModel): my_objects = PolymorphicManager.from_queryset(MyQuerySet)() ... If you do not wish to extend from a custom :class:`~polymorphic.managers.PolymorphicManager` you may also prefer the :meth:`~polymorphic.managers.PolymorphicQuerySet.as_manager` shortcut: .. code-block:: python from polymorphic.models import PolymorphicModel from polymorphic.query import PolymorphicQuerySet class MyQuerySet(PolymorphicQuerySet): def my_queryset_method(self): ... class MyModel(PolymorphicModel): my_objects = MyQuerySet.as_manager() ... For further discussion see `this topic on the Q&A page `_.django-polymorphic-4.10.2/docs/migrating.rst000066400000000000000000000052511513173623500210570ustar00rootroot00000000000000Migrating Existing Models ========================= Existing models can be migrated to become polymorphic models. During migration, the :attr:`~polymorphic.models.PolymorphicModel.polymorphic_ctype` field needs to be populated. This can be done in the following steps: #. Inherit your model from :class:`~polymorphic.models.PolymorphicModel`. #. Create a Django migration file to create the ``polymorphic_ctype_id`` database column. #. Make sure the proper :class:`~django.contrib.contenttypes.models.ContentType` value is filled in. Filling the content type value ------------------------------ The following code can be used to fill the value of a model: .. code-block:: python from django.contrib.contenttypes.models import ContentType from myapp.models import MyModel new_ct = ContentType.objects.get_for_model(MyModel) MyModel.objects.filter(polymorphic_ctype__isnull=True).update(polymorphic_ctype=new_ct) The creation and update of the ``polymorphic_ctype_id`` column can be included in a single Django migration. For example: .. code-block:: python # -*- coding: utf-8 -*- from django.db import migrations, models def forwards_func(apps, schema_editor): MyModel = apps.get_model('myapp', 'MyModel') ContentType = apps.get_model('contenttypes', 'ContentType') new_ct = ContentType.objects.get_for_model(MyModel) MyModel.objects.filter(polymorphic_ctype__isnull=True).update( polymorphic_ctype=new_ct ) class Migration(migrations.Migration): dependencies = [ ('contenttypes', '0001_initial'), ('myapp', '0001_initial'), ] operations = [ migrations.AddField( model_name='mymodel', name='polymorphic_ctype', field=models.ForeignKey( related_name='polymorphic_myapp.mymodel_set+', editable=False, to='contenttypes.ContentType', null=True ), ), migrations.RunPython(forwards_func, migrations.RunPython.noop), ] It's recommended to let :django-admin:`makemigrations` create the migration file, and include the :class:`~django.db.migrations.operations.RunPython` manually before running the migration. .. versionadded:: 1.1 When the model is created elsewhere, you can also use the :func:`~polymorphic.utils.reset_polymorphic_ctype` function: .. code-block:: python from polymorphic.utils import reset_polymorphic_ctype from myapp.models import Base, Sub1, Sub2 reset_polymorphic_ctype(Base, Sub1, Sub2) reset_polymorphic_ctype(Base, Sub1, Sub2, ignore_existing=True) django-polymorphic-4.10.2/docs/performance.rst000066400000000000000000000100431513173623500213720ustar00rootroot00000000000000.. _performance: Performance Considerations ========================== Usually, when Django users create their own polymorphic ad-hoc solution without a tool like :pypi:`django-polymorphic`, this usually results in a variation of: .. code-block:: python result_objects = [ o.get_real_instance() for o in BaseModel.objects.filter(...) ] which has very bad performance, as it introduces one additional SQL query for every object in the result which is not of class ``BaseModel``. Compared to these solutions, :pypi:`django-polymorphic` has the advantage that it only needs 1 SQL query *per object type*, and not *per object*. The current implementation does not use any custom SQL or Django DB layer internals - it is purely based on the standard Django ORM. Specifically, the query: .. code-block:: python result_objects = list( ModelA.objects.filter(...) ) performs one SQL query to retrieve ``ModelA`` objects and one additional query for each unique derived class occurring in result_objects. The best case for retrieving 100 objects is 1 SQL query if all are class ``ModelA``. If 50 objects are ``ModelA`` and 50 are ``ModelB``, then two queries are executed. The pathological worst case is 101 db queries if result_objects contains 100 different object types (with all of them subclasses of ``ModelA``). Iteration: Memory vs DB Round Trips ----------------------------------- When iterating over large QuerySets, there is a trade-off between memory consumption and number of round trips to the database. One additional query is needed per model subclass present in the QuerySet and these queries take the form of ``SELECT ... WHERE pk IN (....)`` with a potentially large number of IDs in the IN clause. All models in the IN clause will be loaded into memory during iteration. To balance this trade-off, by default a maximum of 2000 objects are requested at once. This means that if your QuerySet contains 10,000 objects of 3 different subclasses, then 16 queries will be executed: 1 to fetch the base objects, and 5 (10/2 == 5) * 3 more to fetch the subclasses. The `chunk_size` parameter on :meth:`~django.db.models.query.QuerySet.iterator` can be used to change the number of objects loaded into memory at once during iteration. For example, to load 5000 objects at once: .. code-block:: python for obj in ModelA.objects.all().iterator(chunk_size=5000): process(obj) .. note:: ``chunk_size`` on non-polymorphic QuerySets controls the number of rows fetched from the database at once, but for polymorphic QuerySets the behavior is more analogous to its behavior when :meth:`~django.db.models.query.QuerySet.prefetch_related` is used. Some database backends limit the number of parameters in a query. For those backends the ``chunk_size`` will be restricted to be no greater than that limit. This limit can be checked in: .. code-block:: python from django.db import connection print(connection.features.max_query_params) You may change the global default fallback ``chunk_size`` by modifying the :attr:`polymorphic.query.Polymorphic_QuerySet_objects_per_request` attribute. Place code like this somewhere that will be executed during startup: .. code-block:: python from polymorphic import query query.Polymorphic_QuerySet_objects_per_request = 5000 :class:`~django.contrib.contenttypes.models.ContentType` retrieval ------------------------------------------------------------------ When fetching the :class:`~django.contrib.contenttypes.models.ContentType` class, it's tempting to read the :attr:`~polymorphic.models.PolymorphicModel.polymorphic_ctype` field directly. However, this performs an additional query via the :class:`~django.db.models.ForeignKey` object to fetch the :class:`~django.contrib.contenttypes.models.ContentType`. Instead, use: .. code-block:: python from django.contrib.contenttypes.models import ContentType ctype = ContentType.objects.get_for_id(object.polymorphic_ctype_id) This uses the :meth:`~django.contrib.contenttypes.models.ContentTypeManager.get_for_id` function which caches the results internally. django-polymorphic-4.10.2/docs/quickstart.rst000066400000000000000000000067611513173623500212770ustar00rootroot00000000000000Quickstart =========== Install the project using:: pip install django-polymorphic Update the settings file: .. code-block:: python INSTALLED_APPS += ( 'polymorphic', 'django.contrib.contenttypes', ) The current release of :pypi:`django-polymorphic` supports: .. image:: https://badge.fury.io/py/django-polymorphic.svg :target: https://pypi.python.org/pypi/django-polymorphic/ :alt: PyPI version .. image:: https://img.shields.io/pypi/pyversions/django-polymorphic.svg :target: https://pypi.python.org/pypi/django-polymorphic/ :alt: Supported Pythons .. image:: https://img.shields.io/pypi/djversions/django-polymorphic.svg :target: https://pypi.org/project/django-polymorphic/ :alt: Supported Django Making Your Models Polymorphic ------------------------------ Use :class:`~polymorphic.models.PolymorphicModel` instead of Django's :class:`~django.db.models.Model`, like so: .. code-block:: python from polymorphic.models import PolymorphicModel class Project(PolymorphicModel): topic = models.CharField(max_length=30) class ArtProject(Project): artist = models.CharField(max_length=30) class ResearchProject(Project): supervisor = models.CharField(max_length=30) All models inheriting from your polymorphic models will be polymorphic as well. Using Polymorphic Models ------------------------ Create some objects: .. code-block:: python >>> Project.objects.create(topic="Department Party") >>> ArtProject.objects.create(topic="Painting with Tim", artist="T. Turner") >>> ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") Get polymorphic query results: .. code-block:: python >>> Project.objects.all() [ , , ] Use :meth:`~polymorphic.managers.PolymorphicQuerySet.instance_of` and :meth:`~polymorphic.managers.PolymorphicQuerySet.not_instance_of` for narrowing the result to specific subtypes: .. code-block:: python >>> Project.objects.instance_of(ArtProject) [ ] .. code-block:: python >>> Project.objects.instance_of(ArtProject) | Project.objects.instance_of(ResearchProject) [ , ] Polymorphic filtering: Get all projects where Mr. Turner is involved as an artist or supervisor (note the three underscores): .. code-block:: python >>> Project.objects.filter(Q(ArtProject___artist='T. Turner') | Q(ResearchProject___supervisor='T. Turner')) [ , ] This is basically all you need to know, as *django-polymorphic* mostly works fully automatic and just delivers the expected results. .. note:: While :pypi:`django-polymorphic` makes subclassed models easy to use in Django, we still encourage to use them with caution. Each subclassed model will require Django to perform an ``INNER JOIN`` to fetch the model fields from the database. While taking this in mind, there are valid reasons for using subclassed models. That's what this library is designed for! django-polymorphic-4.10.2/docs/views.rst000066400000000000000000000041141513173623500202300ustar00rootroot00000000000000.. _views: Class Based Views ================= While :pypi:`django-polymorphic` provides full admin integration, you might want to build front-end views that allow users to create polymorphic objects. Since a single URL cannot easily handle different form fields for different models, the best approach is a two-step process: 1. **Step 1:** Let the user choose the desired type. 2. **Step 2:** Display the form for that specific type. .. tip:: The code for this example can be found `here `_. This example uses model labels (e.g., ``app.ModelName``) to identify the selected type. Assume we have the following models: .. literalinclude:: ../src/polymorphic/tests/examples/views/models.py :language: python :linenos: Step 1: Selecting the Type -------------------------- Create a form that allows users select the desired model type. You can use a simple choice field for this. .. literalinclude:: ../src/polymorphic/tests/examples/views/views.py :language: python :lines: 1-45 :linenos: Your template ``project_type_select.html``, might look like this: .. literalinclude:: ../src/polymorphic/tests/examples/views/templates/project_type_select.html :language: html Step 2: Displaying the Form --------------------------- The creation view needs to dynamically select the correct form class based on the chosen model label. .. literalinclude:: ../src/polymorphic/tests/examples/views/views.py :language: python :lines: 47- :linenos: In your template ``project_form.html``, make sure to preserve the ``model`` parameter: .. literalinclude:: ../src/polymorphic/tests/examples/views/templates/project_form.html :language: html And our urls might look like this: .. literalinclude:: ../src/polymorphic/tests/examples/views/urls.py :linenos: Using ``extra_views`` --------------------- If you are using :pypi:`django-extra-views`, :pypi:`django-polymorphic` provides mixins to help with formsets. See :mod:`polymorphic.contrib.extra_views` for more details. django-polymorphic-4.10.2/example/000077500000000000000000000000001513173623500170445ustar00rootroot00000000000000django-polymorphic-4.10.2/example/example/000077500000000000000000000000001513173623500204775ustar00rootroot00000000000000django-polymorphic-4.10.2/example/example/__init__.py000066400000000000000000000000001513173623500225760ustar00rootroot00000000000000django-polymorphic-4.10.2/example/example/settings.py000066400000000000000000000056511513173623500227200ustar00rootroot00000000000000import os DEBUG = True ADMINS = ( # ('Your Name', 'your_email@example.com'), ) MANAGERS = ADMINS PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__)) DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(PROJECT_ROOT, "example.db"), } } SITE_ID = 1 # Make this unique, and don't share it with anybody. SECRET_KEY = "5$f%)&a4tc*bg(79+ku!7o$kri-duw99@hq_)va^_kaw9*l)!7" # Language # TIME_ZONE = 'America/Chicago' LANGUAGE_CODE = "en-us" USE_I18N = True USE_L10N = True USE_TZ = True # Paths MEDIA_ROOT = "" MEDIA_URL = "/media/" STATIC_ROOT = "" STATIC_URL = "/static/" # Apps STATICFILES_FINDERS = ( "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) MIDDLEWARE = ( "django.middleware.common.CommonMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", ) TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": (), "OPTIONS": { "loaders": ( "django.template.loaders.filesystem.Loader", "django.template.loaders.app_directories.Loader", ), "context_processors": ( "django.template.context_processors.debug", "django.template.context_processors.i18n", "django.template.context_processors.media", "django.template.context_processors.request", "django.template.context_processors.static", "django.contrib.messages.context_processors.messages", "django.contrib.auth.context_processors.auth", ), }, } ] ROOT_URLCONF = "example.urls" WSGI_APPLICATION = "example.wsgi.application" INSTALLED_APPS = ( "django.contrib.auth", "django.contrib.admin", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "polymorphic", # needed if you want to use the polymorphic admin "pexp", # this Django app is for testing and experimentation; not needed otherwise "orders", ) TEST_RUNNER = "django.test.runner.DiscoverRunner" # silence system checks # Logging configuration LOGGING = { "version": 1, "disable_existing_loggers": False, "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, "handlers": { "mail_admins": { "level": "ERROR", "filters": ["require_debug_false"], "class": "django.utils.log.AdminEmailHandler", } }, "loggers": { "django.request": { "handlers": ["mail_admins"], "level": "ERROR", "propagate": True, } }, } django-polymorphic-4.10.2/example/example/urls.py000066400000000000000000000004361513173623500220410ustar00rootroot00000000000000from django.contrib import admin from django.urls import path, reverse_lazy from django.views.generic import RedirectView admin.autodiscover() urlpatterns = [ path("admin/", admin.site.urls), path("", RedirectView.as_view(url=reverse_lazy("admin:index"), permanent=False)), ] django-polymorphic-4.10.2/example/example/wsgi.py000066400000000000000000000021631513173623500220240ustar00rootroot00000000000000""" WSGI config for example project. This module contains the WSGI application used by Django's development server and any production WSGI deployments. It should expose a module-level variable named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover this application via the ``WSGI_APPLICATION`` setting. Usually you will have the standard Django WSGI application here, but it also might make sense to replace the whole Django WSGI application with a custom one that later delegates to the Django one. For example, you could introduce WSGI middleware here, or combine a Django application with an application of another framework. """ import os # This application object is used by any WSGI server configured to use this # file. This includes Django's development server, if the WSGI_APPLICATION # setting points here. from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") application = get_wsgi_application() # Apply WSGI middleware here. # from helloworld.wsgi import HelloWorldApplication # application = HelloWorldApplication(application) django-polymorphic-4.10.2/example/manage.py000077500000000000000000000006231513173623500206520ustar00rootroot00000000000000#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") # Import polymorphic from this folder. SRC_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) sys.path.insert(0, SRC_ROOT) from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) django-polymorphic-4.10.2/example/orders/000077500000000000000000000000001513173623500203425ustar00rootroot00000000000000django-polymorphic-4.10.2/example/orders/__init__.py000066400000000000000000000000001513173623500224410ustar00rootroot00000000000000django-polymorphic-4.10.2/example/orders/admin.py000066400000000000000000000021101513173623500217760ustar00rootroot00000000000000from django.contrib import admin from polymorphic.admin import PolymorphicInlineSupportMixin, StackedPolymorphicInline from .models import BankPayment, CreditCardPayment, Order, Payment, SepaPayment class CreditCardPaymentInline(StackedPolymorphicInline.Child): model = CreditCardPayment class BankPaymentInline(StackedPolymorphicInline.Child): model = BankPayment class SepaPaymentInline(StackedPolymorphicInline.Child): model = SepaPayment class PaymentInline(StackedPolymorphicInline): """ An inline for a polymorphic model. The actual form appearance of each row is determined by the child inline that corresponds with the actual model type. """ model = Payment child_inlines = (CreditCardPaymentInline, BankPaymentInline, SepaPaymentInline) @admin.register(Order) class OrderAdmin(PolymorphicInlineSupportMixin, admin.ModelAdmin): """ Admin for orders. The inline is polymorphic. To make sure the inlines are properly handled, the ``PolymorphicInlineSupportMixin`` is needed to """ inlines = (PaymentInline,) django-polymorphic-4.10.2/example/orders/migrations/000077500000000000000000000000001513173623500225165ustar00rootroot00000000000000django-polymorphic-4.10.2/example/orders/migrations/0001_initial.py000066400000000000000000000121201513173623500251550ustar00rootroot00000000000000from django.db import migrations, models class Migration(migrations.Migration): dependencies = [("contenttypes", "0002_remove_content_type_name")] operations = [ migrations.CreateModel( name="Order", fields=[ ( "id", models.AutoField( verbose_name="ID", serialize=False, auto_created=True, primary_key=True, ), ), ("title", models.CharField(max_length=200, verbose_name="Title")), ], options={ "ordering": ("title",), "verbose_name": "Organisation", "verbose_name_plural": "Organisations", }, ), migrations.CreateModel( name="Payment", fields=[ ( "id", models.AutoField( verbose_name="ID", serialize=False, auto_created=True, primary_key=True, ), ), ("currency", models.CharField(default=b"USD", max_length=3)), ("amount", models.DecimalField(max_digits=10, decimal_places=2)), ], options={"verbose_name": "Payment", "verbose_name_plural": "Payments"}, ), migrations.CreateModel( name="BankPayment", fields=[ ( "payment_ptr", models.OneToOneField( parent_link=True, auto_created=True, primary_key=True, serialize=False, on_delete=models.CASCADE, to="orders.Payment", ), ), ("bank_name", models.CharField(max_length=100)), ("swift", models.CharField(max_length=20)), ], options={ "verbose_name": "Bank Payment", "verbose_name_plural": "Bank Payments", }, bases=("orders.payment",), ), migrations.CreateModel( name="CreditCardPayment", fields=[ ( "payment_ptr", models.OneToOneField( parent_link=True, auto_created=True, primary_key=True, serialize=False, on_delete=models.CASCADE, to="orders.Payment", ), ), ("card_type", models.CharField(max_length=10)), ( "expiry_month", models.PositiveSmallIntegerField( choices=[ (1, "jan"), (2, "feb"), (3, "mar"), (4, "apr"), (5, "may"), (6, "jun"), (7, "jul"), (8, "aug"), (9, "sep"), (10, "oct"), (11, "nov"), (12, "dec"), ] ), ), ("expiry_year", models.PositiveIntegerField()), ], options={ "verbose_name": "Credit Card Payment", "verbose_name_plural": "Credit Card Payments", }, bases=("orders.payment",), ), migrations.CreateModel( name="SepaPayment", fields=[ ( "payment_ptr", models.OneToOneField( parent_link=True, auto_created=True, primary_key=True, serialize=False, on_delete=models.CASCADE, to="orders.Payment", ), ), ("iban", models.CharField(max_length=34)), ("bic", models.CharField(max_length=11)), ], options={ "verbose_name": "Bank Payment", "verbose_name_plural": "Bank Payments", }, bases=("orders.payment",), ), migrations.AddField( model_name="payment", name="order", field=models.ForeignKey(to="orders.Order", on_delete=models.CASCADE), ), migrations.AddField( model_name="payment", name="polymorphic_ctype", field=models.ForeignKey( related_name="polymorphic_orders.payment_set+", editable=False, on_delete=models.CASCADE, to="contenttypes.ContentType", null=True, ), ), ] django-polymorphic-4.10.2/example/orders/migrations/__init__.py000066400000000000000000000000001513173623500246150ustar00rootroot00000000000000django-polymorphic-4.10.2/example/orders/models.py000066400000000000000000000035721513173623500222060ustar00rootroot00000000000000from django.db import models from django.utils.dates import MONTHS_3 from django.utils.translation import gettext_lazy as _ from polymorphic.models import PolymorphicModel class Order(models.Model): """ An example order that has polymorphic relations """ title = models.CharField(_("Title"), max_length=200) class Meta: verbose_name = _("Organisation") verbose_name_plural = _("Organisations") ordering = ("title",) def __str__(self): return self.title class Payment(PolymorphicModel): """ A generic payment model. """ order = models.ForeignKey(Order, on_delete=models.CASCADE) currency = models.CharField(default="USD", max_length=3) amount = models.DecimalField(max_digits=10, decimal_places=2) class Meta: verbose_name = _("Payment") verbose_name_plural = _("Payments") def __str__(self): return f"{self.currency} {self.amount}" class CreditCardPayment(Payment): """ Credit card """ MONTH_CHOICES = [(i, n) for i, n in sorted(MONTHS_3.items())] card_type = models.CharField(max_length=10) expiry_month = models.PositiveSmallIntegerField(choices=MONTH_CHOICES) expiry_year = models.PositiveIntegerField() class Meta: verbose_name = _("Credit Card Payment") verbose_name_plural = _("Credit Card Payments") class BankPayment(Payment): """ Payment by bank """ bank_name = models.CharField(max_length=100) swift = models.CharField(max_length=20) class Meta: verbose_name = _("Bank Payment") verbose_name_plural = _("Bank Payments") class SepaPayment(Payment): """ Payment by SEPA (EU) """ iban = models.CharField(max_length=34) bic = models.CharField(max_length=11) class Meta: verbose_name = _("SEPA Payment") verbose_name_plural = _("SEPA Payments") django-polymorphic-4.10.2/example/pexp/000077500000000000000000000000001513173623500200205ustar00rootroot00000000000000django-polymorphic-4.10.2/example/pexp/__init__.py000066400000000000000000000000001513173623500221170ustar00rootroot00000000000000django-polymorphic-4.10.2/example/pexp/admin.py000066400000000000000000000030111513173623500214550ustar00rootroot00000000000000from django.contrib import admin from pexp.models import * from polymorphic.admin import ( PolymorphicChildModelAdmin, PolymorphicChildModelFilter, PolymorphicParentModelAdmin, ) class ProjectAdmin(PolymorphicParentModelAdmin): base_model = Project # Can be set explicitly. list_filter = (PolymorphicChildModelFilter,) child_models = (Project, ArtProject, ResearchProject) class ProjectChildAdmin(PolymorphicChildModelAdmin): base_model = Project # Can be set explicitly. # On purpose, only have the shared fields here. # The fields of the derived model should still be displayed. base_fieldsets = (("Base fields", {"fields": ("topic",)}),) admin.site.register(Project, ProjectAdmin) admin.site.register(ArtProject, ProjectChildAdmin) admin.site.register(ResearchProject, ProjectChildAdmin) class UUIDModelAAdmin(PolymorphicParentModelAdmin): list_filter = (PolymorphicChildModelFilter,) child_models = (UUIDModelA, UUIDModelB) class UUIDModelAChildAdmin(PolymorphicChildModelAdmin): pass admin.site.register(UUIDModelA, UUIDModelAAdmin) admin.site.register(UUIDModelB, UUIDModelAChildAdmin) admin.site.register(UUIDModelC, UUIDModelAChildAdmin) class ProxyAdmin(PolymorphicParentModelAdmin): list_filter = (PolymorphicChildModelFilter,) child_models = (ProxyA, ProxyB) class ProxyChildAdmin(PolymorphicChildModelAdmin): pass admin.site.register(ProxyBase, ProxyAdmin) admin.site.register(ProxyA, ProxyChildAdmin) admin.site.register(ProxyB, ProxyChildAdmin) django-polymorphic-4.10.2/example/pexp/dumpdata_test_correct_output.txt000066400000000000000000000014221513173623500265570ustar00rootroot00000000000000[ { "pk": 1, "model": "pexp.project", "fields": { "topic": "John's gathering", "polymorphic_ctype": 2 } }, { "pk": 2, "model": "pexp.project", "fields": { "topic": "Sculpting with Tim", "polymorphic_ctype": 3 } }, { "pk": 3, "model": "pexp.project", "fields": { "topic": "Swallow Aerodynamics", "polymorphic_ctype": 4 } }, { "pk": 2, "model": "pexp.artproject", "fields": { "artist": "T. Turner" } }, { "pk": 3, "model": "pexp.researchproject", "fields": { "supervisor": "Dr. Winter" } } ] django-polymorphic-4.10.2/example/pexp/management/000077500000000000000000000000001513173623500221345ustar00rootroot00000000000000django-polymorphic-4.10.2/example/pexp/management/__init__.py000066400000000000000000000000001513173623500242330ustar00rootroot00000000000000django-polymorphic-4.10.2/example/pexp/management/commands/000077500000000000000000000000001513173623500237355ustar00rootroot00000000000000django-polymorphic-4.10.2/example/pexp/management/commands/__init__.py000066400000000000000000000000001513173623500260340ustar00rootroot00000000000000django-polymorphic-4.10.2/example/pexp/management/commands/p2cmd.py000066400000000000000000000065561513173623500253300ustar00rootroot00000000000000""" This module is a scratchpad for general development, testing & debugging Well, even more so than pcmd.py. You best ignore p2cmd.py. """ import sys import time from pprint import pprint from random import Random from django.core.management import BaseCommand from django.db import connection from pexp.models import * rnd = Random() def show_queries(): print() print("QUERIES:", len(connection.queries)) pprint(connection.queries) print() connection.queries = [] def print_timing(func, message="", iterations=1): def wrapper(*arg): results = [] connection.queries_log.clear() for i in range(iterations): t1 = time.time() x = func(*arg) t2 = time.time() results.append((t2 - t1) * 1000.0) res_sum = 0 for r in results: res_sum += r print( f"{message}{func.func_name:<19}: {res_sum:.4f} ms, " f"{len(connection.queries)} queries ({iterations} times)" ) sys.stdout.flush() return wrapper class Command(BaseCommand): help = "" def handle_noargs(self, **options): if False: TestModelA.objects.all().delete() a = TestModelA.objects.create(field1="A1") b = TestModelB.objects.create(field1="B1", field2="B2") c = TestModelC.objects.create(field1="C1", field2="C2", field3="C3") connection.queries_log.clear() print(TestModelC.base_objects.all()) show_queries() if False: TestModelA.objects.all().delete() for i in range(1000): a = TestModelA.objects.create(field1=str(i % 100)) b = TestModelB.objects.create(field1=str(i % 100), field2=str(i % 200)) c = TestModelC.objects.create( field1=str(i % 100), field2=str(i % 200), field3=str(i % 300) ) if i % 100 == 0: print(i) f = print_timing(poly_sql_query, iterations=1000) f() f = print_timing(poly_sql_query2, iterations=1000) f() return NormalModelA.objects.all().delete() a = NormalModelA.objects.create(field1="A1") b = NormalModelB.objects.create(field1="B1", field2="B2") c = NormalModelC.objects.create(field1="C1", field2="C2", field3="C3") qs = TestModelA.objects.raw("SELECT * from pexp_testmodela") for o in list(qs): print(o) def poly_sql_query(): cursor = connection.cursor() cursor.execute( """ SELECT id, pexp_testmodela.field1, pexp_testmodelb.field2, pexp_testmodelc.field3 FROM pexp_testmodela LEFT OUTER JOIN pexp_testmodelb ON pexp_testmodela.id = pexp_testmodelb.testmodela_ptr_id LEFT OUTER JOIN pexp_testmodelc ON pexp_testmodelb.testmodela_ptr_id = pexp_testmodelc.testmodelb_ptr_id WHERE pexp_testmodela.field1=%i ORDER BY pexp_testmodela.id """ % rnd.randint(0, 100) ) # row=cursor.fetchone() return def poly_sql_query2(): cursor = connection.cursor() cursor.execute( f""" SELECT id, pexp_testmodela.field1 FROM pexp_testmodela WHERE pexp_testmodela.field1={rnd.randint(0, 100)} ORDER BY pexp_testmodela.id """ ) # row=cursor.fetchone() return django-polymorphic-4.10.2/example/pexp/management/commands/pcmd.py000066400000000000000000000017061513173623500252360ustar00rootroot00000000000000""" This module is a scratchpad for general development, testing & debugging. """ from django.core.management.base import NoArgsCommand from django.db import connection from pexp.models import * def reset_queries(): connection.queries = [] class Command(NoArgsCommand): help = "" def handle_noargs(self, **options): Project.objects.all().delete() a = Project.objects.create(topic="John's gathering") b = ArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner") c = ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") print(Project.objects.all()) print("") TestModelA.objects.all().delete() a = TestModelA.objects.create(field1="A1") b = TestModelB.objects.create(field1="B1", field2="B2") c = TestModelC.objects.create(field1="C1", field2="C2", field3="C3") print(TestModelA.objects.all()) print("") django-polymorphic-4.10.2/example/pexp/management/commands/polybench.py000066400000000000000000000052731513173623500263010ustar00rootroot00000000000000""" This module is a scratchpad for general development, testing & debugging """ import sys import time from pprint import pprint from django.core.management import BaseCommand from django.db import connection from pexp.models import * num_objects = 1000 def show_queries(): print() print("QUERIES:", len(connection.queries)) pprint(connection.queries) print() connection.queries_log.clear() ################################################################################### # benchmark wrappers def print_timing(func, message="", iterations=1): def wrapper(*arg): results = [] connection.queries_log.clear() for i in range(iterations): t1 = time.time() x = func(*arg) t2 = time.time() results.append((t2 - t1) * 1000.0) res_sum = 0 for r in results: res_sum += r median = res_sum / len(results) print( f"{message}{func.func_name:<19}: {median:.0f} ms, " f"{len(connection.queries) / len(results):d} queries" ) sys.stdout.flush() return wrapper def run_vanilla_any_poly(func, iterations=1): f = print_timing(func, " ", iterations) f(NormalModelC) f = print_timing(func, "poly ", iterations) f(TestModelC) ################################################################################### # benchmarks def bench_create(model): for i in range(num_objects): model.objects.create( field1=f"abc{i}", field2=f"abcd{i}", field3=f"abcde{i}", ) # print 'count:',model.objects.count() def bench_load1(model): for o in model.objects.all(): pass def bench_load1_short(model): for i in range(num_objects / 100): for o in model.objects.all()[:100]: pass def bench_load2(model): for o in model.objects.all(): f1 = o.field1 f2 = o.field2 f3 = o.field3 def bench_load2_short(model): for i in range(num_objects / 100): for o in model.objects.all()[:100]: f1 = o.field1 f2 = o.field2 f3 = o.field3 def bench_delete(model): model.objects.all().delete() ################################################################################### # Command class Command(BaseCommand): help = "" def handle_noargs(self, **options): func_list = [ (bench_delete, 1), (bench_create, 1), (bench_load1, 5), (bench_load1_short, 5), (bench_load2, 5), (bench_load2_short, 5), ] for f, iterations in func_list: run_vanilla_any_poly(f, iterations=iterations) django-polymorphic-4.10.2/example/pexp/management/commands/polymorphic_create_test_data.py000066400000000000000000000010611513173623500322250ustar00rootroot00000000000000""" This module is a scratchpad for general development, testing & debugging """ from django.core.management import BaseCommand from pexp.models import * class Command(BaseCommand): help = "" def handle_noargs(self, **options): Project.objects.all().delete() o = Project.objects.create(topic="John's gathering") o = ArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner") o = ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") print(Project.objects.all()) django-polymorphic-4.10.2/example/pexp/migrations/000077500000000000000000000000001513173623500221745ustar00rootroot00000000000000django-polymorphic-4.10.2/example/pexp/migrations/0001_initial.py000066400000000000000000000232731513173623500246460ustar00rootroot00000000000000from django.db import migrations, models import polymorphic.showfields class Migration(migrations.Migration): dependencies = [("contenttypes", "0002_remove_content_type_name")] operations = [ migrations.CreateModel( name="NormalModelA", fields=[ ( "id", models.AutoField( verbose_name="ID", serialize=False, auto_created=True, primary_key=True, ), ), ("field1", models.CharField(max_length=10)), ], ), migrations.CreateModel( name="Project", fields=[ ( "id", models.AutoField( verbose_name="ID", serialize=False, auto_created=True, primary_key=True, ), ), ("topic", models.CharField(max_length=30)), ], options={"abstract": False}, bases=(polymorphic.showfields.ShowFieldContent, models.Model), ), migrations.CreateModel( name="ProxyBase", fields=[ ( "id", models.AutoField( verbose_name="ID", serialize=False, auto_created=True, primary_key=True, ), ), ("title", models.CharField(max_length=200)), ( "polymorphic_ctype", models.ForeignKey( related_name="polymorphic_pexp.proxybase_set+", editable=False, on_delete=models.CASCADE, to="contenttypes.ContentType", null=True, ), ), ], options={"ordering": ("title",)}, ), migrations.CreateModel( name="TestModelA", fields=[ ( "id", models.AutoField( verbose_name="ID", serialize=False, auto_created=True, primary_key=True, ), ), ("field1", models.CharField(max_length=10)), ], options={"abstract": False}, bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), ), migrations.CreateModel( name="UUIDModelA", fields=[ ( "uuid_primary_key", models.UUIDField(serialize=False, primary_key=True), ), ("field1", models.CharField(max_length=10)), ], options={"abstract": False}, bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), ), migrations.CreateModel( name="ArtProject", fields=[ ( "project_ptr", models.OneToOneField( parent_link=True, auto_created=True, primary_key=True, serialize=False, on_delete=models.CASCADE, to="pexp.Project", ), ), ("artist", models.CharField(max_length=30)), ], options={"abstract": False}, bases=("pexp.project",), ), migrations.CreateModel( name="NormalModelB", fields=[ ( "normalmodela_ptr", models.OneToOneField( parent_link=True, auto_created=True, primary_key=True, serialize=False, on_delete=models.CASCADE, to="pexp.NormalModelA", ), ), ("field2", models.CharField(max_length=10)), ], bases=("pexp.normalmodela",), ), migrations.CreateModel( name="ResearchProject", fields=[ ( "project_ptr", models.OneToOneField( parent_link=True, auto_created=True, primary_key=True, serialize=False, on_delete=models.CASCADE, to="pexp.Project", ), ), ("supervisor", models.CharField(max_length=30)), ], options={"abstract": False}, bases=("pexp.project",), ), migrations.CreateModel( name="TestModelB", fields=[ ( "testmodela_ptr", models.OneToOneField( parent_link=True, auto_created=True, primary_key=True, serialize=False, on_delete=models.CASCADE, to="pexp.TestModelA", ), ), ("field2", models.CharField(max_length=10)), ], options={"abstract": False}, bases=("pexp.testmodela",), ), migrations.CreateModel( name="UUIDModelB", fields=[ ( "uuidmodela_ptr", models.OneToOneField( parent_link=True, auto_created=True, primary_key=True, serialize=False, on_delete=models.CASCADE, to="pexp.UUIDModelA", ), ), ("field2", models.CharField(max_length=10)), ], options={"abstract": False}, bases=("pexp.uuidmodela",), ), migrations.AddField( model_name="uuidmodela", name="polymorphic_ctype", field=models.ForeignKey( related_name="polymorphic_pexp.uuidmodela_set+", editable=False, on_delete=models.CASCADE, to="contenttypes.ContentType", null=True, ), ), migrations.AddField( model_name="testmodela", name="polymorphic_ctype", field=models.ForeignKey( related_name="polymorphic_pexp.testmodela_set+", editable=False, on_delete=models.CASCADE, to="contenttypes.ContentType", null=True, ), ), migrations.AddField( model_name="project", name="polymorphic_ctype", field=models.ForeignKey( related_name="polymorphic_pexp.project_set+", editable=False, on_delete=models.CASCADE, to="contenttypes.ContentType", null=True, ), ), migrations.CreateModel( name="ProxyA", fields=[], options={"proxy": True}, bases=("pexp.proxybase",) ), migrations.CreateModel( name="ProxyB", fields=[], options={"proxy": True}, bases=("pexp.proxybase",) ), migrations.CreateModel( name="NormalModelC", fields=[ ( "normalmodelb_ptr", models.OneToOneField( parent_link=True, auto_created=True, primary_key=True, serialize=False, on_delete=models.CASCADE, to="pexp.NormalModelB", ), ), ("field3", models.CharField(max_length=10)), ], bases=("pexp.normalmodelb",), ), migrations.CreateModel( name="TestModelC", fields=[ ( "testmodelb_ptr", models.OneToOneField( parent_link=True, auto_created=True, primary_key=True, serialize=False, on_delete=models.CASCADE, to="pexp.TestModelB", ), ), ("field3", models.CharField(max_length=10)), ( "field4", models.ManyToManyField(related_name="related_c", to="pexp.TestModelB"), ), ], options={"abstract": False}, bases=("pexp.testmodelb",), ), migrations.CreateModel( name="UUIDModelC", fields=[ ( "uuidmodelb_ptr", models.OneToOneField( parent_link=True, auto_created=True, primary_key=True, serialize=False, on_delete=models.CASCADE, to="pexp.UUIDModelB", ), ), ("field3", models.CharField(max_length=10)), ], options={"abstract": False}, bases=("pexp.uuidmodelb",), ), ] django-polymorphic-4.10.2/example/pexp/migrations/__init__.py000066400000000000000000000000001513173623500242730ustar00rootroot00000000000000django-polymorphic-4.10.2/example/pexp/models.py000066400000000000000000000040001513173623500216470ustar00rootroot00000000000000from django.db import models from polymorphic.models import PolymorphicModel from polymorphic.showfields import ShowFieldContent, ShowFieldTypeAndContent class Project(ShowFieldContent, PolymorphicModel): """Polymorphic model""" topic = models.CharField(max_length=30) class ArtProject(Project): artist = models.CharField(max_length=30) class ResearchProject(Project): supervisor = models.CharField(max_length=30) class UUIDModelA(ShowFieldTypeAndContent, PolymorphicModel): """UUID as primary key example""" uuid_primary_key = models.UUIDField(primary_key=True) field1 = models.CharField(max_length=10) class UUIDModelB(UUIDModelA): field2 = models.CharField(max_length=10) class UUIDModelC(UUIDModelB): field3 = models.CharField(max_length=10) class ProxyBase(PolymorphicModel): """Proxy model example - a single table with multiple types.""" title = models.CharField(max_length=200) def __unicode__(self): return f"" class Meta: ordering = ("title",) class ProxyA(ProxyBase): class Meta: proxy = True def __unicode__(self): return f"" class ProxyB(ProxyBase): class Meta: proxy = True def __unicode__(self): return f"" # Internals for management command tests class TestModelA(ShowFieldTypeAndContent, PolymorphicModel): field1 = models.CharField(max_length=10) class TestModelB(TestModelA): field2 = models.CharField(max_length=10) class TestModelC(TestModelB): field3 = models.CharField(max_length=10) field4 = models.ManyToManyField(TestModelB, related_name="related_c") class NormalModelA(models.Model): """Normal Django inheritance, no polymorphic behavior""" field1 = models.CharField(max_length=10) class NormalModelB(NormalModelA): field2 = models.CharField(max_length=10) class NormalModelC(NormalModelB): field3 = models.CharField(max_length=10) django-polymorphic-4.10.2/justfile000066400000000000000000000203451513173623500171650ustar00rootroot00000000000000set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] set unstable := true set script-interpreter := ['uv', 'run', '--script'] export PYTHONPATH := source_directory() [private] default: @just --list --list-submodules # run the django admin [script] manage *COMMAND: import os import sys from django.core import management sys.path.append(os.getcwd()) os.environ["DJANGO_SETTINGS_MODULE"] = "polymorphic.tests.debug" os.environ["SQLITE_DATABASES"] = "test1.db,test2.db" management.execute_from_command_line(sys.argv + "{{ COMMAND }}".split(" ")) # install the uv package manager [linux] [macos] install_uv: curl -LsSf https://astral.sh/uv/install.sh | sh # install the uv package manager [windows] install_uv: powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # setup the venv, pre-commit hooks and playwright dependencies setup python="python": uv venv -p {{ python }} @just run pre-commit install @just run playwright install # install git pre-commit hooks install-precommit: @just run pre-commit install # update and install development dependencies install *OPTS: uv sync {{ OPTS }} @just run pre-commit install # install playwright dependencies install-playwright: @just run playwright install # install documentation dependencies install-docs: uv sync --group docs --all-extras # run static type checking check-types: #TODO @just run mypy src/polymorphic # run package checks check-package: @just run pip check # remove doc build artifacts- [script] clean-docs: import shutil shutil.rmtree('./docs/_build', ignore_errors=True) # remove the virtual environment [script] clean-env: import shutil import sys shutil.rmtree(".venv", ignore_errors=True) # remove all git ignored files clean-git-ignored: git clean -fdX # remove all non repository artifacts clean: clean-docs clean-env clean-git-ignored # build html documentation build-docs-html: install-docs @just run sphinx-build --fresh-env --builder html --doctree-dir ./docs/_build/doctrees ./docs/ ./docs/_build/html # build pdf documentation build-docs-pdf: install-docs @just run sphinx-build --fresh-env --builder latex --doctree-dir ./docs/_build/doctrees ./docs/ ./docs/_build/pdf cd docs/_build/pdf && make # build the docs build-docs: build-docs-html # build docs and package build: build-docs-html @just manage compilemessages --ignore ".venv/*" uv build # regenerate test migrations using the lowest version of Django remake-test-migrations: - rm src/polymorphic/tests/migrations/00*.py - rm src/polymorphic/tests/deletion/migrations/00*.py - rm src/polymorphic/tests/other/migrations/00*.py - rm src/polymorphic/tests/examples/**/migrations/00*.py - rm src/polymorphic/tests/examples/integrations/**/migrations/00*.py uv run --exact --isolated --resolution lowest-direct --group reversion --group extra-views --group drf --script ./manage.py makemigrations # open the html documentation [script] open-docs: import os import webbrowser webbrowser.open(f'file://{os.getcwd()}/docs/_build/html/index.html') # build and open the documentation docs: build-docs-html open-docs # serve the documentation, with auto-reload docs-live: install-docs @just run sphinx-autobuild docs docs/_build --open-browser --watch src --port 8000 --delay 1 _link_check: -uv run sphinx-build -b linkcheck -Q -D linkcheck_timeout=10 ./docs/ ./docs/_build # check the documentation links for broken links [script] check-docs-links: _link_check import os import sys import json from pathlib import Path # The json output isn't valid, so we have to fix it before we can process. data = json.loads(f"[{','.join((Path(os.getcwd()) / 'docs/_build/output.json').read_text().splitlines())}]") broken_links = [link for link in data if link["status"] not in {"working", "redirected", "unchecked", "ignored"}] if broken_links: for link in broken_links: print(f"[{link['status']}] {link['filename']}:{link['lineno']} -> {link['uri']}", file=sys.stderr) sys.exit(1) # lint the documentation check-docs: @just run doc8 --ignore-path ./docs/_build --max-line-length 100 -q ./docs # lint the code check-lint: @just run ruff check --select I @just run ruff check # check if the code needs formatting check-format: @just run ruff format --check # check that the readme renders check-readme: @just run -m readme_renderer ./README.md -o /tmp/README.html _check-readme-quiet: @just --quiet check-readme # sort the python imports sort-imports: @just run ruff check --fix --select I # format the code and sort imports format: sort-imports just --fmt --unstable @just run ruff format # sort the imports and fix linting issues lint: sort-imports @just run ruff check --fix # fix formatting, linting issues and import sorting fix: lint format # run all static checks check: check-lint check-format check-types check-package check-docs check-docs-links _check-readme-quiet [script] _lock-python: import tomlkit import sys f='pyproject.toml' d=tomlkit.parse(open(f).read()) d['project']['requires-python']='=={}'.format(sys.version.split()[0]) open(f,'w').write(tomlkit.dumps(d)) # lock to specific python and versions of given dependencies test-lock +PACKAGES: _lock-python uv add {{ PACKAGES }} # run tests test *TESTS: install-playwright @just run --exact pytest {{ TESTS }} --cov test-db DB_CLIENT="dev" *TESTS: install-playwright # No Optional Dependency Unit Tests # todo clean this up, rerunning a lot of tests uv sync --exact --group {{ DB_CLIENT }} @just run pytest {{ TESTS }} --cov # run django-reversion integration tests test-reversion *TESTS: install-playwright uv sync --exact --group reversion @just run pytest -m integration src/polymorphic/tests/examples/integrations/reversion {{ TESTS }} test-extra-views *TESTS: uv sync --group extra-views @just run pytest -m integration src/polymorphic/tests/examples/integrations/extra_views {{ TESTS }} test-drf *TESTS: uv sync --group drf @just run pytest -m integration src/polymorphic/tests/examples/integrations/drf {{ TESTS }} test-guardian *TESTS: uv sync --group guardian @just run pytest -m integration src/polymorphic/tests/examples/integrations/guardian {{ TESTS }} # run all third party integration tests test-integrations DB_CLIENT="dev": install-playwright # Integration Tests uv sync --group {{ DB_CLIENT }} --group reversion --group extra-views --group drf --group guardian @just run pytest -m integration --cov --cov-append # debug an test debug-test *TESTS: @just run pytest \ -o addopts='-ra -q' \ -s --trace --pdbcls=IPython.terminal.debugger:Pdb \ --headed {{ TESTS }} # run the pre-commit checks precommit: @just run pre-commit # generate the test coverage report coverage: @just run coverage combine --keep *.coverage @just run coverage report @just run coverage xml [script] fetch-refs LIB: install-docs import os from pathlib import Path import logging as _logging import sys import runpy from sphinx.ext.intersphinx import inspect_main _logging.basicConfig() libs = runpy.run_path(Path(os.getcwd()) / "docs/conf.py").get("intersphinx_mapping") url = libs.get("{{ LIB }}", None) if not url: sys.exit(f"Unrecognized {{ LIB }}, must be one of: {', '.join(libs.keys())}") if url[1] is None: url = f"{url[0].rstrip('/')}/objects.inv" else: url = url[1] raise SystemExit(inspect_main([url])) # run the command in the virtual environment run +ARGS: uv run {{ ARGS }} # validate the given version string against the lib version [script] validate_version VERSION: import re import tomllib import polymorphic version = re.match(r"v?(\d+[.]\d+[.]\w+)", "{{ VERSION }}").groups()[0] assert version == tomllib.load(open('pyproject.toml', 'rb'))['project']['version'] assert version == polymorphic.__version__ print(version) # issue a release for the given semver string (e.g. 2.1.0) release VERSION: @just validate_version v{{ VERSION }} git tag -s v{{ VERSION }} -m "{{ VERSION }} Release" git push upstream v{{ VERSION }} django-polymorphic-4.10.2/manage.py000066400000000000000000000004341513173623500172140ustar00rootroot00000000000000# This helps pytest-django locate the project. import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polymorphic.tests.settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) django-polymorphic-4.10.2/pyproject.toml000066400000000000000000000113211513173623500203230ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "django-polymorphic" version = "4.10.2" description = "Seamless polymorphic inheritance for Django models." readme = "README.md" license = "BSD-3-Clause" license-files = [ "LICENSE", "src/polymorphic/contrib/drf/LICENSE" ] requires-python = ">=3.10,<4.0" repository = "https://github.com/jazzband/django-polymorphic" homepage = "https://django-polymorphic.rtfd.io" authors = [ {name = "Bert Constantin", email = "bert.constantin@gmx.de"}, {name = "Diederik van der Boor", email = "vdboor@edoburu.nl"}, {name = "Christopher Glass", email = "tribaal@ubuntu.com"}, ] maintainers = [ {name = "Brian Kohan", email = "bckohan@gmail.com"} ] keywords = [ "django", "polymorphic", "polymorphism", "django-admin", "django-orm", "django-formsets", "model" ] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.1", "Framework :: Django :: 5.2", "Framework :: Django :: 6.0", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Database", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Site Management", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules" ] dependencies = [ "Django >= 4.2", ] [project.urls] "Download" = "https://github.com/jazzband/django-polymorphic/tarball/master" "Documentation" = "https://django-polymorphic.readthedocs.io" "Homepage" = "https://github.com/jazzband/django-polymorphic" "Repository" = "https://github.com/jazzband/django-polymorphic" "Issues" = "https://github.com/jazzband/django-polymorphic/issues" "Changelog" = "https://django-polymorphic.readthedocs.io/en/stable/changelog/index.html" "Code_of_Conduct" = "https://jazzband.co/about/conduct" [tool.uv] package = true [tool.hatch.version] path = "src/polymorphic/__init__.py" [tool.hatch.build.targets.sdist] include = ["src/polymorphic"] exclude = ["src/polymorphic/tests"] [tool.hatch.build.targets.wheel] packages = ["src/polymorphic"] artifacts = ["*.mo"] [tool.doc8] max-line-length = 100 sphinx = true [tool.ruff] line-length = 99 exclude = [ "**/migrations/*.py", "**/migrations/**", ] [tool.ruff.lint] extend-ignore = [ "E501", ] select = [ "E", "F", "I", "W", ] [tool.ruff.lint.per-file-ignores] "example/**" = [ "F401", "F403", "F405", "F841", "I", ] "src/polymorphic/tests/**" = [ "F401", "F403", "F405", "F841", "I", ] [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "polymorphic.tests.settings" pythonpath = ["src"] django_find_project = false testpaths = ["src/polymorphic/tests"] python_files = "test*.py" python_classes = "Test*" python_functions = "test_*" norecursedirs = "*.egg .eggs dist build docs .tox .git __pycache__" markers = [ "integration: tests under examples/integrations (opt-in)", ] addopts = [ "--strict-markers", "-m", "not integration", ] [tool.coverage.run] source = [ "src" ] omit = ["*/migrations/*", "*/tests/*", "src/polymorphic/tests/*"] branch = true relative_files = true [tool.coverage.report] show_missing = true [dependency-groups] dev = [ "coverage>=7.6.1", "dj-database-url>=2.2.0", "django-test-migrations>=1.5.0", "ipdb>=0.13.13", "ipython>=8.18.1", "mypy>=1.14.1", "pre-commit>=3.5.0", "pytest>=8.3.4", "pytest-cov>=5.0.0", "pytest-django>=4.10.0", "pytest-mock>=3.15.1", "pytest-playwright>=0.7.2", "ruff>=0.9.8", "tomlkit>=0.13.3", "tox>=4.24.1", "tox-uv>=1.13.1", ] docs = [ "django-extra-views>=0.16.0", "djangorestframework>=3.16.1", "doc8>=1.1.2", "furo>=2025.7.19", "readme-renderer[md]>=43.0", "sphinx>=7.1.2", "sphinx-autobuild>=2024.10.3", "sphinxcontrib-django>=2.5", ] psycopg2 = [ "psycopg2>=2.9.10", ] psycopg3 = [ "psycopg", ] mysql = [ "mysqlclient>=1.4.0", ] cx_oracle = [ "cx-oracle>=8.3.0", ] oracledb = [ "oracledb>=2.3.0", ] reversion = [ "django-reversion>=6.1.0", ] extra-views = [ "django-extra-views>=0.16.0", ] drf = [ "django-filter>=24.0", "djangorestframework>=3.16.1", ] guardian = [ "django-guardian>=2.4.0", ] django-polymorphic-4.10.2/src/000077500000000000000000000000001513173623500162005ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/000077500000000000000000000000001513173623500205455ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/__init__.py000066400000000000000000000051401513173623500226560ustar00rootroot00000000000000r""" :: ██████╗ ██╗ █████╗ ███╗ ██╗ ██████╗ ██████╗ ██╔══██╗ ██║██╔══██╗████╗ ██║██╔════╝ ██╔═══██╗ ██║ ██║ ██║███████║██╔██╗ ██║██║ ███╗██║ ██║ ██║ ██║██ ██║██╔══██║██║╚██╗██║██║ ██║██║ ██║ ██████╔╝╚█████╔╝██║ ██║██║ ╚████║╚██████╔╝╚██████╔╝ ╚═════╝ ╚════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ██████╗ ██████╗ ██╗ ██╗ ██╗███╗ ███╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗██╗ ██████╗ ██╔══██╗██╔═══██╗██║ ╚██╗ ██╔╝████╗ ████║██╔═══██╗██╔══██╗██╔══██╗██║ ██║██║██╔════╝ ██████╔╝██║ ██║██║ ╚████╔╝ ██╔████╔██║██║ ██║██████╔╝██████╔╝███████║██║██║ ██╔═══╝ ██║ ██║██║ ╚██╔╝ ██║╚██╔╝██║██║ ██║██╔══██╗██╔═══╝ ██╔══██║██║██║ ██║ ╚██████╔╝███████╗██║ ██║ ╚═╝ ██║╚██████╔╝██║ ██║██║ ██║ ██║██║╚██████╗ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ Seamless Polymorphic Inheritance for Django Models """ VERSION = "4.10.2" __title__ = "Django Polymorphic" __version__ = VERSION # version synonym for backwards compatibility __author__ = "Brian Kohan" __license__ = "BSD-3-Clause" __copyright__ = ( "Copyright 2010-2025, Bert Constantin, Chris Glass, Diederik van der Boor, Brian Kohan" ) django-polymorphic-4.10.2/src/polymorphic/admin/000077500000000000000000000000001513173623500216355ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/admin/__init__.py000066400000000000000000000026631513173623500237550ustar00rootroot00000000000000""" ModelAdmin code to display polymorphic models. The admin consists of a parent admin (which shows in the admin with a list), and a child admin (which is used internally to show the edit/delete dialog). """ # Admins for the regular models from .parentadmin import PolymorphicParentModelAdmin # noqa from .childadmin import PolymorphicChildModelAdmin from .filters import PolymorphicChildModelFilter # Utils from .forms import PolymorphicModelChoiceForm # Expose generic admin features too. There is no need to split those # as the admin already relies on contenttypes. from .generic import GenericPolymorphicInlineModelAdmin # base class from .generic import GenericStackedPolymorphicInline # stacked inline # Helpers for the inlines from .helpers import PolymorphicInlineSupportMixin # mixin for the regular model admin! from .helpers import PolymorphicInlineAdminForm, PolymorphicInlineAdminFormSet # Inlines from .inlines import PolymorphicInlineModelAdmin # base class from .inlines import StackedPolymorphicInline # stacked inline __all__ = ( "PolymorphicParentModelAdmin", "PolymorphicChildModelAdmin", "PolymorphicModelChoiceForm", "PolymorphicChildModelFilter", "PolymorphicInlineAdminForm", "PolymorphicInlineAdminFormSet", "PolymorphicInlineSupportMixin", "PolymorphicInlineModelAdmin", "StackedPolymorphicInline", "GenericPolymorphicInlineModelAdmin", "GenericStackedPolymorphicInline", ) django-polymorphic-4.10.2/src/polymorphic/admin/childadmin.py000066400000000000000000000232761513173623500243150ustar00rootroot00000000000000""" The child admin displays the change/delete view of the subclass model. """ import inspect from django.contrib import admin from django.urls import resolve from django.utils.translation import gettext_lazy as _ from polymorphic.utils import get_base_polymorphic_model from ..admin import PolymorphicParentModelAdmin class ParentAdminNotRegistered(RuntimeError): "The admin site for the model is not registered." class PolymorphicChildModelAdmin(admin.ModelAdmin): """ The *optional* base class for the admin interface of derived models. This base class defines some convenience behavior for the admin interface: * It corrects the breadcrumbs in the admin pages. * It adds the base model to the template lookup paths. * It allows to set ``base_form`` so the derived class will automatically include other fields in the form. * It allows to set ``base_fieldsets`` so the derived class will automatically display any extra fields. """ #: The base model that the class uses (auto-detected if not set explicitly) base_model = None #: By setting ``base_form`` instead of ``form``, any subclass fields are automatically added to the form. #: This is useful when your model admin class is inherited by others. base_form = None #: By setting ``base_fieldsets`` instead of ``fieldsets``, #: any subclass fields can be automatically added. #: This is useful when your model admin class is inherited by others. base_fieldsets = None #: Default title for extra fieldset extra_fieldset_title = _("Contents") #: Whether the child admin model should be visible in the admin index page. show_in_index = False def __init__(self, model, admin_site, *args, **kwargs): super().__init__(model, admin_site, *args, **kwargs) if self.base_model is None: self.base_model = get_base_polymorphic_model(model) def get_form(self, request, obj=None, **kwargs): # The django admin validation requires the form to have a 'class Meta: model = ..' # attribute, or it will complain that the fields are missing. # However, this enforces all derived ModelAdmin classes to redefine the model as well, # because they need to explicitly set the model again - it will stick with the base model. # # Instead, pass the form unchecked here, because the standard ModelForm will just work. # If the derived class sets the model explicitly, respect that setting. kwargs.setdefault("form", self.base_form or self.form) # prevent infinite recursion when this is called from get_subclass_fields if not self.fieldsets and not self.fields: kwargs.setdefault("fields", "__all__") return super().get_form(request, obj, **kwargs) def get_model_perms(self, request): match = resolve(request.path_info) if not self.show_in_index and match.namespace == self.admin_site.name: return {"add": False, "change": False, "delete": False} return super().get_model_perms(request) @property def change_form_template(self): opts = self.model._meta app_label = opts.app_label # Pass the base options base_opts = self.base_model._meta base_app_label = base_opts.app_label return [ f"admin/{app_label}/{opts.object_name.lower()}/change_form.html", f"admin/{app_label}/change_form.html", # Added: f"admin/{base_app_label}/{base_opts.object_name.lower()}/change_form.html", f"admin/{base_app_label}/change_form.html", "admin/polymorphic/change_form.html", "admin/change_form.html", ] @property def delete_confirmation_template(self): opts = self.model._meta app_label = opts.app_label # Pass the base options base_opts = self.base_model._meta base_app_label = base_opts.app_label return [ f"admin/{app_label}/{opts.object_name.lower()}/delete_confirmation.html", f"admin/{app_label}/delete_confirmation.html", # Added: f"admin/{base_app_label}/{base_opts.object_name.lower()}/delete_confirmation.html", f"admin/{base_app_label}/delete_confirmation.html", "admin/polymorphic/delete_confirmation.html", "admin/delete_confirmation.html", ] @property def object_history_template(self): opts = self.model._meta app_label = opts.app_label # Pass the base options base_opts = self.base_model._meta base_app_label = base_opts.app_label return [ f"admin/{app_label}/{opts.object_name.lower()}/object_history.html", f"admin/{app_label}/object_history.html", # Added: f"admin/{base_app_label}/{base_opts.object_name.lower()}/object_history.html", f"admin/{base_app_label}/object_history.html", "admin/polymorphic/object_history.html", "admin/object_history.html", ] def _get_parent_admin(self): # this returns parent admin instance on which to call response_post_save methods parent_model = self.model._meta.get_field("polymorphic_ctype").model if parent_model == self.model: # when parent_model is in among child_models, just return super instance return super() try: return self.admin_site._registry[parent_model] except KeyError: # Admin is not registered for polymorphic_ctype model, but perhaps it's registered # for a intermediate proxy model, between the parent_model and this model. for klass in inspect.getmro(self.model): if not issubclass(klass, parent_model): continue # e.g. found a mixin. # Fetch admin instance for model class, see if it's a possible candidate. model_admin = self.admin_site._registry.get(klass) if model_admin is not None and isinstance( model_admin, PolymorphicParentModelAdmin ): return model_admin # Success! # If we get this far without returning there is no admin available raise ParentAdminNotRegistered( f"No parent admin was registered for a '{parent_model}' model." ) def response_post_save_add(self, request, obj): return self._get_parent_admin().response_post_save_add(request, obj) def response_post_save_change(self, request, obj): return self._get_parent_admin().response_post_save_change(request, obj) def render_change_form(self, request, context, add=False, change=False, form_url="", obj=None): context.update({"base_opts": self.base_model._meta}) return super().render_change_form( request, context, add=add, change=change, form_url=form_url, obj=obj ) def delete_view(self, request, object_id, context=None): extra_context = {"base_opts": self.base_model._meta} return super().delete_view(request, object_id, extra_context) def history_view(self, request, object_id, extra_context=None): # Make sure the history view can also display polymorphic breadcrumbs context = {"base_opts": self.base_model._meta} if extra_context: context.update(extra_context) return super().history_view(request, object_id, extra_context=context) # ---- Extra: improving the form/fieldset default display ---- def get_base_fieldsets(self, request, obj=None): return self.base_fieldsets def get_fieldsets(self, request, obj=None): base_fieldsets = self.get_base_fieldsets(request, obj) # If subclass declares fieldsets or fields, this is respected if self.fieldsets or self.fields or not self.base_fieldsets: return super().get_fieldsets(request, obj) # Have a reasonable default fieldsets, # where the subclass fields are automatically included. other_fields = self.get_subclass_fields(request, obj) if other_fields: return ( base_fieldsets[0], (self.extra_fieldset_title, {"fields": other_fields}), ) + base_fieldsets[1:] else: return base_fieldsets def get_subclass_fields(self, request, obj=None): # Find out how many fields would really be on the form, # if it weren't restricted by declared fields. exclude = list(self.exclude or []) exclude.extend(self.get_readonly_fields(request, obj)) # By not declaring the fields/form in the base class, # get_form() will populate the form with all available fields. form = self.get_form(request, obj, exclude=exclude) subclass_fields = list(form.base_fields.keys()) + list( self.get_readonly_fields(request, obj) ) # Find which fields are not part of the common fields. for fieldset in self.get_base_fieldsets(request, obj): for field in fieldset[1]["fields"]: # multiple elements in single line if isinstance(field, tuple): for line_field in field: try: subclass_fields.remove(line_field) except ValueError: pass # field not found in form, Django will raise exception later. else: # regular one-element-per-line try: subclass_fields.remove(field) except ValueError: pass # field not found in form, Django will raise exception later. return subclass_fields django-polymorphic-4.10.2/src/polymorphic/admin/filters.py000066400000000000000000000022541513173623500236620ustar00rootroot00000000000000from django.contrib import admin from django.core.exceptions import PermissionDenied from django.utils.translation import gettext_lazy as _ class PolymorphicChildModelFilter(admin.SimpleListFilter): """ An admin list filter for the PolymorphicParentModelAdmin which enables filtering by its child models. This can be used in the parent admin: .. code-block:: python list_filter = (PolymorphicChildModelFilter,) """ title = _("Type") parameter_name = "polymorphic_ctype" def lookups(self, request, model_admin): return model_admin.get_child_type_choices(request, "change") def queryset(self, request, queryset): try: value = int(self.value()) except TypeError: value = None if value: # ensure the content type is allowed for choice_value, _ in self.lookup_choices: # noqa: F402 if choice_value == value: return queryset.filter(polymorphic_ctype_id=choice_value) raise PermissionDenied( f'Invalid ContentType "{value}". It must be registered as child model.' ) return queryset django-polymorphic-4.10.2/src/polymorphic/admin/forms.py000066400000000000000000000012541513173623500233370ustar00rootroot00000000000000from django import forms from django.contrib.admin.widgets import AdminRadioSelect from django.utils.translation import gettext_lazy as _ class PolymorphicModelChoiceForm(forms.Form): """ The default form for the ``add_type_form``. Can be overwritten and replaced. """ #: Define the label for the radiofield type_label = _("Type") ct_id = forms.ChoiceField( label=type_label, widget=AdminRadioSelect(attrs={"class": "radiolist"}) ) def __init__(self, *args, **kwargs): # Allow to easily redefine the label (a commonly expected usecase) super().__init__(*args, **kwargs) self.fields["ct_id"].label = self.type_label django-polymorphic-4.10.2/src/polymorphic/admin/generic.py000066400000000000000000000051301513173623500236220ustar00rootroot00000000000000from django.contrib.contenttypes.admin import GenericInlineModelAdmin from django.contrib.contenttypes.models import ContentType from django.utils.functional import cached_property from polymorphic.formsets import ( BaseGenericPolymorphicInlineFormSet, GenericPolymorphicFormSetChild, polymorphic_child_forms_factory, ) from .inlines import PolymorphicInlineModelAdmin class GenericPolymorphicInlineModelAdmin(PolymorphicInlineModelAdmin, GenericInlineModelAdmin): """ Base class for variation of inlines based on generic foreign keys. """ #: The formset class formset = BaseGenericPolymorphicInlineFormSet def get_formset(self, request, obj=None, **kwargs): """ Construct the generic inline formset class. """ # Construct the FormSet class. This is almost the same as parent version, # except that a different super is called so generic_inlineformset_factory() is used. # NOTE that generic_inlineformset_factory() also makes sure the GFK fields are excluded in the form. FormSet = GenericInlineModelAdmin.get_formset(self, request, obj=obj, **kwargs) FormSet.child_forms = polymorphic_child_forms_factory( formset_children=self.get_formset_children(request, obj=obj) ) return FormSet class Child(PolymorphicInlineModelAdmin.Child): """ Variation for generic inlines. """ # Make sure that the GFK fields are excluded from the child forms formset_child = GenericPolymorphicFormSetChild ct_field = "content_type" ct_fk_field = "object_id" @cached_property def content_type(self): """ Expose the ContentType that the child relates to. This can be used for the ``polymorphic_ctype`` field. """ return ContentType.objects.get_for_model(self.model, for_concrete_model=False) def get_formset_child(self, request, obj=None, **kwargs): # Similar to GenericInlineModelAdmin.get_formset(), # make sure the GFK is automatically excluded from the form defaults = {"ct_field": self.ct_field, "fk_field": self.ct_fk_field} defaults.update(kwargs) return super(GenericPolymorphicInlineModelAdmin.Child, self).get_formset_child( request, obj=obj, **defaults ) class GenericStackedPolymorphicInline(GenericPolymorphicInlineModelAdmin): """ The stacked layout for generic inlines. """ #: The default template to use. template = "admin/polymorphic/edit_inline/stacked.html" django-polymorphic-4.10.2/src/polymorphic/admin/helpers.py000066400000000000000000000130141513173623500236500ustar00rootroot00000000000000""" Rendering utils for admin forms; This makes sure that admin fieldsets/layout settings are exported to the template. """ import json from django.contrib.admin.helpers import AdminField, InlineAdminForm, InlineAdminFormSet from django.utils.encoding import force_str from django.utils.text import capfirst from django.utils.translation import gettext from polymorphic.formsets import BasePolymorphicModelFormSet class PolymorphicInlineAdminForm(InlineAdminForm): """ Expose the admin configuration for a form """ def polymorphic_ctype_field(self): return AdminField(self.form, "polymorphic_ctype", False) @property def is_empty(self): return "__prefix__" in self.form.prefix class PolymorphicInlineAdminFormSet(InlineAdminFormSet): """ Internally used class to expose the formset in the template. """ def __init__(self, *args, **kwargs): # Assigned later via PolymorphicInlineSupportMixin later. self.request = kwargs.pop("request", None) self.obj = kwargs.pop("obj", None) super().__init__(*args, **kwargs) def __iter__(self): """ Output all forms using the proper subtype settings. """ for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()): # Output the form model = original.get_real_instance_class() child_inline = self.opts.get_child_inline_instance(model) view_on_site_url = self.opts.get_view_on_site_url(original) yield PolymorphicInlineAdminForm( formset=self.formset, form=form, fieldsets=self.get_child_fieldsets(child_inline), prepopulated_fields=self.get_child_prepopulated_fields(child_inline), original=original, readonly_fields=self.get_child_readonly_fields(child_inline), model_admin=child_inline, view_on_site_url=view_on_site_url, ) # Extra rows, and empty prefixed forms. for form in self.formset.extra_forms + self.formset.empty_forms: model = form._meta.model child_inline = self.opts.get_child_inline_instance(model) yield PolymorphicInlineAdminForm( formset=self.formset, form=form, fieldsets=self.get_child_fieldsets(child_inline), prepopulated_fields=self.get_child_prepopulated_fields(child_inline), original=None, readonly_fields=self.get_child_readonly_fields(child_inline), model_admin=child_inline, ) def get_child_fieldsets(self, child_inline): return list(child_inline.get_fieldsets(self.request, self.obj) or ()) def get_child_readonly_fields(self, child_inline): return list(child_inline.get_readonly_fields(self.request, self.obj)) def get_child_prepopulated_fields(self, child_inline): fields = self.prepopulated_fields.copy() fields.update(child_inline.get_prepopulated_fields(self.request, self.obj)) return fields def inline_formset_data(self): """ A JavaScript data structure for the JavaScript code This overrides the default Django version to add the ``childTypes`` data. """ verbose_name = self.opts.verbose_name return json.dumps( { "name": f"#{self.formset.prefix}", "options": { "prefix": self.formset.prefix, "addText": gettext("Add another %(verbose_name)s") % {"verbose_name": capfirst(verbose_name)}, "childTypes": [ { "type": model._meta.model_name, "name": force_str(model._meta.verbose_name), } for model in self.formset.child_forms.keys() ], "deleteText": gettext("Remove"), }, } ) class PolymorphicInlineSupportMixin: """ A Mixin to add to the regular admin, so it can work with our polymorphic inlines. This mixin needs to be included in the admin that hosts the ``inlines``. It makes sure the generated admin forms have different fieldsets/fields depending on the polymorphic type of the form instance. This is achieved by overwriting :func:`get_inline_formsets` to return an :class:`PolymorphicInlineAdminFormSet` instead of a standard Django :class:`~django.contrib.admin.helpers.InlineAdminFormSet` for the polymorphic formsets. """ def get_inline_formsets(self, request, formsets, inline_instances, obj=None, *args, **kwargs): """ Overwritten version to produce the proper admin wrapping for the polymorphic inline formset. This fixes the media and form appearance of the inline polymorphic models. """ inline_admin_formsets = super().get_inline_formsets( request, formsets, inline_instances, obj=obj ) for admin_formset in inline_admin_formsets: if isinstance(admin_formset.formset, BasePolymorphicModelFormSet): # This is a polymorphic formset, which belongs to our inline. # Downcast the admin wrapper that generates the form fields. admin_formset.__class__ = PolymorphicInlineAdminFormSet admin_formset.request = request admin_formset.obj = obj return inline_admin_formsets django-polymorphic-4.10.2/src/polymorphic/admin/inlines.py000066400000000000000000000247111513173623500236550ustar00rootroot00000000000000""" Django Admin support for polymorphic inlines. Each row in the inline can correspond with a different subclass. """ from functools import partial from django.conf import settings from django.contrib.admin.options import InlineModelAdmin from django.contrib.admin.utils import flatten_fieldsets from django.core.exceptions import ImproperlyConfigured from django.forms import Media from polymorphic.formsets import ( BasePolymorphicInlineFormSet, PolymorphicFormSetChild, UnsupportedChildType, polymorphic_child_forms_factory, ) from polymorphic.formsets.utils import add_media from .helpers import PolymorphicInlineSupportMixin class PolymorphicInlineModelAdmin(InlineModelAdmin): """ A polymorphic inline, where each formset row can be a different form. Note that: * Permissions are only checked on the base model. * The child inlines can't override the base model fields, only this parent inline can do that. """ formset = BasePolymorphicInlineFormSet #: The extra media to add for the polymorphic inlines effect. #: This can be redefined for subclasses. polymorphic_media = Media( js=( f"admin/js/vendor/jquery/{'jquery' if settings.DEBUG else 'jquery.min'}.js", "admin/js/jquery.init.js", "polymorphic/js/polymorphic_inlines.js", ), css={"all": ("polymorphic/css/polymorphic_inlines.css",)}, ) #: The extra forms to show #: By default there are no 'extra' forms as the desired type is unknown. #: Instead, add each new item using JavaScript that first offers a type-selection. extra = 0 #: Inlines for all model sub types that can be displayed in this inline. #: Each row is a :class:`PolymorphicInlineModelAdmin.Child` child_inlines = () def __init__(self, parent_model, admin_site): super().__init__(parent_model, admin_site) # Extra check to avoid confusion # While we could monkeypatch the admin here, better stay explicit. parent_admin = admin_site._registry.get(parent_model, None) if parent_admin is not None: # Can be None during check if not isinstance(parent_admin, PolymorphicInlineSupportMixin): raise ImproperlyConfigured( "To use polymorphic inlines, add the `PolymorphicInlineSupportMixin` mixin " "to the ModelAdmin that hosts the inline." ) # While the inline is created per request, the 'request' object is not known here. # Hence, creating all child inlines unconditionally, without checking permissions. self.child_inline_instances = self.get_child_inline_instances() # Create a lookup table self._child_inlines_lookup = {} for child_inline in self.child_inline_instances: self._child_inlines_lookup[child_inline.model] = child_inline def get_child_inlines(self): """ Return the derived inline classes which this admin should handle. This should return an iterable of :class:`~polymorphic.admin.inlines.PolymorphicInlineModelAdmin.Child` classes, to override :attr:`~polymorphic.admin.inlines.PolymorphicInlineModelAdmin.child_inlines`. """ return self.child_inlines or [] def get_child_inline_instances(self): """ :rtype List[PolymorphicInlineModelAdmin.Child] """ instances = [] for ChildInlineType in self.get_child_inlines(): instances.append(ChildInlineType(parent_inline=self)) return instances def get_child_inline_instance(self, model): """ Find the child inline for a given model. :rtype: PolymorphicInlineModelAdmin.Child """ try: return self._child_inlines_lookup[model] except KeyError: raise UnsupportedChildType(f"Model '{model.__name__}' not found in child_inlines") def get_formset(self, request, obj=None, **kwargs): """ Construct the inline formset class. This passes all class attributes to the formset. :rtype: type """ # Construct the FormSet class FormSet = super().get_formset(request, obj=obj, **kwargs) # Instead of completely redefining super().get_formset(), we use # the regular inlineformset_factory(), and amend that with our extra bits. # This code line is the essence of what polymorphic_inlineformset_factory() does. FormSet.child_forms = polymorphic_child_forms_factory( formset_children=self.get_formset_children(request, obj=obj) ) return FormSet def get_formset_children(self, request, obj=None): """ The formset 'children' provide the details for all child models that are part of this formset. It provides a stripped version of the modelform/formset factory methods. """ formset_children = [] for child_inline in self.child_inline_instances: # TODO: the children can be limited here per request based on permissions. formset_children.append(child_inline.get_formset_child(request, obj=obj)) return formset_children def get_fieldsets(self, request, obj=None): """ Hook for specifying fieldsets. """ if self.fieldsets: return self.fieldsets else: return [] # Avoid exposing fields to the child def get_fields(self, request, obj=None): if self.fields: return self.fields else: return [] # Avoid exposing fields to the child @property def media(self): # The media of the inline focuses on the admin settings, # whether to expose the scripts for filter_horizontal etc.. # The admin helper exposes the inline + formset media. base_media = super().media all_media = Media() add_media(all_media, base_media) # Add all media of the child inline instances for child_instance in self.child_inline_instances: child_media = child_instance.media # Avoid adding the same media object again and again if child_media._css != base_media._css and child_media._js != base_media._js: add_media(all_media, child_media) add_media(all_media, self.polymorphic_media) return all_media class Child(InlineModelAdmin): """ The child inline; which allows configuring the admin options for the child appearance. Note that not all options will be honored by the parent, notably the formset options: * :attr:`extra` * :attr:`min_num` * :attr:`max_num` The model form options however, will all be read. """ formset_child = PolymorphicFormSetChild extra = 0 # TODO: currently unused for the children. def __init__(self, parent_inline): self.parent_inline = parent_inline super(PolymorphicInlineModelAdmin.Child, self).__init__( parent_inline.parent_model, parent_inline.admin_site ) def get_formset(self, request, obj=None, **kwargs): # The child inline is only used to construct the form, # and allow to override the form field attributes. # The formset is created by the parent inline. raise RuntimeError("The child get_formset() is not used.") def get_fields(self, request, obj=None): if self.fields: return self.fields # Standard Django logic, use the form to determine the fields. # The form needs to pass through all factory logic so all 'excludes' are set as well. # Default Django does: form = self.get_formset(request, obj, fields=None).form # Use 'fields=None' avoids recursion in the field autodetection. form = self.get_formset_child(request, obj, fields=None).get_form() return list(form.base_fields) + list(self.get_readonly_fields(request, obj)) def get_formset_child(self, request, obj=None, **kwargs): """ Return the formset child that the parent inline can use to represent us. :rtype: PolymorphicFormSetChild """ # Similar to the normal get_formset(), the caller may pass fields to override the defaults settings # in the inline. In Django's GenericInlineModelAdmin.get_formset() this is also used in the same way, # to make sure the 'exclude' also contains the GFK fields. # # Hence this code is almost identical to InlineModelAdmin.get_formset() # and GenericInlineModelAdmin.get_formset() # # Transfer the local inline attributes to the formset child, # this allows overriding settings. if "fields" in kwargs: fields = kwargs.pop("fields") else: fields = flatten_fieldsets(self.get_fieldsets(request, obj)) if self.exclude is None: exclude = [] else: exclude = list(self.exclude) exclude.extend(self.get_readonly_fields(request, obj)) # Add forcefully, as Django 1.10 doesn't include readonly fields. exclude.append("polymorphic_ctype") if self.exclude is None and hasattr(self.form, "_meta") and self.form._meta.exclude: # Take the custom ModelForm's Meta.exclude into account only if the # InlineModelAdmin doesn't define its own. exclude.extend(self.form._meta.exclude) # can_delete = self.can_delete and self.has_delete_permission(request, obj) defaults = { "form": self.form, "fields": fields, "exclude": exclude or None, "formfield_callback": partial(self.formfield_for_dbfield, request=request), } defaults.update(kwargs) # This goes through the same logic that get_formset() calls # by passing the inline class attributes to modelform_factory() FormSetChildClass = self.formset_child return FormSetChildClass(self.model, **defaults) class StackedPolymorphicInline(PolymorphicInlineModelAdmin): """ Stacked inline for django-polymorphic models. Since tabular doesn't make much sense with changed fields, just offer this one. """ #: The default template to use. template = "admin/polymorphic/edit_inline/stacked.html" django-polymorphic-4.10.2/src/polymorphic/admin/parentadmin.py000066400000000000000000000350741513173623500245220ustar00rootroot00000000000000""" The parent admin displays the list view of the base model. """ from django.contrib import admin from django.contrib.admin.helpers import AdminErrorList, AdminForm from django.contrib.admin.templatetags.admin_urls import add_preserved_filters from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.db import models from django.http import Http404, HttpResponseRedirect from django.template.response import TemplateResponse from django.urls import URLResolver from django.utils.encoding import force_str from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from polymorphic.utils import get_base_polymorphic_model from .forms import PolymorphicModelChoiceForm class RegistrationClosed(RuntimeError): "The admin model can't be registered anymore at this point." class ChildAdminNotRegistered(RuntimeError): "The admin site for the model is not registered." class PolymorphicParentModelAdmin(admin.ModelAdmin): """ A admin interface that can displays different change/delete pages, depending on the polymorphic model. To use this class, one attribute need to be defined: * :attr:`child_models` should be a list models. Alternatively, the following methods can be implemented: * :func:`get_child_models` should return a list of models. * optionally, :func:`get_child_type_choices` can be overwritten to refine the choices for the add dialog. This class needs to be inherited by the model admin base class that is registered in the site. The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_child_models`. """ #: The base model that the class uses (auto-detected if not set explicitly) base_model = None #: The child models that should be displayed child_models = None #: Whether the list should be polymorphic too, leave to ``False`` to optimize polymorphic_list = False add_type_template = None add_type_form = PolymorphicModelChoiceForm #: The regular expression to filter the primary key in the URL. #: This accepts only numbers as defensive measure against catch-all URLs. #: If your primary key consists of string values, update this regular expression. pk_regex = r"(\d+|__fk__)" def __init__(self, model, admin_site, *args, **kwargs): super().__init__(model, admin_site, *args, **kwargs) self._is_setup = False if self.base_model is None: self.base_model = get_base_polymorphic_model(model) def _lazy_setup(self): if self._is_setup: return self._child_models = self.get_child_models() # Make absolutely sure that the child models don't use the old 0.9 format, # as of polymorphic 1.4 this deprecated configuration is no longer supported. # Instead, register the child models in the admin too. if self._child_models and not issubclass(self._child_models[0], models.Model): raise ImproperlyConfigured( "Since django-polymorphic 1.4, the `child_models` attribute " "and `get_child_models()` method should be a list of models only.\n" "The model-admin class should be registered in the regular Django admin." ) self._child_admin_site = self.admin_site self._is_setup = True def register_child(self, model, model_admin): """ Register a model with admin to display. """ # After the get_urls() is called, the URLs of the child model can't be exposed anymore to the Django URLconf, # which also means that a "Save and continue editing" button won't work. if self._is_setup: raise RegistrationClosed("The admin model can't be registered anymore at this point.") if not issubclass(model, self.base_model): raise TypeError(f"{model.__name__} should be a subclass of {self.base_model.__name__}") if not issubclass(model_admin, admin.ModelAdmin): raise TypeError( f"{model_admin.__name__} should be a subclass of {admin.ModelAdmin.__name__}" ) self._child_admin_site.register(model, model_admin) def get_child_models(self): """ Return the derived model classes which this admin should handle. This should return a list of tuples, exactly like :attr:`child_models` is. The model classes can be retrieved as ``base_model.__subclasses__()``, a setting in a config file, or a query of a plugin registration system at your option """ if self.child_models is None: raise NotImplementedError("Implement get_child_models() or child_models") return self.child_models def get_child_type_choices(self, request, action): """ Return a list of polymorphic types for which the user has the permission to perform the given action. """ self._lazy_setup() choices = [] content_types = ContentType.objects.get_for_models( *self.get_child_models(), for_concrete_models=False ) for model, ct in content_types.items(): perm_function_name = f"has_{action}_permission" model_admin = self._get_real_admin_by_model(model) perm_function = getattr(model_admin, perm_function_name) if not perm_function(request): continue choices.append((ct.id, model._meta.verbose_name)) return choices def _get_real_admin(self, object_id, super_if_self=True): try: obj = ( self.model.objects.non_polymorphic().values("polymorphic_ctype").get(pk=object_id) ) except self.model.DoesNotExist: raise Http404 return self._get_real_admin_by_ct(obj["polymorphic_ctype"], super_if_self=super_if_self) def _get_real_admin_by_ct(self, ct_id, super_if_self=True): try: ct = ContentType.objects.get_for_id(ct_id) except ContentType.DoesNotExist as e: raise Http404(e) # Handle invalid GET parameters model_class = ct.model_class() if not model_class: # Handle model deletion app_label, model = ct.natural_key() raise Http404(f"No model found for '{app_label}.{model}'.") return self._get_real_admin_by_model(model_class, super_if_self=super_if_self) def _get_real_admin_by_model(self, model_class, super_if_self=True): # In case of a ?ct_id=### parameter, the view is already checked for permissions. # Hence, make sure this is a derived object, or risk exposing other admin interfaces. if model_class not in self._child_models: raise PermissionDenied( f"Invalid model '{model_class}', it must be registered as child model." ) try: # HACK: the only way to get the instance of an model admin, # is to read the registry of the AdminSite. real_admin = self._child_admin_site._registry[model_class] except KeyError: raise ChildAdminNotRegistered( f"No child admin site was registered for a '{model_class}' model." ) if super_if_self and real_admin is self: return super() else: return real_admin def get_queryset(self, request): # optimize the list display. qs = super().get_queryset(request) if not self.polymorphic_list: qs = qs.non_polymorphic() return qs def add_view(self, request, form_url="", extra_context=None): """Redirect the add view to the real admin.""" ct_id = int(request.GET.get("ct_id", 0)) if not ct_id: # Display choices return self.add_type_view(request) else: real_admin = self._get_real_admin_by_ct(ct_id) # rebuild form_url, otherwise libraries below will override it. # Preserve popup-related parameters to ensure popup functionality works # correctly even after validation errors (issue #612) form_url = add_preserved_filters( { "preserved_filters": request.GET.urlencode(), "opts": self.model._meta, }, form_url, ) return real_admin.add_view(request, form_url, extra_context) def change_view(self, request, object_id, *args, **kwargs): """Redirect the change view to the real admin.""" real_admin = self._get_real_admin(object_id) return real_admin.change_view(request, object_id, *args, **kwargs) def changeform_view(self, request, object_id=None, *args, **kwargs): # The `changeform_view` is available as of Django 1.7, combining the add_view and change_view. # As it's directly called by django-reversion, this method is also overwritten to make sure it # also redirects to the child admin. if object_id: real_admin = self._get_real_admin(object_id) return real_admin.changeform_view(request, object_id, *args, **kwargs) else: # Add view. As it should already be handled via `add_view`, this means something custom is done here! return super().changeform_view(request, object_id, *args, **kwargs) def history_view(self, request, object_id, extra_context=None): """Redirect the history view to the real admin.""" real_admin = self._get_real_admin(object_id) return real_admin.history_view(request, object_id, extra_context=extra_context) def delete_view(self, request, object_id, extra_context=None): """Redirect the delete view to the real admin.""" real_admin = self._get_real_admin(object_id) return real_admin.delete_view(request, object_id, extra_context) def get_urls(self): """ Expose the custom URLs for the subclasses and the URL resolver. """ urls = super().get_urls() # At this point. all admin code needs to be known. self._lazy_setup() return urls def subclass_view(self, request, path): """ Forward any request to a custom view of the real admin. """ ct_id = int(request.GET.get("ct_id", 0)) if not ct_id: # See if the path started with an ID. try: pos = path.find("/") if pos == -1: object_id = int(path) else: object_id = int(path[0:pos]) except ValueError: raise Http404( f"No ct_id parameter, unable to find admin subclass for path '{path}'." ) ct_id = self.model.objects.values_list("polymorphic_ctype_id", flat=True).get( pk=object_id ) real_admin = self._get_real_admin_by_ct(ct_id) resolver = URLResolver("^", real_admin.urls) resolvermatch = resolver.resolve(path) # May raise Resolver404 if not resolvermatch: raise Http404(f"No match for path '{path}' in admin subclass.") return resolvermatch.func(request, *resolvermatch.args, **resolvermatch.kwargs) def add_type_view(self, request, form_url=""): """ Display a choice form to select which page type to add. """ if not self.has_add_permission(request): raise PermissionDenied extra_qs = "" if request.META["QUERY_STRING"]: # QUERY_STRING is bytes in Python 3, using force_str() to decode it as string. # See QueryDict how Django deals with that. # TODO: should this use a Django method instead of manipulating the string directly? extra_qs = f"&{force_str(request.META['QUERY_STRING'])}" choices = self.get_child_type_choices(request, "add") if len(choices) == 0: raise PermissionDenied if len(choices) == 1: return HttpResponseRedirect(f"?ct_id={choices[0][0]}{extra_qs}") # Create form form = self.add_type_form( data=request.POST if request.method == "POST" else None, initial={"ct_id": choices[0][0]}, ) form.fields["ct_id"].choices = choices if form.is_valid(): return HttpResponseRedirect(f"?ct_id={form.cleaned_data['ct_id']}{extra_qs}") # Wrap in all admin layout fieldsets = ((None, {"fields": ("ct_id",)}),) adminForm = AdminForm(form, fieldsets, {}, model_admin=self) media = self.media + adminForm.media opts = self.model._meta context = { "title": _("Add %s") % force_str(opts.verbose_name), "adminform": adminForm, "is_popup": ("_popup" in request.POST or "_popup" in request.GET), "media": mark_safe(media), "errors": AdminErrorList(form, ()), "app_label": opts.app_label, } return self.render_add_type_form(request, context, form_url) def render_add_type_form(self, request, context, form_url=""): """ Render the page type choice form. """ opts = self.model._meta app_label = opts.app_label context.update( { "has_change_permission": self.has_change_permission(request), "form_url": mark_safe(form_url), "opts": opts, "add": True, "save_on_top": self.save_on_top, **self.admin_site.each_context(request), } ) templates = self.add_type_template or [ f"admin/{app_label}/{opts.object_name.lower()}/add_type_form.html", f"admin/{app_label}/add_type_form.html", "admin/polymorphic/add_type_form.html", # added default here "admin/add_type_form.html", ] request.current_app = self.admin_site.name return self.admin_site.admin_view(TemplateResponse)(request, templates, context) @property def change_list_template(self): opts = self.model._meta app_label = opts.app_label # Pass the base options base_opts = self.base_model._meta base_app_label = base_opts.app_label return [ f"admin/{app_label}/{opts.object_name.lower()}/change_list.html", f"admin/{app_label}/change_list.html", # Added base class: f"admin/{base_app_label}/{base_opts.object_name.lower()}/change_list.html", f"admin/{base_app_label}/change_list.html", "admin/change_list.html", ] django-polymorphic-4.10.2/src/polymorphic/apps.py000066400000000000000000000053441513173623500220700ustar00rootroot00000000000000from django.apps import AppConfig, apps from django.core.checks import Error, Tags, Warning, register @register(Tags.models) def check_reserved_field_names(app_configs, **kwargs): """ System check that ensures models don't use reserved field names. """ from .models import PolymorphicModel findings = [] for app_config in app_configs or apps.get_app_configs(): for model in app_config.get_models(): if issubclass(model, PolymorphicModel): findings.extend(_check_model_reserved_field_names(model)) findings.extend(_check_polymorphic_managers(model)) return findings def _check_polymorphic_managers(model): from polymorphic.managers import PolymorphicManager from polymorphic.query import PolymorphicQuerySet findings = [] # First manager declared with use_in_migrations=True wins. for mgr in model._meta.managers: if getattr(mgr, "use_in_migrations", True): if isinstance(mgr, PolymorphicManager): findings.append( Error( f"The migration manager '{model._meta.label}.{mgr.name}' is polymorphic.", obj=mgr, hint="Set use_in_migrations = False on the manager.", id="polymorphic.E002", ) ) break for manager in ["base", "default"]: mgr = getattr(model._meta, f"{manager}_manager") if not isinstance(mgr, PolymorphicManager): findings.append( Warning( f"The {manager} manager {model._meta.label}.{mgr.name}' is not polymorphic.", obj=mgr, id="polymorphic.W001", ) ) if not isinstance(mgr.get_queryset(), PolymorphicQuerySet): findings.append( Warning( f"The {manager} manager {model._meta.label}.{mgr.name}' is not " "using a PolymorphicQuerySet.", obj=mgr, id="polymorphic.W002", ) ) return findings def _check_model_reserved_field_names(model): from polymorphic.base import POLYMORPHIC_SPECIAL_Q_KWORDS errors = [] for field in model._meta.get_fields(): if field.name in POLYMORPHIC_SPECIAL_Q_KWORDS: errors.append( Error( f"Field '{field.name}' on model '{model.__name__}' is a reserved name.", obj=field, id="polymorphic.E001", ) ) return errors class PolymorphicConfig(AppConfig): name = "polymorphic" verbose_name = "Django Polymorphic" django-polymorphic-4.10.2/src/polymorphic/base.py000066400000000000000000000217201513173623500220330ustar00rootroot00000000000000""" PolymorphicModel Meta Class """ import sys import warnings from django.db import models from django.db.models.base import ModelBase from .deletion import PolymorphicGuard from .managers import PolymorphicManager from .related_descriptors import ( NonPolymorphicForwardOneToOneDescriptor, NonPolymorphicReverseOneToOneDescriptor, ) from .utils import _clear_utility_caches # PolymorphicQuerySet Q objects (and filter()) support these additional key words. # These are forbidden as field names (a descriptive exception is raised) POLYMORPHIC_SPECIAL_Q_KWORDS = {"instance_of", "not_instance_of"} class ManagerInheritanceWarning(RuntimeWarning): pass # check that we're on cpython to enable dumpdata frame inspection guard check_dump = hasattr(sys, "_getframe") ################################################################################### # PolymorphicModel meta class class PolymorphicModelBase(ModelBase): """ Manager inheritance is a pretty complex topic which may need more thought regarding how this should be handled for polymorphic models. In any case, we probably should propagate 'objects' and 'base_objects' from PolymorphicModel to every subclass. We also want to somehow inherit/propagate _default_manager as well, as it needs to be polymorphic. The current implementation below is an experiment to solve this problem with a very simplistic approach: We unconditionally inherit/propagate any and all managers (using _copy_to_model), as long as they are defined on polymorphic models (the others are left alone). Like Django ModelBase, we special-case _default_manager: if there are any user-defined managers, it is set to the first of these. We also require that _default_manager as well as any user defined polymorphic managers produce querysets that are derived from PolymorphicQuerySet. We also replace the parent/child relation field descriptors with versions that will use non-polymorphic querysets. If we have inheritance of the form ModelA -> ModelB ->ModelC then Django creates accessors like this: - ModelA: modelb - ModelB: modela_ptr, modelb, modelc - ModelC: modela_ptr, modelb, modelb_ptr, modelc These accessors allow Django (and everyone else) to travel up and down the inheritance tree for the db object at hand. This is important for deletion among other things. """ def __new__(cls, model_name, bases, attrs, **kwargs): # create new model new_class = super().__new__(cls, model_name, bases, attrs, **kwargs) if new_class._meta.base_manager_name is None: # by default, use polymorphic manager as the base manager new_class._meta.base_manager_name = new_class._meta.default_manager_name or "objects" # ensure base_manager is a plain PolymorphicManager by resetting it if it # was not explicitly set and it defaults to a changed default_manager # the base class manager determination logic is complex enough that we prefer # to observe its application and correct rather than preempting it if ( type(new_class._meta.default_manager) is not PolymorphicManager and new_class._meta.base_manager is new_class._meta.default_manager ): manager = PolymorphicManager() manager.name = "_base_manager" manager.model = new_class manager.auto_created = True new_class._meta.base_manager_name = None # write new manager to property cache new_class._meta.__dict__["base_manager"] = manager # wrap on_delete handlers of reverse relations back to this model with the # polymorphic deletion guard for fk in new_class._meta.fields: if isinstance(fk, (models.ForeignKey, models.OneToOneField)) and not isinstance( fk.remote_field.on_delete, PolymorphicGuard ): fk.remote_field.on_delete = PolymorphicGuard(fk.remote_field.on_delete) # replace the parent/child descriptors if new_class._meta.parents and not (new_class._meta.abstract or new_class._meta.proxy): # PolymorphicModel is guaranteed to be defined here from .models import PolymorphicModel def replace_inheritance_descriptors(model): for super_cls, field_to_super in model._meta.parents.items(): if issubclass(super_cls, PolymorphicModel): if field_to_super is not None: setattr( new_class, field_to_super.name, NonPolymorphicForwardOneToOneDescriptor(field_to_super), ) setattr( super_cls, field_to_super.remote_field.related_name or field_to_super.remote_field.name, NonPolymorphicReverseOneToOneDescriptor( field_to_super.remote_field ), ) else: # pragma: no cover # proxy models have no field_to_super because the relations # are to the parent model - the else here should never # happen b/c we filter out proxy models above pass replace_inheritance_descriptors(super_cls) replace_inheritance_descriptors(new_class) _clear_utility_caches() return new_class @property def base_objects(self): warnings.warn( "Using PolymorphicModel.base_objects is deprecated.\n" f"Use {self.__class__.__name__}.objects.non_polymorphic() instead.", DeprecationWarning, stacklevel=2, ) return self._base_objects @property def _base_objects(self): # Create a manager so the API works as expected. Just don't register it # anymore in the Model Meta, so it doesn't substitute our polymorphic # manager as default manager for the third level of inheritance when # that third level doesn't define a manager at all. manager = models.Manager() manager.name = "base_objects" manager.model = self return manager @property def _default_manager(cls): mgr = super()._default_manager if ( check_dump and sys._getframe(1).f_globals.get("__name__") == "django.core.management.commands.dumpdata" ): # The downcasting of polymorphic querysets breaks dumpdata because it # expects to serialize multi-table models at each inheritance level. # dumpdata uses Model._default_manager to retrieve the objects by default # and uses Model._base_manager to retrieve objects if the --all flag is # specified. We need to make both of these managers polymorphic to satisfy # our contract that both Model.objects (_default_manager) is polymorphic and # reverse relations Other.related (_base_manager) to our polymorphic models # are also polymorphic. # # It would be best if load/dump data constructed its own managers like # migrations do, but it doesn't. The only way to get around this is to # detect when dumpdata is running and return the non-polymorphic manager in # that case. We do this here by inspecting the call stack and checking if # it came from the dumpdata command module. We use a CPython specific API # sys._getframe to inspect the call stack because it is very fast # (10s of nanoseconds) and disable the check if not on CPython # conceding that dumpdata will just not work in that case. It is important # that this check be fast because _default_manager is accessed very often. # inspect.stack() builds the entire stack frame and a bunch of complicated # datastructures - its use here should be avoided. # # Note that if you are stepping through this code in the debugger it will # be looking at the wrong frame because a bunch of debugging frames will be # on the top of the stack. return mgr.non_polymorphic() if isinstance(mgr, PolymorphicManager) else mgr return mgr @property def _base_manager(cls): mgr = super()._base_manager if ( check_dump and sys._getframe(1).f_globals.get("__name__") == "django.core.management.commands.dumpdata" ): # base manager is used when the --all flag is passed - see analogous comment # for _default_manager return mgr.non_polymorphic() if isinstance(mgr, PolymorphicManager) else mgr return mgr django-polymorphic-4.10.2/src/polymorphic/contrib/000077500000000000000000000000001513173623500222055ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/contrib/__init__.py000066400000000000000000000000001513173623500243040ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/contrib/drf/000077500000000000000000000000001513173623500227605ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/contrib/drf/LICENSE000066400000000000000000000020611513173623500237640ustar00rootroot00000000000000MIT License Copyright (c) 2017, Denis Orehovsky Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. django-polymorphic-4.10.2/src/polymorphic/contrib/drf/__init__.py000066400000000000000000000000001513173623500250570ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/contrib/drf/serializers.py000066400000000000000000000144601513173623500256730ustar00rootroot00000000000000from collections.abc import Mapping from django.core.exceptions import ImproperlyConfigured from django.db import models from rest_framework import serializers from rest_framework.fields import empty class PolymorphicSerializer(serializers.Serializer): model_serializer_mapping = None resource_type_field_name = "resourcetype" def __new__(cls, *args, **kwargs): if cls.model_serializer_mapping is None: raise ImproperlyConfigured( "`{cls}` is missing a `{cls}.model_serializer_mapping` attribute".format( cls=cls.__name__ ) ) if not isinstance(cls.resource_type_field_name, str): raise ImproperlyConfigured( "`{cls}.resource_type_field_name` must be a string".format(cls=cls.__name__) ) return super(PolymorphicSerializer, cls).__new__(cls, *args, **kwargs) def __init__(self, *args, **kwargs): super(PolymorphicSerializer, self).__init__(*args, **kwargs) model_serializer_mapping = self.model_serializer_mapping self.model_serializer_mapping = {} self.resource_type_model_mapping = {} for model, serializer in model_serializer_mapping.items(): resource_type = self.to_resource_type(model) if callable(serializer): serializer = serializer(*args, **kwargs) serializer.parent = self self.resource_type_model_mapping[resource_type] = model self.model_serializer_mapping[model] = serializer # ---------- # Public API def to_resource_type(self, model_or_instance): return model_or_instance._meta.object_name def to_representation(self, instance): if isinstance(instance, Mapping): resource_type = self._get_resource_type_from_mapping(instance) serializer = self._get_serializer_from_resource_type(resource_type) else: resource_type = self.to_resource_type(instance) serializer = self._get_serializer_from_model_or_instance(instance) ret = serializer.to_representation(instance) ret[self.resource_type_field_name] = resource_type return ret def to_internal_value(self, data): if self.partial and self.instance: resource_type = self.to_resource_type(self.instance) serializer = self._get_serializer_from_model_or_instance(self.instance) else: resource_type = self._get_resource_type_from_mapping(data) serializer = self._get_serializer_from_resource_type(resource_type) ret = serializer.to_internal_value(data) ret[self.resource_type_field_name] = resource_type return ret def create(self, validated_data): resource_type = validated_data.pop(self.resource_type_field_name) serializer = self._get_serializer_from_resource_type(resource_type) return serializer.create(validated_data) def update(self, instance, validated_data): resource_type = validated_data.pop(self.resource_type_field_name) serializer = self._get_serializer_from_resource_type(resource_type) return serializer.update(instance, validated_data) def is_valid(self, *args, **kwargs): valid = super(PolymorphicSerializer, self).is_valid(*args, **kwargs) try: if self.partial and self.instance: resource_type = self.to_resource_type(self.instance) serializer = self._get_serializer_from_model_or_instance(self.instance) else: resource_type = self._get_resource_type_from_mapping(self.initial_data) serializer = self._get_serializer_from_resource_type(resource_type) except serializers.ValidationError: child_valid = False else: child_valid = serializer.is_valid(*args, **kwargs) # Update parent's validated_data with child's validated_data # to preserve any modifications made in child's validate() method if child_valid and hasattr(self, "_validated_data"): self._validated_data.update(serializer._validated_data) self._errors.update(serializer.errors) return valid and child_valid def run_validation(self, data=empty): if self.partial and self.instance: resource_type = self.to_resource_type(self.instance) serializer = self._get_serializer_from_model_or_instance(self.instance) else: resource_type = self._get_resource_type_from_mapping(data) serializer = self._get_serializer_from_resource_type(resource_type) validated_data = serializer.run_validation(data) validated_data[self.resource_type_field_name] = resource_type return validated_data # -------------- # Implementation def _to_model(self, model_or_instance): return ( model_or_instance.__class__ if isinstance(model_or_instance, models.Model) else model_or_instance ) def _get_resource_type_from_mapping(self, mapping): try: return mapping[self.resource_type_field_name] except KeyError: raise serializers.ValidationError( { self.resource_type_field_name: "This field is required", } ) def _get_serializer_from_model_or_instance(self, model_or_instance): model = self._to_model(model_or_instance) for klass in model.mro(): if klass in self.model_serializer_mapping: return self.model_serializer_mapping[klass] raise KeyError( "`{cls}.model_serializer_mapping` is missing " "a corresponding serializer for `{model}` model".format( cls=self.__class__.__name__, model=model.__name__ ) ) def _get_serializer_from_resource_type(self, resource_type): try: model = self.resource_type_model_mapping[resource_type] except KeyError: raise serializers.ValidationError( { self.resource_type_field_name: "Invalid {0}".format( self.resource_type_field_name ) } ) return self._get_serializer_from_model_or_instance(model) django-polymorphic-4.10.2/src/polymorphic/contrib/extra_views.py000066400000000000000000000077351513173623500251330ustar00rootroot00000000000000""" The ``extra_views.formsets`` provides a simple way to handle formsets. The ``extra_views.advanced`` provides a method to combine that with a create/update form. This package provides classes that support both options for polymorphic formsets. """ import extra_views from django.core.exceptions import ImproperlyConfigured from polymorphic.formsets import ( BasePolymorphicInlineFormSet, BasePolymorphicModelFormSet, polymorphic_child_forms_factory, ) __all__ = ( "PolymorphicFormSetView", "PolymorphicInlineFormSetView", "PolymorphicInlineFormSet", ) class PolymorphicFormSetMixin: """ Internal Mixin, that provides polymorphic integration with the ``extra_views`` package. """ formset_class = BasePolymorphicModelFormSet #: Default 0 extra forms factory_kwargs = {"extra": 0} #: Define the children # :type: list[PolymorphicFormSetChild] formset_children = None def get_formset_children(self): """ :rtype: list[PolymorphicFormSetChild] """ if not self.formset_children: raise ImproperlyConfigured( "Define 'formset_children' as list of `PolymorphicFormSetChild`" ) return self.formset_children def get_formset_child_kwargs(self): return {} def get_formset(self): """ Returns the formset class from the inline formset factory """ # Implementation detail: # Since `polymorphic_modelformset_factory` and `polymorphic_inlineformset_factory` mainly # reuse the standard factories, and then add `child_forms`, the same can be done here. # This makes sure the base class construction is completely honored. FormSet = super().get_formset() FormSet.child_forms = polymorphic_child_forms_factory( self.get_formset_children(), **self.get_formset_child_kwargs() ) return FormSet class PolymorphicFormSetView(PolymorphicFormSetMixin, extra_views.ModelFormSetView): """ A view that displays a single polymorphic formset. .. code-block:: python from polymorphic.formsets import PolymorphicFormSetChild class ItemsView(PolymorphicFormSetView): model = Item formset_children = [ PolymorphicFormSetChild(ItemSubclass1), PolymorphicFormSetChild(ItemSubclass2), ] """ formset_class = BasePolymorphicModelFormSet class PolymorphicInlineFormSetView(PolymorphicFormSetMixin, extra_views.InlineFormSetView): """ A view that displays a single polymorphic formset - with one parent object. This is a variation of the :mod:`extra_views` package classes for django-polymorphic. .. code-block:: python from polymorphic.formsets import PolymorphicFormSetChild class OrderItemsView(PolymorphicInlineFormSetView): model = Order inline_model = Item formset_children = [ PolymorphicFormSetChild(ItemSubclass1), PolymorphicFormSetChild(ItemSubclass2), ] """ formset_class = BasePolymorphicInlineFormSet class PolymorphicInlineFormSet(PolymorphicFormSetMixin, extra_views.InlineFormSetFactory): """ An inline to add to the ``inlines`` of the :class:`~extra_views.advanced.CreateWithInlinesView` and :class:`~extra_views.advanced.UpdateWithInlinesView` class. .. code-block:: python from polymorphic.formsets import PolymorphicFormSetChild class ItemsInline(PolymorphicInlineFormSet): model = Item formset_children = [ PolymorphicFormSetChild(ItemSubclass1), PolymorphicFormSetChild(ItemSubclass2), ] class OrderCreateView(CreateWithInlinesView): model = Order inlines = [ItemsInline] def get_success_url(self): return self.object.get_absolute_url() """ formset_class = BasePolymorphicInlineFormSet django-polymorphic-4.10.2/src/polymorphic/contrib/guardian.py000066400000000000000000000014211513173623500243470ustar00rootroot00000000000000from django.contrib.contenttypes.models import ContentType from ..models import PolymorphicModel from ..utils import get_base_polymorphic_model def get_polymorphic_base_content_type(obj): """ Helper function to return the base polymorphic content type id. This should used with django-guardian and the ``GUARDIAN_GET_CONTENT_TYPE`` option. See the django-guardian documentation for more information: https://django-guardian.readthedocs.io/en/latest/configuration """ model_type = obj if isinstance(obj, type) else type(obj) if issubclass(model_type, PolymorphicModel) and ( base := get_base_polymorphic_model(model_type) ): return ContentType.objects.get_for_model(base) return ContentType.objects.get_for_model(model_type) django-polymorphic-4.10.2/src/polymorphic/deletion.py000066400000000000000000000101631513173623500227230ustar00rootroot00000000000000""" Classes and utilities for handling deletions in polymorphic models. """ from functools import cached_property from django.db.migrations.serializer import BaseSerializer, serializer_factory from django.db.migrations.writer import MigrationWriter from .query import PolymorphicQuerySet def migration_fingerprint(value): """ Produce a stable, hashable fingerprint for a value as Django would represent it in migrations, but in a structured form when possible. """ # Canonical deconstruction path for SET(...), @deconstructible, etc. deconstruct = getattr(value, "deconstruct", None) if callable(deconstruct): path, args, kwargs = value.deconstruct() return ( path, tuple(migration_fingerprint(a) for a in args), tuple(sorted((k, migration_fingerprint(v)) for k, v in kwargs.items())), ) # Fallback: canonical "code string" Django would emit in a migration. # (Works for CASCADE/PROTECT/SET_NULL, primitives, etc.) code, _imports = serializer_factory(value).serialize() return code class PolymorphicGuard: """ Wrap an :attr:`django.db.models.ForeignKey.on_delete` callable (CASCADE/PROTECT/SET_NULL/SET(...)/custom), but serialize as the underlying callable. :param action: The :attr:`django.db.models.ForeignKey.on_delete` callable to wrap. """ def __init__(self, action): if not callable(action): raise TypeError("action must be callable") self.action = action def __call__(self, collector, field, sub_objs, using): """ This guard wraps an on_delete action to ensure that any polymorphic queryset passed to it is converted to a non-polymorphic queryset before proceeding. This prevents issues with cascading deletes on polymorphic models. This guard should be automatically applied to reverse relations such that .. code-block:: python class MyModel(PolymorphicModel): related = models.ForeignKey( OtherModel, on_delete=models.CASCADE # <- equal to PolymorphicGuard(models.CASCADE) ) """ if isinstance(sub_objs, PolymorphicQuerySet) and not sub_objs.polymorphic_disabled: sub_objs = sub_objs.non_polymorphic() return self.action(collector, field, sub_objs, using) @cached_property def migration_key(self): return migration_fingerprint(self.action) def __eq__(self, other): if ( isinstance(other, tuple) and len(other) == 3 and callable(getattr(self.action, "deconstruct", None)) ): # In some cases the autodetector compares us to a reconstructed, # deconstruct() tuple. This has been seen for SET(...) callables. # The arguments element may be a list instead of a tuple though, this # handles that special case return self.action.deconstruct() == ( other[0], tuple(other[1]) if isinstance(other[1], list) else other[1], other[2], ) if isinstance(other, PolymorphicGuard): return self.migration_key == other.migration_key else: try: return self.migration_key == migration_fingerprint(other) except Exception: return False def __hash__(self): return hash(self.migration_key) class PolymorphicGuardSerializer(BaseSerializer): """ A serializer for PolymorphicGuard that serializes the underlying action. There is no need to serialize the PolymorphicGuard itself, as it is just a wrapper that ensures that polymorphic querysets are converted to non-polymorphic but no polymorphic managers are present in migrations. This also ensures that new migrations will not be generated. """ def serialize(self): """ Serialize the underlying action of the PolymorphicGuard. """ return serializer_factory(self.value.action).serialize() MigrationWriter.register_serializer(PolymorphicGuard, PolymorphicGuardSerializer) django-polymorphic-4.10.2/src/polymorphic/formsets/000077500000000000000000000000001513173623500224075ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/formsets/__init__.py000066400000000000000000000025601513173623500245230ustar00rootroot00000000000000""" This allows creating formsets where each row can be a different form type. The logic of the formsets work similar to the standard Django formsets; there are factory methods to construct the classes with the proper form settings. The "parent" formset hosts the entire model and their child model. For every child type, there is an :class:`PolymorphicFormSetChild` instance that describes how to display and construct the child. It's parameters are very similar to the parent's factory method. """ from .generic import ( # Can import generic here, as polymorphic already depends on the 'contenttypes' app. BaseGenericPolymorphicInlineFormSet, GenericPolymorphicFormSetChild, generic_polymorphic_inlineformset_factory, ) from .models import ( BasePolymorphicInlineFormSet, BasePolymorphicModelFormSet, PolymorphicFormSetChild, UnsupportedChildType, polymorphic_child_forms_factory, polymorphic_inlineformset_factory, polymorphic_modelformset_factory, ) __all__ = ( "BasePolymorphicModelFormSet", "BasePolymorphicInlineFormSet", "PolymorphicFormSetChild", "UnsupportedChildType", "polymorphic_modelformset_factory", "polymorphic_inlineformset_factory", "polymorphic_child_forms_factory", "BaseGenericPolymorphicInlineFormSet", "GenericPolymorphicFormSetChild", "generic_polymorphic_inlineformset_factory", ) django-polymorphic-4.10.2/src/polymorphic/formsets/generic.py000066400000000000000000000101301513173623500243700ustar00rootroot00000000000000from django.contrib.contenttypes.forms import ( BaseGenericInlineFormSet, generic_inlineformset_factory, ) from django.contrib.contenttypes.models import ContentType from django.db import models from django.forms.models import ModelForm from .models import ( BasePolymorphicModelFormSet, PolymorphicFormSetChild, polymorphic_child_forms_factory, ) class GenericPolymorphicFormSetChild(PolymorphicFormSetChild): """ Formset child for generic inlines """ def __init__(self, *args, **kwargs): self.ct_field = kwargs.pop("ct_field", "content_type") self.fk_field = kwargs.pop("fk_field", "object_id") super().__init__(*args, **kwargs) def get_form(self, ct_field="content_type", fk_field="object_id", **kwargs): """ Construct the form class for the formset child. """ exclude = list(self.exclude) extra_exclude = kwargs.pop("extra_exclude", None) if extra_exclude: exclude += list(extra_exclude) # Make sure the GFK fields are excluded by default # This is similar to what generic_inlineformset_factory() does # if there is no field called `ct_field` let the exception propagate opts = self.model._meta ct_field = opts.get_field(self.ct_field) if ( not isinstance(ct_field, models.ForeignKey) or ct_field.remote_field.model != ContentType ): raise Exception(f"fk_name '{ct_field}' is not a ForeignKey to ContentType") fk_field = opts.get_field(self.fk_field) # let the exception propagate exclude.extend([ct_field.name, fk_field.name]) kwargs["exclude"] = exclude return super().get_form(**kwargs) class BaseGenericPolymorphicInlineFormSet(BaseGenericInlineFormSet, BasePolymorphicModelFormSet): """ Polymorphic formset variation for inline generic formsets """ def generic_polymorphic_inlineformset_factory( model, formset_children, form=ModelForm, formset=BaseGenericPolymorphicInlineFormSet, ct_field="content_type", fk_field="object_id", # Base form # TODO: should these fields be removed in favor of creating # the base form as a formset child too? fields=None, exclude=None, extra=1, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False, for_concrete_model=True, min_num=None, validate_min=False, child_form_kwargs=None, ): """ Construct the class for a generic inline polymorphic formset. All arguments are identical to :func:`~django.contrib.contenttypes.forms.generic_inlineformset_factory`, with the exception of the ``formset_children`` argument. :param formset_children: A list of all child :class:`PolymorphicFormSetChild` objects that tell the inline how to render the child model types. :type formset_children: Iterable[PolymorphicFormSetChild] :rtype: type """ kwargs = { "model": model, "form": form, "formfield_callback": formfield_callback, "formset": formset, "ct_field": ct_field, "fk_field": fk_field, "extra": extra, "can_delete": can_delete, "can_order": can_order, "fields": fields, "exclude": exclude, "min_num": min_num, "max_num": max_num, "validate_min": validate_min, "validate_max": validate_max, "for_concrete_model": for_concrete_model, # 'localized_fields': localized_fields, # 'labels': labels, # 'help_texts': help_texts, # 'error_messages': error_messages, # 'field_classes': field_classes, } if child_form_kwargs is None: child_form_kwargs = {} child_kwargs = { # 'exclude': exclude, "ct_field": ct_field, "fk_field": fk_field, } if child_form_kwargs: child_kwargs.update(child_form_kwargs) FormSet = generic_inlineformset_factory(**kwargs) FormSet.child_forms = polymorphic_child_forms_factory(formset_children, **child_kwargs) return FormSet django-polymorphic-4.10.2/src/polymorphic/formsets/models.py000066400000000000000000000416361513173623500242560ustar00rootroot00000000000000from collections import OrderedDict from django import forms from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, ValidationError from django.forms.models import ( BaseInlineFormSet, BaseModelFormSet, ModelForm, inlineformset_factory, modelform_factory, modelformset_factory, ) from django.utils.functional import cached_property from polymorphic.models import PolymorphicModel from .utils import add_media class UnsupportedChildType(LookupError): pass class PolymorphicFormSetChild: """ Metadata to define the inline of a polymorphic child. Provide this information in the :func:'polymorphic_inlineformset_factory' construction. """ def __init__( self, model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None, labels=None, help_texts=None, error_messages=None, ): self.model = model # Instead of initializing the form here right away, # the settings are saved so get_form() can receive additional exclude kwargs. # This is mostly needed for the generic inline formsets self._form_base = form self.fields = fields # Normalize exclude=None to () to match Django's formset behavior self.exclude = () if exclude is None else exclude self.formfield_callback = formfield_callback self.widgets = widgets self.localized_fields = localized_fields self.labels = labels self.help_texts = help_texts self.error_messages = error_messages @cached_property def content_type(self): """ Expose the ContentType that the child relates to. This can be used for the ''polymorphic_ctype'' field. """ return ContentType.objects.get_for_model(self.model, for_concrete_model=False) def get_form(self, **kwargs): """ Construct the form class for the formset child. """ # Do what modelformset_factory() / inlineformset_factory() does to the 'form' argument; # Construct the form with the given ModelFormOptions values # Fields can be overwritten. To support the global 'polymorphic_child_forms_factory' kwargs, # that doesn't completely replace all 'exclude' settings defined per child type, # we allow to define things like 'extra_...' fields that are amended to the current child settings. # Handle exclude parameter carefully: # - If exclude was explicitly provided (not empty), use it # - If extra_exclude is provided, merge it with self.exclude # - If neither was provided, don't pass exclude to modelform_factory at all, # allowing the form's Meta.exclude to take effect extra_exclude = kwargs.pop("extra_exclude", None) # Determine if we should pass exclude to modelform_factory # Treat empty tuples/lists the same as None to allow form's Meta.exclude to take effect should_pass_exclude = bool(self.exclude) or extra_exclude is not None if should_pass_exclude: if self.exclude: exclude = list(self.exclude) else: exclude = [] if extra_exclude: exclude += list(extra_exclude) defaults = { "form": self._form_base, "formfield_callback": self.formfield_callback, "fields": self.fields, # 'for_concrete_model': for_concrete_model, "localized_fields": self.localized_fields, "labels": self.labels, "help_texts": self.help_texts, "error_messages": self.error_messages, "widgets": self.widgets, # 'field_classes': field_classes, } # Only add exclude to defaults if we determined it should be passed if should_pass_exclude: defaults["exclude"] = exclude defaults.update(kwargs) return modelform_factory(self.model, **defaults) def polymorphic_child_forms_factory(formset_children, **kwargs): """ Construct the forms for the formset children. This is mostly used internally, and rarely needs to be used by external projects. When using the factory methods (:func:'polymorphic_inlineformset_factory'), this feature is called already for you. """ child_forms = OrderedDict() for formset_child in formset_children: child_forms[formset_child.model] = formset_child.get_form(**kwargs) return child_forms class BasePolymorphicModelFormSet(BaseModelFormSet): """ A formset that can produce different forms depending on the object type. Note that the 'add' feature is therefore more complex, as all variations need ot be exposed somewhere. When switching existing formsets to the polymorphic formset, note that the ID field will no longer be named ''model_ptr'', but just appear as ''id''. """ # Assigned by the factory child_forms = OrderedDict() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.queryset_data = self.get_queryset() def _construct_form(self, i, **kwargs): """ Create the form, depending on the model that's behind it. """ # BaseModelFormSet logic if self.is_bound and i < self.initial_form_count(): pk_key = f"{self.add_prefix(i)}-{self.model._meta.pk.name}" pk = self.data[pk_key] pk_field = self.model._meta.pk to_python = self._get_to_python(pk_field) pk = to_python(pk) kwargs["instance"] = self._existing_object(pk) if i < self.initial_form_count() and "instance" not in kwargs: kwargs["instance"] = self.get_queryset()[i] if i >= self.initial_form_count() and self.initial_extra: # Set initial values for extra forms try: kwargs["initial"] = self.initial_extra[i - self.initial_form_count()] except IndexError: pass # BaseFormSet logic, with custom formset_class defaults = { "auto_id": self.auto_id, "prefix": self.add_prefix(i), "error_class": self.error_class, } if self.is_bound: defaults["data"] = self.data defaults["files"] = self.files if self.initial and "initial" not in kwargs: try: defaults["initial"] = self.initial[i] except IndexError: pass # Allow extra forms to be empty, unless they're part of # the minimum forms. if i >= self.initial_form_count() and i >= self.min_num: defaults["empty_permitted"] = True defaults["use_required_attribute"] = False defaults.update(kwargs) # Need to find the model that will be displayed in this form. # Hence, peeking in the self.queryset_data beforehand. if self.is_bound: if "instance" in defaults and defaults["instance"] is not None: # Object is already bound to a model, won't change the content type model = defaults["instance"].get_real_instance_class() # allow proxy models else: # Extra or empty form, use the provided type. # Note this completely tru prefix = defaults["prefix"] try: ct_id = int(self.data[f"{prefix}-polymorphic_ctype"]) except (KeyError, ValueError): raise ValidationError( f"Formset row {prefix} has no 'polymorphic_ctype' defined!" ) model = ContentType.objects.get_for_id(ct_id).model_class() if model not in self.child_forms: # Perform basic validation, as we skip the ChoiceField here. raise UnsupportedChildType( f"Child model type {model} is not part of the formset" ) else: if "instance" in defaults and defaults["instance"] is not None: model = defaults["instance"].get_real_instance_class() # allow proxy models elif "polymorphic_ctype" in defaults.get("initial", {}): ct_value = defaults["initial"]["polymorphic_ctype"] # Handle both ContentType instances and IDs if isinstance(ct_value, ContentType): model = ct_value.model_class() else: model = ContentType.objects.get_for_id(ct_value).model_class() elif i < len(self.queryset_data): model = self.queryset_data[i].__class__ else: # Extra forms, cycle between all types # TODO: take the 'extra' value of each child formset into account. total_known = len(self.queryset_data) child_models = list(self.child_forms.keys()) model = child_models[(i - total_known) % len(child_models)] # Normalize polymorphic_ctype in initial data if it's a ContentType instance # This allows users to set initial[i]['polymorphic_ctype'] = ct (ContentType instance) # while the form field expects an integer ID # We do this AFTER determining the model so the model determination can use the ContentType if "initial" in defaults and "polymorphic_ctype" in defaults["initial"]: ct_value = defaults["initial"]["polymorphic_ctype"] if isinstance(ct_value, ContentType): # Create a copy to avoid modifying the original formset.initial defaults["initial"] = defaults["initial"].copy() # Convert ContentType instance to its ID defaults["initial"]["polymorphic_ctype"] = ct_value.pk form_class = self.get_form_class(model) form = form_class(**defaults) self.add_fields(form, i) return form def add_fields(self, form, index): """Add a hidden field for the content type.""" ct = ContentType.objects.get_for_model(form._meta.model, for_concrete_model=False) choices = [(ct.pk, ct)] # Single choice, existing forms can't change the value. form.fields["polymorphic_ctype"] = forms.TypedChoiceField( choices=choices, initial=ct.pk, required=False, widget=forms.HiddenInput, coerce=int, ) super().add_fields(form, index) def get_form_class(self, model): """ Return the proper form class for the given model. """ if not self.child_forms: raise ImproperlyConfigured(f"No 'child_forms' defined in {self.__class__.__name__}") if not issubclass(model, PolymorphicModel): raise TypeError(f"Expect polymorphic model type, not {model}") try: return self.child_forms[model] except KeyError: # This may happen when the query returns objects of a type that was not handled by the formset. raise UnsupportedChildType( f"The '{self.__class__.__name__}' found a '{model.__name__}' model in the queryset, " f"but no form class is registered to display it." ) def is_multipart(self): """ Returns True if the formset needs to be multipart, i.e. it has FileInput. Otherwise, False. """ return any(f.is_multipart() for f in self.empty_forms) @property def media(self): # Include the media of all form types. # The form media includes all form widget media media = forms.Media() for form in self.empty_forms: add_media(media, form.media) return media @cached_property def empty_forms(self): """ Return all possible empty forms """ forms = [] for model, form_class in self.child_forms.items(): kwargs = self.get_form_kwargs(None) form = form_class( auto_id=self.auto_id, prefix=self.add_prefix("__prefix__"), empty_permitted=True, use_required_attribute=False, **kwargs, ) self.add_fields(form, None) forms.append(form) return forms @property def empty_form(self): # TODO: make an exception when can_add_base is defined? raise RuntimeError( "'empty_form' is not used in polymorphic formsets, use 'empty_forms' instead." ) def polymorphic_modelformset_factory( model, formset_children, formset=BasePolymorphicModelFormSet, # Base field # TODO: should these fields be removed in favor of creating # the base form as a formset child too? form=ModelForm, fields=None, exclude=None, extra=1, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, child_form_kwargs=None, ): """ Construct the class for an polymorphic model formset. All arguments are identical to :func:'~django.forms.models.modelformset_factory', with the exception of the ''formset_children'' argument. :param formset_children: A list of all child :class:'PolymorphicFormSetChild' objects that tell the inline how to render the child model types. :type formset_children: Iterable[PolymorphicFormSetChild] :rtype: type """ kwargs = { "model": model, "form": form, "formfield_callback": formfield_callback, "formset": formset, "extra": extra, "can_delete": can_delete, "can_order": can_order, "fields": fields, "exclude": exclude, "min_num": min_num, "max_num": max_num, "widgets": widgets, "validate_min": validate_min, "validate_max": validate_max, "localized_fields": localized_fields, "labels": labels, "help_texts": help_texts, "error_messages": error_messages, "field_classes": field_classes, } FormSet = modelformset_factory(**kwargs) child_kwargs = { "fields": fields, # 'exclude': exclude, } if child_form_kwargs: child_kwargs.update(child_form_kwargs) FormSet.child_forms = polymorphic_child_forms_factory(formset_children, **child_kwargs) return FormSet class BasePolymorphicInlineFormSet(BaseInlineFormSet, BasePolymorphicModelFormSet): """ Polymorphic formset variation for inline formsets """ def _construct_form(self, i, **kwargs): return super()._construct_form(i, **kwargs) def polymorphic_inlineformset_factory( parent_model, model, formset_children, formset=BasePolymorphicInlineFormSet, fk_name=None, # Base field # TODO: should these fields be removed in favor of creating # the base form as a formset child too? form=ModelForm, fields=None, exclude=None, extra=1, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, child_form_kwargs=None, ): """ Construct the class for an inline polymorphic formset. All arguments are identical to :func:'~django.forms.models.inlineformset_factory', with the exception of the ''formset_children'' argument. :param formset_children: A list of all child :class:'PolymorphicFormSetChild' objects that tell the inline how to render the child model types. :type formset_children: Iterable[PolymorphicFormSetChild] :rtype: type """ kwargs = { "parent_model": parent_model, "model": model, "form": form, "formfield_callback": formfield_callback, "formset": formset, "fk_name": fk_name, "extra": extra, "can_delete": can_delete, "can_order": can_order, "fields": fields, "exclude": exclude, "min_num": min_num, "max_num": max_num, "widgets": widgets, "validate_min": validate_min, "validate_max": validate_max, "localized_fields": localized_fields, "labels": labels, "help_texts": help_texts, "error_messages": error_messages, "field_classes": field_classes, } FormSet = inlineformset_factory(**kwargs) child_kwargs = { "fields": fields, # 'exclude': exclude, } if child_form_kwargs: child_kwargs.update(child_form_kwargs) FormSet.child_forms = polymorphic_child_forms_factory(formset_children, **child_kwargs) return FormSet django-polymorphic-4.10.2/src/polymorphic/formsets/utils.py000066400000000000000000000003641513173623500241240ustar00rootroot00000000000000""" Internal utils """ def add_media(dest, media): """ Optimized version of django.forms.Media.__add__() that doesn't create new objects. """ dest._css_lists.extend(media._css_lists) dest._js_lists.extend(media._js_lists) django-polymorphic-4.10.2/src/polymorphic/locale/000077500000000000000000000000001513173623500220045ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/locale/en/000077500000000000000000000000001513173623500224065ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/locale/en/LC_MESSAGES/000077500000000000000000000000001513173623500241735ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/locale/en/LC_MESSAGES/django.po000066400000000000000000000013071513173623500257760ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-11-29 18:12+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: admin.py:41 msgid "Type" msgstr "" #: admin.py:56 msgid "Content type" msgstr "" #: admin.py:403 msgid "Contents" msgstr "" django-polymorphic-4.10.2/src/polymorphic/locale/es/000077500000000000000000000000001513173623500224135ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/locale/es/LC_MESSAGES/000077500000000000000000000000001513173623500242005ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/locale/es/LC_MESSAGES/django.po000066400000000000000000000013621513173623500260040ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Gonzalo Bustos, 2015. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-11-29 18:12+0100\n" "PO-Revision-Date: 2015-10-12 11:42-0300\n" "Last-Translator: Gonzalo Bustos\n" "Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.6.10\n" #: admin.py:41 msgid "Type" msgstr "Tipo" #: admin.py:56 msgid "Content type" msgstr "Tipo de contenido" #: admin.py:333 admin.py:403 #, python-format msgid "Contents" msgstr "Contenidos" django-polymorphic-4.10.2/src/polymorphic/locale/fr/000077500000000000000000000000001513173623500224135ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/locale/fr/LC_MESSAGES/000077500000000000000000000000001513173623500242005ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/locale/fr/LC_MESSAGES/django.po000066400000000000000000000015711513173623500260060ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-11-29 18:12+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: admin.py:41 msgid "Type" msgstr "Type" #: admin.py:56 msgid "Content type" msgstr "Type de contenu" # This is already translated in Django # #: admin.py:333 # #, python-format # msgid "Add %s" # msgstr "" #: admin.py:403 msgid "Contents" msgstr "Contenus" django-polymorphic-4.10.2/src/polymorphic/managers.py000066400000000000000000000067271513173623500227300ustar00rootroot00000000000000""" The manager class for use in the models. """ from django.contrib.contenttypes.models import ContentType from django.db import DEFAULT_DB_ALIAS, models, transaction from polymorphic.query import PolymorphicQuerySet __all__ = ("PolymorphicManager", "PolymorphicQuerySet") class PolymorphicManager(models.Manager): """ Manager for PolymorphicModel Usually not explicitly needed, except if a custom manager or a custom queryset class is to be used. """ queryset_class = PolymorphicQuerySet @classmethod def from_queryset(cls, queryset_class, class_name=None): manager = super().from_queryset(queryset_class, class_name=class_name) # also set our version, Django uses _queryset_class manager.queryset_class = queryset_class return manager def get_queryset(self): qs = self.queryset_class(self.model, using=self._db, hints=self._hints) if self.model._meta.proxy: qs = qs.instance_of(self.model) return qs def __str__(self): return ( f"{self.__class__.__name__} (PolymorphicManager) using {self.queryset_class.__name__}" ) # Proxied methods def non_polymorphic(self): return self.all().non_polymorphic() def instance_of(self, *args): return self.all().instance_of(*args) def not_instance_of(self, *args): return self.all().not_instance_of(*args) def get_real_instances(self, base_result_objects=None): return self.all().get_real_instances(base_result_objects=base_result_objects) def create_from_super(self, obj, **kwargs): """ Create an instance of this manager's model class from the given instance of a parent class. This is useful when "promoting" an instance down the inheritance chain. :param obj: An instance of a parent class of the manager's model class. :param kwargs: Additional fields to set on the new instance. :return: The newly created instance. """ from .models import PolymorphicModel with transaction.atomic(using=obj._state.db or DEFAULT_DB_ALIAS): # ensure we have the most derived real instance if isinstance(obj, PolymorphicModel): obj = obj.get_real_instance() parent_ptr = self.model._meta.parents.get(type(obj), None) if not parent_ptr: raise TypeError( f"{obj.__class__.__name__} is not a direct parent of {self.model.__name__}" ) kwargs[parent_ptr.get_attname()] = obj.pk # create the new base class with only fields that apply to it. ctype = ContentType.objects.db_manager( using=(obj._state.db or DEFAULT_DB_ALIAS) ).get_for_model(self.model) nobj = self.model(**kwargs, polymorphic_ctype=ctype) nobj.save_base(raw=True, using=obj._state.db or DEFAULT_DB_ALIAS, force_insert=True) # force update the content type, but first we need to # retrieve a clean copy from the db to fill in the null # fields otherwise they would be overwritten. if isinstance(obj, PolymorphicModel): parent = obj.__class__.objects.using(obj._state.db or DEFAULT_DB_ALIAS).get( pk=obj.pk ) parent.polymorphic_ctype = ctype parent.save() nobj.refresh_from_db() # cast to cls return nobj django-polymorphic-4.10.2/src/polymorphic/models.py000066400000000000000000000236141513173623500224100ustar00rootroot00000000000000""" Seamless Polymorphic Inheritance for Django Models """ import warnings from django.contrib.contenttypes.models import ContentType from django.db import models, transaction from django.db.utils import DEFAULT_DB_ALIAS from django.utils.functional import classproperty from .base import PolymorphicModelBase from .managers import PolymorphicManager from .query_translate import translate_polymorphic_Q_object from .utils import get_base_polymorphic_model, lazy_ctype ################################################################################### # PolymorphicModel class PolymorphicTypeUndefined(LookupError): pass class PolymorphicTypeInvalid(RuntimeError): pass class PolymorphicModel(models.Model, metaclass=PolymorphicModelBase): """ Abstract base class that provides polymorphic behaviour for any model directly or indirectly derived from it. PolymorphicModel declares one field for internal use (:attr:`polymorphic_ctype`) and provides a polymorphic manager as the default manager (and as 'objects'). """ # for PolymorphicModelBase, so it can tell which models are polymorphic and which are not (duck typing) polymorphic_model_marker = True # for PolymorphicQuery, True => an overloaded __repr__ with nicer multi-line output is used by PolymorphicQuery polymorphic_query_multiline_output = False # avoid ContentType related field accessor clash (an error emitted by model validation) #: The model field that stores the :class:`~django.contrib.contenttypes.models.ContentType` reference to the actual class. polymorphic_ctype = models.ForeignKey( ContentType, null=True, editable=False, on_delete=models.CASCADE, related_name="polymorphic_%(app_label)s.%(class)s_set+", ) # some applications want to know the name of the fields that are added to its models polymorphic_internal_model_fields = ["polymorphic_ctype"] objects = PolymorphicManager() class Meta: abstract = True @classproperty def polymorphic_primary_key_name(cls): """ The name of the root primary key field of this polymorphic inheritance chain. """ warnings.warn( "polymorphic_primary_key_name is deprecated and will be removed in " "version 5.0, use get_base_polymorphic_model(Model)._meta.pk.attname " "instead.", DeprecationWarning, stacklevel=2, ) return get_base_polymorphic_model(cls, allow_abstract=True)._meta.pk.attname @classmethod def translate_polymorphic_Q_object(cls, q): return translate_polymorphic_Q_object(cls, q) def pre_save_polymorphic(self, using=DEFAULT_DB_ALIAS): """ Make sure the ``polymorphic_ctype`` value is correctly set on this model. This method automatically updates the polymorphic_ctype when: - The object is being saved for the first time - The object is being saved to a different database than it was loaded from This ensures cross-database saves work correctly without ForeignKeyViolation. """ # This function may be called manually in special use-cases. When the object # is saved for the first time, we store its real class in polymorphic_ctype. # When the object later is retrieved by PolymorphicQuerySet, it uses this # field to figure out the real class of this object # (used by PolymorphicQuerySet._get_real_instances) # Update polymorphic_ctype if: # 1. It's not set yet (new object), OR # 2. The database has changed (cross-database save) needs_update = not self.polymorphic_ctype_id or ( self._state.db and self._state.db != using ) if needs_update: # Set polymorphic_ctype_id directly to avoid database router issues # when saving across databases ctype = ContentType.objects.db_manager(using).get_for_model( self, for_concrete_model=False ) self.polymorphic_ctype_id = ctype.pk def save(self, *args, **kwargs): """Calls :meth:`pre_save_polymorphic` and saves the model.""" # Determine the database to use: # 1. Explicit 'using' parameter takes precedence # 2. Otherwise use self._state.db (the database the object was loaded from) # 3. Fall back to DEFAULT_DB_ALIAS # This ensures database routers are respected when no explicit database is specified using = kwargs.get("using") if using is None: using = self._state.db or DEFAULT_DB_ALIAS self.pre_save_polymorphic(using=using) return super().save(*args, **kwargs) save.alters_data = True def get_real_instance_class(self): """ Return the actual model type of the object. If a non-polymorphic manager (like base_objects) has been used to retrieve objects, then the real class/type of these objects may be determined using this method. """ if self.polymorphic_ctype_id is None: raise PolymorphicTypeUndefined( f"The model {self.__class__.__name__}#{self.pk} does not have a `polymorphic_ctype_id` value defined.\n" f"If you created models outside polymorphic, e.g. through an import or migration, " f"make sure the `polymorphic_ctype_id` field points to the ContentType ID of the model subclass." ) # the following line would be the easiest way to do this, but it produces sql queries # return self.polymorphic_ctype.model_class() # so we use the following version, which uses the ContentType manager cache. # Note that model_class() can return None for stale content types; # when the content type record still exists but no longer refers to an existing model. model = ( ContentType.objects.db_manager(self._state.db) .get_for_id(self.polymorphic_ctype_id) .model_class() ) # Protect against bad imports (dumpdata without --natural) or other # issues missing with the ContentType models. if ( model is not None and not issubclass(model, self.__class__) and ( self.__class__._meta.proxy_for_model is None or not issubclass(model, self.__class__._meta.proxy_for_model) ) ): raise PolymorphicTypeInvalid( f"ContentType {self.polymorphic_ctype_id} for {model} #{self.pk} does " "not point to a subclass!" ) return model def get_real_concrete_instance_class_id(self): model_class = self.get_real_instance_class() if model_class is None: return None return ( ContentType.objects.db_manager(self._state.db) .get_for_model(model_class, for_concrete_model=True) .pk ) def get_real_concrete_instance_class(self): model_class = self.get_real_instance_class() if model_class is None: return None return ( ContentType.objects.db_manager(self._state.db) .get_for_model(model_class, for_concrete_model=True) .model_class() ) def get_real_instance(self): """ Upcast an object to it's actual type. If a non-polymorphic manager (like base_objects) has been used to retrieve objects, then the complete object with it's real class/type and all fields may be retrieved with this method. If the model of the object's actual type does not exist (i.e. its ContentType is stale), this method raises a :class:`~polymorphic.models.PolymorphicTypeInvalid` exception. .. note:: Each method call executes one db query (if necessary). Use the :meth:`~polymorphic.managers.PolymorphicQuerySet.get_real_instances` to upcast a complete list in a single efficient query. """ real_model = self.get_real_instance_class() if real_model is self.__class__: return self if real_model is None: raise PolymorphicTypeInvalid( f"ContentType {self.polymorphic_ctype_id} for {self.__class__} " f"#{self.pk} does not have a corresponding model!" ) return self.__class__.objects.db_manager(self._state.db).get(pk=self.pk) def delete(self, using=None, keep_parents=False): """ Behaves the same as Django's default :meth:`~django.db.models.Model.delete()`, but with support for upcasting when ``keep_parents`` is True. When keeping parents (upcasting the row) the ``polymorphic_ctype`` fields of the parent rows are updated accordingly in a transaction with the child row deletion. """ # if we are keeping parents, we must first determine which polymorphic_ctypes we # need to update parent_updates = ( [ (parent_model, getattr(self, parent_field.get_attname())) for parent_model, parent_field in self._meta.parents.items() if issubclass(parent_model, PolymorphicModel) ] if keep_parents else [] ) if parent_updates: with transaction.atomic(using=using): # If keeping the parents (upcasting) we need to update the relevant # content types for all parent inheritance paths. ret = super().delete(using=using, keep_parents=keep_parents) for parent_model, pk in parent_updates: parent_model.objects.db_manager(using=using).non_polymorphic().filter( pk=pk ).update(polymorphic_ctype=lazy_ctype(parent_model, using=using)) return ret return super().delete(using=using, keep_parents=keep_parents) delete.alters_data = True django-polymorphic-4.10.2/src/polymorphic/query.py000066400000000000000000000640321513173623500222710ustar00rootroot00000000000000""" QuerySet for PolymorphicModel """ import copy import heapq from collections import defaultdict from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist from django.db import connections, models from django.db.models import FilteredRelation from django.db.models.query import ModelIterable, Q, QuerySet from .query_translate import ( translate_polymorphic_field_path, translate_polymorphic_filter_definitions_in_args, translate_polymorphic_filter_definitions_in_kwargs, translate_polymorphic_Q_object, ) from .utils import concrete_descendants, route_to_ancestor Polymorphic_QuerySet_objects_per_request = 2000 """ The maximum number of objects requested per db-request by the polymorphic queryset.iterator() implementation """ class _Inconsistent: """ A marker class indicating that there is a mismatch between the content type and the actual class of an object retrieved from the database. """ pass class PolymorphicModelIterable(ModelIterable): """ ModelIterable for PolymorphicModel Yields real instances if qs.polymorphic_disabled is False, otherwise acts like a regular ModelIterable. """ def __iter__(self): base_iter = super().__iter__() if self.queryset.polymorphic_disabled: return base_iter return self._polymorphic_iterator(base_iter) def _polymorphic_iterator(self, base_iter): """ Here we do the same as:: real_results = queryset._get_real_instances(list(base_iter)) for o in real_results: yield o but it requests the objects in chunks from the database, with QuerySet.iterator(chunk_size) per chunk """ # some databases have a limit on the number of query parameters, we must # respect this for generating get_real_instances queries because those # queries do a large WHERE IN clause with primary keys max_chunk = connections[self.queryset.db].features.max_query_params sql_chunk = self.chunk_size if self.chunked_fetch else None if max_chunk: sql_chunk = ( max_chunk if not self.chunked_fetch # chunk_size was not provided else min(max_chunk, self.chunk_size or max_chunk) ) sql_chunk = sql_chunk or Polymorphic_QuerySet_objects_per_request while True: base_result_objects = [] reached_end = False # Fetch in chunks for _ in range(sql_chunk): try: o = next(base_iter) base_result_objects.append(o) except StopIteration: reached_end = True break yield from self.queryset._get_real_instances(base_result_objects) if reached_end: return def transmogrify(cls, obj): """ Upcast a class to a different type without asking questions. """ if "__init__" not in obj.__class__.__dict__: # Just assign __class__ to a different value. new = obj new.__class__ = cls else: # Run constructor, reassign values new = cls() for k, v in obj.__dict__.items(): new.__dict__[k] = v return new ################################################################################### # PolymorphicQuerySet class PolymorphicQuerySet(QuerySet): """ QuerySet for PolymorphicModel Contains the core functionality for PolymorphicModel Usually not explicitly needed, except if a custom queryset class is to be used. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._iterable_class = PolymorphicModelIterable self.polymorphic_disabled = False # A parallel structure to django.db.models.query.Query.deferred_loading, # which we maintain with the untranslated field names passed to # .defer() and .only() in order to be able to retranslate them when # retrieving the real instance (so that the deferred fields apply # to that queryset as well). self.polymorphic_deferred_loading = (set(), True) def _clone(self, *args, **kwargs): # Django's _clone only copies its own variables, so we need to copy ours here new = super()._clone(*args, **kwargs) new.polymorphic_disabled = self.polymorphic_disabled new.polymorphic_deferred_loading = ( copy.copy(self.polymorphic_deferred_loading[0]), self.polymorphic_deferred_loading[1], ) return new def as_manager(cls): """ Override base :meth:`~django.db.models.query.QuerySet.as_manager` to return a manager extended from :class:`polymorphic.managers.PolymorphicManager`. """ from .managers import PolymorphicManager manager = PolymorphicManager.from_queryset(cls)() manager._built_with_as_manager = True return manager as_manager.queryset_only = True as_manager = classmethod(as_manager) def bulk_create(self, objs, batch_size=None, ignore_conflicts=False): objs = list(objs) for obj in objs: obj.pre_save_polymorphic() return super().bulk_create(objs, batch_size, ignore_conflicts=ignore_conflicts) def non_polymorphic(self): """switch off polymorphic behaviour for this query. When the queryset is evaluated, only objects of the type of the base class used for this query are returned.""" qs = self._clone() qs.polymorphic_disabled = True if issubclass(qs._iterable_class, PolymorphicModelIterable): qs._iterable_class = ModelIterable return qs def instance_of(self, *args): """Filter the queryset to only include the classes in args (and their subclasses).""" # Implementation in _translate_polymorphic_filter_defnition. return self.filter(instance_of=args) def not_instance_of(self, *args): """Filter the queryset to exclude the classes in args (and their subclasses).""" # Implementation in _translate_polymorphic_filter_defnition.""" return self.filter(not_instance_of=args) def _filter_or_exclude(self, negate, args, kwargs): # We override this internal Django function as it is used for all filter member functions. q_objects = translate_polymorphic_filter_definitions_in_args( queryset_model=self.model, args=args, using=self.db ) # filter_field='data' additional_args = translate_polymorphic_filter_definitions_in_kwargs( queryset_model=self.model, kwargs=kwargs, using=self.db ) args = list(q_objects) + additional_args return super()._filter_or_exclude(negate=negate, args=args, kwargs=kwargs) def order_by(self, *field_names): """translate the field paths in the args, then call vanilla order_by.""" field_names = [ translate_polymorphic_field_path(self.model, a) if isinstance(a, str) else a # allow expressions to pass unchanged for a in field_names ] return super().order_by(*field_names) def defer(self, *fields): """ Translate the field paths in the args, then call vanilla defer. Also retain a copy of the original fields passed, which we'll need when we're retrieving the real instance (since we'll need to translate them again, as the model will have changed). """ new_fields = [translate_polymorphic_field_path(self.model, a) for a in fields] clone = super().defer(*new_fields) clone._polymorphic_add_deferred_loading(fields) return clone def only(self, *fields): """ Translate the field paths in the args, then call vanilla only. Also retain a copy of the original fields passed, which we'll need when we're retrieving the real instance (since we'll need to translate them again, as the model will have changed). """ new_fields = {translate_polymorphic_field_path(self.model, a) for a in fields} new_fields.add("polymorphic_ctype_id") clone = super().only(*new_fields) clone._polymorphic_add_immediate_loading(fields) return clone def _polymorphic_add_deferred_loading(self, field_names): """ Follows the logic of django.db.models.query.Query.add_deferred_loading(), but for the non-translated field names that were passed to self.defer(). """ existing, defer = self.polymorphic_deferred_loading if defer: # Add to existing deferred names. self.polymorphic_deferred_loading = existing.union(field_names), True else: # Remove names from the set of any existing "immediate load" names. self.polymorphic_deferred_loading = existing.difference(field_names), False def _polymorphic_add_immediate_loading(self, field_names): """ Follows the logic of django.db.models.query.Query.add_immediate_loading(), but for the non-translated field names that were passed to self.only() """ existing, defer = self.polymorphic_deferred_loading field_names = set(field_names) if "pk" in field_names: field_names.remove("pk") field_names.add(self.model._meta.pk.name) if defer: # Remove any existing deferred names from the current set before # setting the new names. self.polymorphic_deferred_loading = field_names.difference(existing), False else: # Replace any existing "immediate load" field names. self.polymorphic_deferred_loading = field_names, False def _process_aggregate_args(self, args, kwargs): """for aggregate and annotate kwargs: allow ModelX___field syntax for kwargs, forbid it for args. Modifies kwargs if needed (these are Aggregate objects, we translate the lookup member variable) """ ___lookup_assert_msg = "PolymorphicModel: annotate()/aggregate(): ___ model lookup supported for keyword arguments only" def patch_lookup(a): # The field on which the aggregate operates is # stored inside a complex query expression. if isinstance(a, Q): # modify in place - this should be fixed if we want immutable # aggregate/annotation expressions a.children = translate_polymorphic_Q_object(self.model, a).children elif isinstance(a, FilteredRelation): patch_lookup(a.condition) elif isinstance(a, models.F): a.name = translate_polymorphic_field_path(self.model, a.name) elif hasattr(a, "get_source_expressions"): for source_expression in a.get_source_expressions(): if source_expression is not None: patch_lookup(source_expression) else: a.name = translate_polymorphic_field_path(self.model, a.name) def test___lookup(a): """*args might be complex expressions too in django 1.8 so the testing for a '___' is rather complex on this one""" if isinstance(a, Q): def tree_node_test___lookup(my_model, node): "process all children of this Q node" for i in range(len(node.children)): child = node.children[i] if type(child) is tuple: # this Q object child is a tuple => a kwarg like Q( instance_of=ModelB ) assert "___" not in child[0], ___lookup_assert_msg else: # this Q object child is another Q object, recursively process this as well tree_node_test___lookup(my_model, child) tree_node_test___lookup(self.model, a) elif hasattr(a, "get_source_expressions"): for source_expression in a.get_source_expressions(): if source_expression is not None: test___lookup(source_expression) else: assert "___" not in a.name, ___lookup_assert_msg for a in args: test___lookup(a) for a in kwargs.values(): patch_lookup(a) def annotate(self, *args, **kwargs): """translate the polymorphic field paths in the kwargs, then call vanilla annotate. _get_real_instances will do the rest of the job after executing the query.""" self._process_aggregate_args(args, kwargs) return super().annotate(*args, **kwargs) def aggregate(self, *args, **kwargs): """translate the polymorphic field paths in the kwargs, then call vanilla aggregate. We need no polymorphic object retrieval for aggregate => switch it off.""" self._process_aggregate_args(args, kwargs) qs = self.non_polymorphic() return super(PolymorphicQuerySet, qs).aggregate(*args, **kwargs) # Starting with Django 1.9, the copy returned by 'qs.values(...)' has the # same class as 'qs', so our polymorphic modifications would apply. # We want to leave values queries untouched, so we set 'polymorphic_disabled'. def _values(self, *args, **kwargs): clone = super()._values(*args, **kwargs) clone.polymorphic_disabled = True return clone # Since django_polymorphic 'V1.0 beta2', extra() always returns polymorphic results. # The resulting objects are required to have a unique primary key within the result set # (otherwise an error is thrown). # The "polymorphic" keyword argument is not supported anymore. # def extra(self, *args, **kwargs): def _get_real_instances(self, base_result_objects): """ Polymorphic object loader Does the same as: return [ o.get_real_instance() for o in base_result_objects ] but more efficiently. The list base_result_objects contains the objects from the executed base class query. The class of all of them is self.model (our base model). Some, many or all of these objects were not created and stored as class self.model, but as a class derived from self.model. We want to re-fetch these objects from the db as their original class so we can return them just as they were created/saved. We identify these objects by looking at o.polymorphic_ctype, which specifies the real class of these objects (the class at the time they were saved). First, we sort the result objects in base_result_objects for their subclass (from o.polymorphic_ctype), and then we execute one db query per subclass of objects. Here, we handle any annotations from annotate(). Finally we re-sort the resulting objects into the correct order and return them as a list. """ resultlist = [] # polymorphic list of result-objects # dict contains one entry per unique model type occurring in result, # in the format idlist_per_model[modelclass]=[list-of-object-ids] idlist_per_model = defaultdict(list) indexlist_per_model = defaultdict(list) # priority queue holding the query order of concrete classes # we need this so we can retry fetching objects further up the hierarchy with # stale content types - this can happen in some legitimate cases (deletion) # we build up and pop classes off this queue to ensure we process in tree # traversal order (leaves first) - this allows us to retry fetching as parent # classes if child class retrieval fails classes_to_query = [] # use the pk attribute for the base model type used in the query to identify # objects pk_name = self.model._meta.pk.attname # - sort base_result_object ids into idlist_per_model lists, depending on their real class; # - store objects that already have the correct class into "results" content_type_manager = ContentType.objects.db_manager(self.db) self_model_class_id = content_type_manager.get_for_model( self.model, for_concrete_model=False ).pk self_concrete_model_class_id = content_type_manager.get_for_model( self.model, for_concrete_model=True ).pk class_priorities = { mdl: idx + 1 for idx, mdl in enumerate((*reversed(concrete_descendants(self.model)), self.model)) } for i, base_object in enumerate(base_result_objects): if base_object.polymorphic_ctype_id == self_model_class_id: # Real class is exactly the same as base class, go straight to results resultlist.append(base_object) else: real_concrete_class = base_object.get_real_instance_class() real_concrete_class_id = base_object.get_real_concrete_instance_class_id() if real_concrete_class_id is None: # Dealing with a stale content type continue elif real_concrete_class_id == self_concrete_model_class_id: # Real and base classes share the same concrete ancestor, # upcast it and put it in the results resultlist.append(transmogrify(real_concrete_class, base_object)) else: # This model has a concrete derived class, track it for bulk retrieval. real_concrete_class = content_type_manager.get_for_id( real_concrete_class_id ).model_class() if real_concrete_class not in idlist_per_model: # Maintain a priority queue to process the model classes # in order of their occurrence in the inheritance tree - leafs # first heapq.heappush( classes_to_query, (class_priorities.get(real_concrete_class, 0), real_concrete_class), ) idlist_per_model[real_concrete_class].append(getattr(base_object, pk_name)) indexlist_per_model[real_concrete_class].append((i, len(resultlist))) resultlist.append(None) # For each model in "idlist_per_model" request its objects (the real model) # from the db and store them in results[]. # Then we copy the annotate fields from the base objects to the real objects. # Then we copy the extra() select fields from the base objects to the real objects. # TODO: defer(), only(): support for these would be around here while classes_to_query: _, real_concrete_class = heapq.heappop(classes_to_query) idlist = idlist_per_model.pop(real_concrete_class) indices = indexlist_per_model.pop(real_concrete_class) real_objects = real_concrete_class._base_objects.db_manager(self.db).filter( **{(f"{pk_name}__in"): idlist} ) # copy select related configuration to new qs real_objects.query.select_related = self.query.select_related # Copy deferred fields configuration to the new queryset deferred_loading_fields = [] existing_fields = self.polymorphic_deferred_loading[0] for field in existing_fields: try: translated_field_name = translate_polymorphic_field_path( real_concrete_class, field ) except AssertionError: if "___" in field: # The originally passed argument to .defer() or .only() # was in the form Model2B___field2, where Model2B is # now a superclass of real_concrete_class. Thus it's # sufficient to just use the field name. translated_field_name = field.rpartition("___")[-1] # Check if the field does exist. # Ignore deferred fields that don't exist in this subclass type. try: real_concrete_class._meta.get_field(translated_field_name) except FieldDoesNotExist: continue else: raise deferred_loading_fields.append(translated_field_name) real_objects.query.deferred_loading = ( set(deferred_loading_fields), self.query.deferred_loading[1], ) real_objects_dict = { getattr(real_object, pk_name): real_object for real_object in real_objects } for base_idx, result_idx in indices: base_object = base_result_objects[base_idx] o_pk = getattr(base_object, pk_name) real_object = real_objects_dict.get(o_pk) if real_object is None: # Our content type is pointing to a row that does not exist anymore # We try to find the next best available parent row inheritance_path = route_to_ancestor(real_concrete_class, self.model) if not inheritance_path or inheritance_path[0].model is self.model: resultlist[result_idx] = base_object else: next_best_class = inheritance_path[0].model if next_best_class not in idlist_per_model: # add this class to the priority try queue heapq.heappush( classes_to_query, (class_priorities.get(next_best_class, 0), next_best_class), ) idlist_per_model[next_best_class].append(o_pk) indexlist_per_model[next_best_class].append((base_idx, result_idx)) resultlist[result_idx] = _Inconsistent continue # need shallow copy to avoid duplication in caches (see PR #353) real_object = copy.copy(real_object) real_class = ( real_concrete_class if resultlist[result_idx] is _Inconsistent else real_object.get_real_instance_class() ) # If the real class is a proxy, upcast it if real_class != real_concrete_class: real_object = transmogrify(real_class, real_object) if self.query.annotations: # New in Django 3.2+: annotation_select contains only the selected annotations # (excluding aliases). Fallback for older Django versions if needed. annotation_select = getattr( self.query, "annotation_select", self.query.annotations ) for anno_field_name in annotation_select.keys(): if hasattr(base_object, anno_field_name): attr = getattr(base_object, anno_field_name) setattr(real_object, anno_field_name, attr) if self.query.extra_select: for select_field_name in self.query.extra_select.keys(): attr = getattr(base_object, select_field_name) setattr(real_object, select_field_name, attr) resultlist[result_idx] = real_object resultlist = [i for i in resultlist if i and i is not _Inconsistent] # set polymorphic_annotate_names in all objects (currently just used for debugging/printing) if self.query.annotations: # get annotate field list annotation_select = getattr(self.query, "annotation_select", self.query.annotations) annotate_names = list(annotation_select.keys()) for real_object in resultlist: real_object.polymorphic_annotate_names = annotate_names # set polymorphic_extra_select_names in all objects (currently just used for debugging/printing) if self.query.extra_select: # get extra select field list extra_select_names = list(self.query.extra_select.keys()) for real_object in resultlist: real_object.polymorphic_extra_select_names = extra_select_names return resultlist def __repr__(self, *args, **kwargs): if self.model.polymorphic_query_multiline_output: result = ",\n ".join(repr(o) for o in self.all()) return f"[ {result} ]" else: return super().__repr__(*args, **kwargs) class _p_list_class(list): def __repr__(self, *args, **kwargs): result = ",\n ".join(repr(o) for o in self) return f"[ {result} ]" def get_real_instances(self, base_result_objects=None): """ Cast a list of objects to their actual classes. This does roughly the same as:: return [ o.get_real_instance() for o in base_result_objects ] but more efficiently. :rtype: PolymorphicQuerySet """ "same as _get_real_instances, but make sure that __repr__ for ShowField... creates correct output" if base_result_objects is None: base_result_objects = self olist = self._get_real_instances(base_result_objects) if not self.model.polymorphic_query_multiline_output: return olist clist = PolymorphicQuerySet._p_list_class(olist) return clist def delete(self): """ Deletion will be done non-polymorphically because Django's multi-table deletion mechanism is already walking the class hierarchy and producing a correct deletion graph. Introducing polymorphic querysets into the deletion process disrupts the model hierarchy/relationship traversal. """ return QuerySet.delete(self.non_polymorphic()) django-polymorphic-4.10.2/src/polymorphic/query_translate.py000066400000000000000000000237161513173623500243520ustar00rootroot00000000000000""" PolymorphicQuerySet support functions """ import copy from functools import reduce from operator import or_ from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist, FieldError from django.db import models from django.db.models import Q, Subquery from django.db.models.fields.related import ForeignObjectRel, RelatedField from django.db.utils import DEFAULT_DB_ALIAS from .utils import _lazy_ctype, _map_queryname_to_class, concrete_descendants # These functions implement the additional filter- and Q-object functionality. # They form a kind of small framework for easily adding more # functionality to filters and Q objects. # Probably a more general queryset enhancement class could be made out of them. ################################################################################### # PolymorphicQuerySet support functions def translate_polymorphic_filter_definitions_in_kwargs( queryset_model, kwargs, using=DEFAULT_DB_ALIAS ): """ Translate the keyword argument list for PolymorphicQuerySet.filter() Any kwargs with special polymorphic functionality are replaced in the kwargs dict with their vanilla django equivalents. For some kwargs a direct replacement is not possible, as a Q object is needed instead to implement the required functionality. In these cases the kwarg is deleted from the kwargs dict and a Q object is added to the return list. Modifies: kwargs dict Returns: a list of non-keyword-arguments (Q objects) to be added to the filter() query. """ additional_args = [] for field_path, val in kwargs.copy().items(): # `copy` so we're not mutating the dict new_expr = _translate_polymorphic_filter_definition( queryset_model, field_path, val, using=using ) if isinstance(new_expr, tuple): # replace kwargs element del kwargs[field_path] kwargs[new_expr[0]] = new_expr[1] elif isinstance(new_expr, models.Q): del kwargs[field_path] additional_args.append(new_expr) return additional_args def translate_polymorphic_Q_object(queryset_model, potential_q_object, using=DEFAULT_DB_ALIAS): def tree_node_correct_field_specs(my_model, node): "process all children of this Q node" cpy = copy.copy(node) cpy.children = [] for child in node.children: if isinstance(child, (tuple, list)): # this Q object child is a tuple => a kwarg like Q( instance_of=ModelB ) key, val = child new_expr = _translate_polymorphic_filter_definition( my_model, key, val, using=using ) cpy.children.append(new_expr or child) elif isinstance(child, models.Q): # this Q object child is another Q object, recursively process cpy.children.append(tree_node_correct_field_specs(my_model, child)) else: cpy.children.append(child) return cpy if isinstance(potential_q_object, models.Q): return tree_node_correct_field_specs(queryset_model, potential_q_object) return potential_q_object def translate_polymorphic_filter_definitions_in_args(queryset_model, args, using=DEFAULT_DB_ALIAS): """ Translate the non-keyword argument list for PolymorphicQuerySet.filter() In the args list, we return all kwargs to Q-objects that contain special polymorphic functionality with their vanilla django equivalents. We traverse the Q object tree for this (which is simple). Returns: modified Q objects """ return [translate_polymorphic_Q_object(queryset_model, q, using=using) for q in args] def _translate_polymorphic_filter_definition( queryset_model, field_path, field_val, using=DEFAULT_DB_ALIAS ): """ Translate a keyword argument (field_path=field_val), as used for PolymorphicQuerySet.filter()-like functions (and Q objects). A kwarg with special polymorphic functionality is translated into its vanilla django equivalent, which is returned, either as tuple (field_path, field_val) or as Q object. Returns: kwarg tuple or Q object or None (if no change is required) """ # handle instance_of expressions or alternatively, # if this is a normal Django filter expression, return None if field_path == "instance_of": return create_instanceof_q(field_val, using=using) elif field_path == "not_instance_of": return create_instanceof_q(field_val, not_instance_of=True, using=using) elif "___" not in field_path: return None # no change # filter expression contains '___' (i.e. filter for polymorphic field) # => get the model class specified in the filter expression newpath = translate_polymorphic_field_path(queryset_model, field_path) return (newpath, field_val) def translate_polymorphic_field_path(queryset_model, field_path): """ Translate a field path from a keyword argument, as used for PolymorphicQuerySet.filter()-like functions (and Q objects). Supports leading '-' (for order_by args). E.g.: if queryset_model is ModelA, then "ModelC___field3" is translated into modela__modelb__modelc__field3. Returns: translated path (unchanged, if no translation needed) """ classname, sep, pure_field_path = field_path.partition("___") if not sep or not classname: return field_path negated = False if classname[0] == "-": negated = True classname = classname.lstrip("-") if "__" in classname: # the user has app label prepended to class name via __ => use Django's get_model function appname, sep, classname = classname.partition("__") try: model = apps.get_model(appname, classname) except LookupError as le: raise FieldError(f"Model {appname}.{classname} does not exist") from le if not issubclass(model, queryset_model): raise FieldError( f"{model._meta.label} is not derived from {queryset_model._meta.label}" ) else: # the user has only given us the class name via ___ # => select the model from the sub models of the queryset base model # Test whether it's actually a regular relation__ _fieldname (the field starting with an _) # so no tripple ClassName___field was intended. try: # This also retreives M2M relations now (including reverse foreign key relations) field = queryset_model._meta.get_field(classname) if isinstance(field, (RelatedField, ForeignObjectRel)): # Can also test whether the field exists in the related object to avoid ambiguity between # class names and field names, but that never happens when your class names are in CamelCase. return field_path # No exception raised, field does exist. except FieldDoesNotExist: pass model = _map_queryname_to_class(queryset_model, classname) basepath = _create_base_path(queryset_model, model) if negated: newpath = "-" else: newpath = "" newpath += basepath if basepath: newpath += "__" newpath += pure_field_path return newpath def _create_base_path(baseclass, myclass): # create new field path for expressions, e.g. for baseclass=ModelA, myclass=ModelC # 'modelb__modelc" is returned for b in myclass.__bases__: if b == baseclass: return _get_query_related_name(myclass) path = _create_base_path(baseclass, b) if path: if b._meta.abstract or b._meta.proxy: return _get_query_related_name(myclass) else: return f"{path}__{_get_query_related_name(myclass)}" return "" def _get_query_related_name(myclass): for f in myclass._meta.local_fields: if isinstance(f, models.OneToOneField) and f.remote_field.parent_link: return f.related_query_name() # Fallback to undetected name, # this happens on proxy models (e.g. SubclassSelectorProxyModel) return myclass.__name__.lower() def create_instanceof_q(modellist, not_instance_of=False, using=DEFAULT_DB_ALIAS): """ Helper function for instance_of / not_instance_of Creates and returns a Q object that filters for the models in modellist, including all subclasses of these models (as we want to do the same as pythons isinstance() ). . We recursively collect all __subclasses__(), create a Q filter for each, and or-combine these Q objects. This could be done much more efficiently however (regarding the resulting sql), should an optimization be needed. """ if not modellist: return None if not isinstance(modellist, (list, tuple)): from .models import PolymorphicModel if issubclass(modellist, PolymorphicModel): modellist = [modellist] else: raise TypeError( "PolymorphicModel: instance_of expects a list of (polymorphic) " "models or a single (polymorphic) model" ) lazy_cts, ct_ids = _get_mro_content_type_ids(modellist, using) q = Q() if lazy_cts: q |= Q( polymorphic_ctype__in=Subquery( # no need to pass using here ContentType.objects.filter(reduce(or_, lazy_cts)).values("pk") ) ) if ct_ids: q |= Q(polymorphic_ctype__in=ct_ids) if not_instance_of: q = ~q return q def _get_mro_content_type_ids(models, using): lazy = [] ids = [] for model in models: cid = _lazy_ctype(model, using=using) ids.append(cid.pk) if isinstance(cid, ContentType) else lazy.append(cid) for descendent in concrete_descendants(model, include_proxy=True): cid = _lazy_ctype(descendent, using=using) ids.append(cid.pk) if isinstance(cid, ContentType) else lazy.append(cid) return lazy, ids django-polymorphic-4.10.2/src/polymorphic/related_descriptors.py000066400000000000000000000030041513173623500251550ustar00rootroot00000000000000from django.db.models.fields.related_descriptors import ( ForwardOneToOneDescriptor, ReverseOneToOneDescriptor, ) class NonPolymorphicForwardOneToOneDescriptor(ForwardOneToOneDescriptor): """ A custom descriptor for forward OneToOne relations to polymorphic models that returns non-polymorphic instances. This is used for the parent to child links in multi-table polymorphic models. """ def get_queryset(self, **hints): return ( ( getattr( self.field.remote_field.model, "_base_objects", # don't fail if we've been used on a non-poly model self.field.remote_field.model._base_manager, ) ) .db_manager(hints=hints) .all() ) class NonPolymorphicReverseOneToOneDescriptor(ReverseOneToOneDescriptor): """ A custom descriptor for reverse OneToOne relations to polymorphic models that returns non-polymorphic instances. This is used for the child to parent links in multi-table polymorphic models. """ def get_queryset(self, **hints): return ( ( getattr( self.related.related_model, "_base_objects", # don't fail if we've been used on a non-poly model self.related.related_model._base_manager, ) ) .db_manager(hints=hints) .all() ) django-polymorphic-4.10.2/src/polymorphic/showfields.py000066400000000000000000000136721513173623500232770ustar00rootroot00000000000000import re from django.db import models RE_DEFERRED = re.compile("_Deferred_.*") class ShowFieldBase: """base class for the ShowField... model mixins, does the work""" # cause nicer multiline PolymorphicQuery output polymorphic_query_multiline_output = True polymorphic_showfield_type = False polymorphic_showfield_content = False polymorphic_showfield_deferred = False # these may be overridden by the user polymorphic_showfield_max_line_width = None polymorphic_showfield_max_field_width = 20 polymorphic_showfield_old_format = False def __repr__(self): return self.__str__() def _showfields_get_content(self, field_name, field_type=type(None)): "helper for __unicode__" content = getattr(self, field_name) if self.polymorphic_showfield_old_format: out = ": " else: out = " " if issubclass(field_type, models.ForeignKey): if content is None: out += "None" else: out += content.__class__.__name__ elif issubclass(field_type, models.ManyToManyField): out += f"{content.count()}" elif isinstance(content, int): out += str(content) elif content is None: out += "None" else: txt = str(content) max_len = self.polymorphic_showfield_max_field_width if len(txt) > max_len: txt = f"{txt[: max_len - 2]}.." out += f'"{txt}"' return out def _showfields_add_regular_fields(self, parts): "helper for __unicode__" done_fields = set() for field in self._meta.fields + self._meta.many_to_many: if field.name in self.polymorphic_internal_model_fields or "_ptr" in field.name: continue if field.name in done_fields: continue # work around django diamond inheritance problem done_fields.add(field.name) out = field.name # if this is the standard primary key named "id", print it as we did with older versions of django_polymorphic if field.primary_key and field.name == "id" and type(field) is models.AutoField: out += f" {getattr(self, field.name)}" # otherwise, display it just like all other fields (with correct type, shortened content etc.) else: if self.polymorphic_showfield_type: out += f" ({type(field).__name__}" if field.primary_key: out += "/pk" out += ")" if self.polymorphic_showfield_content: out += self._showfields_get_content(field.name, type(field)) parts.append((False, out, ",")) def _showfields_add_dynamic_fields(self, field_list, title, parts): "helper for __unicode__" parts.append((True, f"- {title}", ":")) for field_name in field_list: out = field_name content = getattr(self, field_name) if self.polymorphic_showfield_type: out += f" ({type(content).__name__})" if self.polymorphic_showfield_content: out += self._showfields_get_content(field_name) parts.append((False, out, ",")) def __str__(self): # create list ("parts") containing one tuple for each title/field: # ( bool: new section , item-text , separator to use after item ) # start with model name parts = [(True, RE_DEFERRED.sub("", self.__class__.__name__), ":")] # add all regular fields self._showfields_add_regular_fields(parts) # add annotate fields if hasattr(self, "polymorphic_annotate_names"): self._showfields_add_dynamic_fields(self.polymorphic_annotate_names, "Ann", parts) # add extra() select fields if hasattr(self, "polymorphic_extra_select_names"): self._showfields_add_dynamic_fields( self.polymorphic_extra_select_names, "Extra", parts ) if self.polymorphic_showfield_deferred: fields = self.get_deferred_fields() if fields: fields_str = ",".join(sorted(fields)) parts.append((False, f"deferred[{fields_str}]", "")) # format result indent = len(self.__class__.__name__) + 5 indentstr = "".rjust(indent) out = "" xpos = 0 possible_line_break_pos = None for i in range(len(parts)): new_section, p, separator = parts[i] final = i == len(parts) - 1 if not final: next_new_section, _, _ = parts[i + 1] if ( self.polymorphic_showfield_max_line_width and xpos + len(p) > self.polymorphic_showfield_max_line_width and possible_line_break_pos is not None ): rest = out[possible_line_break_pos:] out = out[:possible_line_break_pos] out += f"\n{indentstr}{rest}" xpos = indent + len(rest) out += p xpos += len(p) if not final: if not next_new_section: out += separator xpos += len(separator) out += " " xpos += 1 if not new_section: possible_line_break_pos = len(out) return f"<{out}>" class ShowFieldType(ShowFieldBase): """model mixin that shows the object's class and it's field types""" polymorphic_showfield_type = True class ShowFieldContent(ShowFieldBase): """model mixin that shows the object's class, it's fields and field contents""" polymorphic_showfield_content = True class ShowFieldTypeAndContent(ShowFieldBase): """model mixin, like ShowFieldContent, but also show field types""" polymorphic_showfield_type = True polymorphic_showfield_content = True django-polymorphic-4.10.2/src/polymorphic/static/000077500000000000000000000000001513173623500220345ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/static/polymorphic/000077500000000000000000000000001513173623500244015ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/static/polymorphic/css/000077500000000000000000000000001513173623500251715ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/static/polymorphic/css/polymorphic_inlines.css000066400000000000000000000014001513173623500317640ustar00rootroot00000000000000.polymorphic-add-choice { position: relative; clear: left; } .polymorphic-add-choice a:focus { text-decoration: none; } .polymorphic-type-menu { position: absolute; top: 2.2em; left: 0.5em; border: 1px solid var(--border-color, #ccc); border-radius: 4px; padding: 2px; background-color: var(--body-bg, #fff); z-index: 1000; } .polymorphic-type-menu ul { padding: 2px; margin: 0; } .polymorphic-type-menu li { list-style: none inside none; padding: 4px 8px; } .inline-related.empty-form { /* needed for grapelli, which uses grp-empty-form */ display: none; } @media (prefers-color-scheme: dark) { .polymorphic-type-menu { border: 1px solid var(--border-color, #121212); background-color: var(--body-bg, #212121); } } django-polymorphic-4.10.2/src/polymorphic/static/polymorphic/js/000077500000000000000000000000001513173623500250155ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/static/polymorphic/js/polymorphic_inlines.js000066400000000000000000000376711513173623500314570ustar00rootroot00000000000000/*global DateTimeShortcuts, SelectFilter*/ // This is a slightly adapted version of Django's inlines.js // Forked for polymorphic by Diederik van der Boor /** * Django admin inlines * * Based on jQuery Formset 1.1 * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com) * @requires jQuery 1.2.6 or later * * Copyright (c) 2009, Stanislaus Madueke * All rights reserved. * * Spiced up with Code from Zain Memon's GSoC project 2009 * and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip. * * Licensed under the New BSD License * See: http://www.opensource.org/licenses/bsd-license.php */ (function($) { 'use strict'; $.fn.polymorphicFormset = function(opts) { var options = $.extend({}, $.fn.polymorphicFormset.defaults, opts); var $this = $(this); var $parent = $this.parent(); var updateElementIndex = function(el, prefix, ndx) { var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); var replacement = prefix + "-" + ndx; if ($(el).prop("for")) { $(el).prop("for", $(el).prop("for").replace(id_regex, replacement)); } if (el.id) { el.id = el.id.replace(id_regex, replacement); } if (el.name) { el.name = el.name.replace(id_regex, replacement); } }; var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off"); var nextIndex = parseInt(totalForms.val(), 10); var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off"); // only show the add button if we are allowed to add more items, // note that max_num = None translates to a blank string. var showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0; $this.each(function(i) { $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); }); if ($this.length && showAddButton) { var addContainer; var menuButton; var addButtons; if(options.childTypes == null) { throw Error("The polymorphic fieldset options.childTypes is not defined!"); } // For Polymorphic inlines, the add button opens a menu. var menu = ''; if ($this.prop("tagName") === "TR") { // If forms are laid out as table rows, insert the // "add" button in a new table row: var numCols = this.eq(-1).children().length; $parent.append('' + options.addText + "" + menu + ""); addContainer = $parent.find("tr:last > td"); menuButton = addContainer.children('a'); addButtons = addContainer.find("li a"); } else { // Otherwise, insert it immediately after the last form: $this.filter(":last").after('"); addContainer = $this.filter(":last").next(); menuButton = addContainer.children('a'); addButtons = addContainer.find("li a"); } menuButton.click(function(event) { event.preventDefault(); event.stopPropagation(); // for menu hide var $menu = $(event.target).next('.polymorphic-type-menu'); if(! $menu.is(':visible')) { var hideMenu = function() { $menu.slideUp(50); $(document).unbind('click', hideMenu); }; $(document).click(hideMenu); } $menu.slideToggle(50); }); addButtons.click(function(event) { event.preventDefault(); var polymorphicType = $(event.target).attr('data-type'); // Select polymorphic type. var template = $("#" + polymorphicType + "-empty"); var row = template.clone(true); row.removeClass(options.emptyCssClass) .addClass(options.formCssClass) .attr("id", options.prefix + "-" + nextIndex); if (row.is("tr")) { // If the forms are laid out in table rows, insert // the remove button into the last table cell: row.children(":last").append('"); } else if (row.is("ul") || row.is("ol")) { // If they're laid out as an ordered/unordered list, // insert an
  • after the last list item: row.append('
  • ' + options.deleteText + "
  • "); } else { // Otherwise, just insert the remove button as the // last child element of the form's container: row.children(":first").append('' + options.deleteText + ""); } let totForms = parseInt(totalForms.val(), 10); row.find("*").each(function() { updateElementIndex(this, options.prefix, totForms); }); row.find("h3 span.inline_label").each(function () { $(this).text($(this).text().replace(/##/g, `#${totForms+1}`)); }); // Insert the new form when it has been fully edited const firstTemplate = $("#" + options.childTypes[0].type + "-empty"); row.insertBefore($(firstTemplate)); // Update number of total forms $(totalForms).val(totForms+1); nextIndex += 1; // Hide add button in case we've hit the max, except we want to add infinitely if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) { addContainer.hide(); } // The delete button of each row triggers a bunch of other things row.find("a." + options.deleteCssClass).click(function(e1) { e1.preventDefault(); // Remove the parent form containing this button: row.remove(); nextIndex -= 1; // If a post-delete callback was provided, call it with the deleted form: if (options.removed) { options.removed(row); } document.dispatchEvent(new CustomEvent("formset:removed", { detail: { formsetName: options.prefix } })); // Update the TOTAL_FORMS form count. var forms = $("." + options.formCssClass); $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); // Show add button again once we drop below max if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) { addContainer.show(); } // Also, update names and ids for all remaining form controls // so they remain in sequence: var i, formCount; var updateElementCallback = function() { updateElementIndex(this, options.prefix, i); }; for (i = 0, formCount = forms.length; i < formCount; i++) { updateElementIndex($(forms).get(i), options.prefix, i); $(forms.get(i)).find("*").each(updateElementCallback); $(forms.get(i)).find("h3 span.inline_label").each(function () { $(this).text($(this).text().replace(/#\d+/g, `#${i+1}`)); }); } }); // If a post-add callback was supplied, call it with the added form: if (options.added) { options.added(row); } row.get(0).dispatchEvent(new CustomEvent("formset:added", { bubbles: true, detail: { formsetName: options.prefix } })); }); } return this; }; /* Setup plugin defaults */ $.fn.polymorphicFormset.defaults = { prefix: "form", // The form prefix for your django formset addText: "add another", // Text for the add link childTypes: null, // defined by the client. deleteText: "remove", // Text for the delete link addCssClass: "add-row", // CSS class applied to the add link deleteCssClass: "delete-row", // CSS class applied to the delete link emptyCssClass: "empty-row", // CSS class applied to the empty row formCssClass: "dynamic-form", // CSS class applied to each form in a formset added: null, // Function called each time a new form is added removed: null, // Function called each time a form is deleted addButton: null // Existing add button to use }; // Tabular inlines --------------------------------------------------------- $.fn.tabularPolymorphicFormset = function(options) { var $rows = $(this); var alternatingRows = function(row) { $($rows.selector).not(".add-row").removeClass("row1 row2") .filter(":even").addClass("row1").end() .filter(":odd").addClass("row2"); }; var reinitDateTimeShortCuts = function() { // Reinitialize the calendar and clock widgets by force if (typeof DateTimeShortcuts !== "undefined") { $(".datetimeshortcuts").remove(); DateTimeShortcuts.init(); } }; var updateSelectFilter = function() { // If any SelectFilter widgets are a part of the new form, // instantiate a new SelectFilter instance for it. if (typeof SelectFilter !== 'undefined') { $('.selectfilter').each(function(index, value) { var namearr = value.name.split('-'); SelectFilter.init(value.id, namearr[namearr.length - 1], false); }); $('.selectfilterstacked').each(function(index, value) { var namearr = value.name.split('-'); SelectFilter.init(value.id, namearr[namearr.length - 1], true); }); } }; var initPrepopulatedFields = function(row) { row.find('.prepopulated_field').each(function() { var field = $(this), input = field.find('input, select, textarea'), dependency_list = input.data('dependency_list') || [], dependencies = []; $.each(dependency_list, function(i, field_name) { dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id')); }); if (dependencies.length) { input.prepopulate(dependencies, input.attr('maxlength')); } }); }; $rows.polymorphicFormset({ prefix: options.prefix, addText: options.addText, childTypes: options.childTypes, formCssClass: "dynamic-" + options.prefix, deleteCssClass: "inline-deletelink", deleteText: options.deleteText, emptyCssClass: "empty-form", removed: alternatingRows, added: function(row) { initPrepopulatedFields(row); reinitDateTimeShortCuts(); updateSelectFilter(); alternatingRows(row); }, addButton: options.addButton }); return $rows; }; // Stacked inlines --------------------------------------------------------- $.fn.stackedPolymorphicFormset = function(options) { var $rows = $(this); var updateInlineLabel = function(row) { $($rows.selector).find(".inline_label").each(function(i) { var count = i + 1; $(this).html($(this).html().replace(/(#\d+)/g, "#" + count)); }); }; var reinitDateTimeShortCuts = function() { // Reinitialize the calendar and clock widgets by force, yuck. if (typeof DateTimeShortcuts !== "undefined") { $(".datetimeshortcuts").remove(); DateTimeShortcuts.init(); } }; var updateSelectFilter = function() { // If any SelectFilter widgets were added, instantiate a new instance. if (typeof SelectFilter !== "undefined") { $(".selectfilter").each(function(index, value) { var namearr = value.name.split('-'); SelectFilter.init(value.id, namearr[namearr.length - 1], false); }); $(".selectfilterstacked").each(function(index, value) { var namearr = value.name.split('-'); SelectFilter.init(value.id, namearr[namearr.length - 1], true); }); } }; var initPrepopulatedFields = function(row) { row.find('.prepopulated_field').each(function() { var field = $(this), input = field.find('input, select, textarea'), dependency_list = input.data('dependency_list') || [], dependencies = []; $.each(dependency_list, function(i, field_name) { dependencies.push('#' + row.find('.form-row .field-' + field_name).find('input, select, textarea').attr('id')); }); if (dependencies.length) { input.prepopulate(dependencies, input.attr('maxlength')); } }); }; $rows.polymorphicFormset({ prefix: options.prefix, addText: options.addText, childTypes: options.childTypes, formCssClass: "dynamic-" + options.prefix, deleteCssClass: "inline-deletelink", deleteText: options.deleteText, emptyCssClass: "empty-form", removed: updateInlineLabel, added: function(row) { initPrepopulatedFields(row); reinitDateTimeShortCuts(); updateSelectFilter(); updateInlineLabel(row); }, addButton: options.addButton }); return $rows; }; $(document).ready(function() { $(".js-inline-polymorphic-admin-formset").each(function() { var data = $(this).data(), inlineOptions = data.inlineFormset; switch(data.inlineType) { case "stacked": $(inlineOptions.name + "-group .inline-related").stackedPolymorphicFormset(inlineOptions.options); break; case "tabular": $(inlineOptions.name + "-group .tabular.inline-related tbody tr").tabularPolymorphicFormset(inlineOptions.options); break; } }); }); })(django.jQuery); django-polymorphic-4.10.2/src/polymorphic/templates/000077500000000000000000000000001513173623500225435ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/templates/admin/000077500000000000000000000000001513173623500236335ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/templates/admin/polymorphic/000077500000000000000000000000001513173623500262005ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/templates/admin/polymorphic/add_type_form.html000066400000000000000000000004431513173623500317030ustar00rootroot00000000000000{% extends "admin/change_form.html" %} {% if save_on_top %} {% block submit_buttons_top %} {% include 'admin/submit_line.html' with show_save=1 %} {% endblock %} {% endif %} {% block submit_buttons_bottom %} {% include 'admin/submit_line.html' with show_save=1 %} {% endblock %} django-polymorphic-4.10.2/src/polymorphic/templates/admin/polymorphic/change_form.html000066400000000000000000000002761513173623500313430ustar00rootroot00000000000000{% extends "admin/change_form.html" %} {% load polymorphic_admin_tags %} {% block breadcrumbs %} {% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %} {% endblock %} django-polymorphic-4.10.2/src/polymorphic/templates/admin/polymorphic/delete_confirmation.html000066400000000000000000000003061513173623500330770ustar00rootroot00000000000000{% extends "admin/delete_confirmation.html" %} {% load polymorphic_admin_tags %} {% block breadcrumbs %} {% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %} {% endblock %} django-polymorphic-4.10.2/src/polymorphic/templates/admin/polymorphic/edit_inline/000077500000000000000000000000001513173623500304635ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/templates/admin/polymorphic/edit_inline/stacked.html000066400000000000000000000044671513173623500330020ustar00rootroot00000000000000{% load i18n admin_urls static %}

    {{ inline_admin_formset.opts.verbose_name_plural|capfirst }}

    {{ inline_admin_formset.formset.management_form }} {{ inline_admin_formset.formset.non_form_errors }} {% for inline_admin_form in inline_admin_formset %} {% endfor %}
    django-polymorphic-4.10.2/src/polymorphic/templates/admin/polymorphic/object_history.html000066400000000000000000000003011513173623500321070ustar00rootroot00000000000000{% extends "admin/object_history.html" %} {% load polymorphic_admin_tags %} {% block breadcrumbs %} {% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %} {% endblock %} django-polymorphic-4.10.2/src/polymorphic/templatetags/000077500000000000000000000000001513173623500232375ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/templatetags/__init__.py000066400000000000000000000001631513173623500253500ustar00rootroot00000000000000""" We provide collections of tags that override or extend template tags for formsets and the admin interface. """ django-polymorphic-4.10.2/src/polymorphic/templatetags/polymorphic_admin_tags.py000066400000000000000000000042721513173623500303510ustar00rootroot00000000000000from django.template import Library, Node, TemplateSyntaxError register = Library() class BreadcrumbScope(Node): def __init__(self, base_opts, nodelist): self.base_opts = base_opts self.nodelist = nodelist # Note, takes advantage of Node.child_nodelists @classmethod def parse(cls, parser, token): bits = token.split_contents() if len(bits) == 2: (tagname, base_opts) = bits base_opts = parser.compile_filter(base_opts) nodelist = parser.parse(("endbreadcrumb_scope",)) parser.delete_first_token() return cls(base_opts=base_opts, nodelist=nodelist) else: raise TemplateSyntaxError(f"{token.contents[0]} tag expects 1 argument") def render(self, context): # app_label is really hard to overwrite in the standard Django ModelAdmin. # To insert it in the template, the entire render_change_form() and delete_view() have to copied and adjusted. # Instead, have an assignment tag that inserts that in the template. base_opts = self.base_opts.resolve(context) new_vars = {} if base_opts and not isinstance(base_opts, str): new_vars = { "app_label": base_opts.app_label, # What this is all about "opts": base_opts, } new_scope = context.push() new_scope.update(new_vars) html = self.nodelist.render(context) context.pop() return html @register.tag def breadcrumb_scope(parser, token): """ .. templatetag:: breadcrumb_scope Easily allow the breadcrumb to be generated in the admin change templates. The ``{% breadcrumb_scope ... %}`` tag makes sure the ``{{ opts }}`` and ``{{ app_label }}`` values are temporary based on the provided ``{{ base_opts }}``. This allows fixing the breadcrumb in admin templates: .. code-block:: html+django {% extends "admin/change_form.html" %} {% load polymorphic_admin_tags %} {% block breadcrumbs %} {% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %} {% endblock %} """ return BreadcrumbScope.parse(parser, token) django-polymorphic-4.10.2/src/polymorphic/templatetags/polymorphic_formset_tags.py000066400000000000000000000105611513173623500307360ustar00rootroot00000000000000""" .. versionadded:: 1.1 To render formsets in the frontend, the ``polymorphic_tags`` provides extra filters to implement HTML rendering of polymorphic formsets. The following filters are provided; * ``{{ formset|as_script_options }}`` render the ``data-options`` for a JavaScript formset library. * ``{{ formset|include_empty_form }}`` provide the placeholder form for an add button. * ``{{ form|as_form_type }}`` return the model name that the form instance uses. * ``{{ model|as_model_name }}`` performs the same, for a model class or instance. .. code-block:: html+django {% load i18n polymorphic_formset_tags %}
    {% block add_button %} {% if formset.show_add_button|default_if_none:'1' %} {% if formset.empty_forms %} {# django-polymorphic formset (e.g. PolymorphicInlineFormSetView) #}
    {% for model in formset.child_forms %} {% glyphicon 'plus' %} {{ model|as_verbose_name }} {% endfor %}
    {% else %} {% trans "Add" %} {% endif %} {% endif %} {% endblock %} {{ formset.management_form }} {% for form in formset|include_empty_form %} {% block formset_form_wrapper %}
    {{ form.non_field_errors }} {# Add the 'pk' field that is not mentioned in crispy #} {% for field in form.hidden_fields %} {{ field }} {% endfor %} {% block formset_form %} {% crispy form %} {% endblock %}
    {% endblock %} {% endfor %}
    """ import json from django.template import Library from django.utils.encoding import force_str from django.utils.text import capfirst from django.utils.translation import gettext from polymorphic.formsets import BasePolymorphicModelFormSet register = Library() @register.filter() def include_empty_form(formset): """ .. templatetag:: include_empty_form Make sure the "empty form" is included when displaying a formset (typically table with input rows) """ yield from formset if hasattr(formset, "empty_forms"): # BasePolymorphicModelFormSet yield from formset.empty_forms else: # Standard Django formset yield formset.empty_form @register.filter def as_script_options(formset): """ .. templatetag:: as_script_options A JavaScript data structure for the JavaScript code This generates the ``data-options`` attribute for ``jquery.django-inlines.js`` The formset may define the following extra attributes: - ``verbose_name`` - ``add_text`` - ``show_add_button`` """ verbose_name = getattr(formset, "verbose_name", formset.model._meta.verbose_name) options = { "prefix": formset.prefix, "pkFieldName": formset.model._meta.pk.name, "addText": getattr(formset, "add_text", None) or gettext("Add another %(verbose_name)s") % {"verbose_name": capfirst(verbose_name)}, "showAddButton": getattr(formset, "show_add_button", True), "deleteText": gettext("Delete"), } if isinstance(formset, BasePolymorphicModelFormSet): # Allow to add different types options["childTypes"] = [ { "name": force_str(model._meta.verbose_name), "type": model._meta.model_name, } for model in formset.child_forms.keys() ] return json.dumps(options) @register.filter def as_form_type(form): """ .. templatetag:: as_form_type Usage: ``{{ form|as_form_type }}`` """ return form._meta.model._meta.model_name @register.filter def as_model_name(model): """ .. templatetag:: as_model_name Usage: ``{{ model|as_model_name }}`` """ return model._meta.model_name django-polymorphic-4.10.2/src/polymorphic/tests/000077500000000000000000000000001513173623500217075ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/__init__.py000066400000000000000000000000201513173623500240100ustar00rootroot00000000000000HEADLESS = True django-polymorphic-4.10.2/src/polymorphic/tests/admin.py000066400000000000000000000126001513173623500233500ustar00rootroot00000000000000from inspect import isclass from django.contrib.admin import register, ModelAdmin, TabularInline, site as admin_site from django.db.models.query import QuerySet from django.http import HttpRequest from polymorphic.admin import ( StackedPolymorphicInline, PolymorphicInlineSupportMixin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter, PolymorphicParentModelAdmin, ) from polymorphic.tests.models import ( PlainA, Model2A, Model2B, Model2C, Model2D, InlineModelA, InlineModelB, InlineParent, NoChildren, ModelWithPolyFK, M2MAdminTest, M2MAdminTestChildA, M2MAdminTestChildB, M2MAdminTestChildC, M2MThroughBase, M2MThroughProject, M2MThroughPerson, M2MThroughMembership, M2MThroughMembershipWithPerson, M2MThroughMembershipWithSpecialPerson, M2MThroughProjectWithTeam, M2MThroughSpecialPerson, DirectM2MContainer, ) @register(Model2A) class Model2Admin(PolymorphicParentModelAdmin): list_filter = (PolymorphicChildModelFilter,) child_models = (Model2A, Model2B, Model2C, Model2D) admin_site.register(Model2B, PolymorphicChildModelAdmin) admin_site.register(Model2C, PolymorphicChildModelAdmin) @register(Model2D) class Model2DAdmin(PolymorphicChildModelAdmin): exclude = ("field3",) @register(PlainA) class PlainAAdmin(ModelAdmin): search_fields = ["field1"] def get_queryset(self, request: HttpRequest) -> QuerySet: return super().get_queryset(request).order_by("pk") class Inline(StackedPolymorphicInline): model = InlineModelA def get_child_inlines(self): return [ child for child in self.__class__.__dict__.values() if isclass(child) and issubclass(child, StackedPolymorphicInline.Child) ] class InlineModelAChild(StackedPolymorphicInline.Child): model = InlineModelA class InlineModelBChild(StackedPolymorphicInline.Child): model = InlineModelB autocomplete_fields = ["plain_a"] @register(InlineParent) class InlineParentAdmin(PolymorphicInlineSupportMixin, ModelAdmin): inlines = (Inline,) extra = 1 @register(NoChildren) class NoChildrenAdmin(PolymorphicParentModelAdmin): child_models = (NoChildren,) @register(ModelWithPolyFK) class ModelWithPolyFKAdmin(ModelAdmin): fields = ["name", "poly_fk"] @register(M2MAdminTest) class M2MAdminTestAdmin(PolymorphicParentModelAdmin): list_filter = (PolymorphicChildModelFilter,) child_models = (M2MAdminTestChildA, M2MAdminTestChildB, M2MAdminTestChildC) @register(M2MAdminTestChildA) class M2MAdminTestChildA(PolymorphicChildModelAdmin): raw_id_fields = ("child_bs",) @register(M2MAdminTestChildB) class M2MAdminTestChildB(PolymorphicChildModelAdmin): raw_id_fields = ("child_as",) @register(M2MAdminTestChildC) class M2MAdminTestChildC(PolymorphicChildModelAdmin): raw_id_fields = ("child_as",) # Issue #182: M2M field in model admin # Register models to test M2M field to polymorphic model @register(M2MThroughBase) class M2MThroughBaseAdmin(PolymorphicParentModelAdmin): """Base admin for polymorphic M2M test models.""" child_models = ( M2MThroughProject, M2MThroughPerson, M2MThroughProjectWithTeam, M2MThroughSpecialPerson, ) @register(M2MThroughProject) class M2MThroughProjectAdmin(PolymorphicChildModelAdmin): """Admin for M2MThroughProject polymorphic child.""" pass @register(M2MThroughPerson) class M2MThroughPersonAdmin(PolymorphicChildModelAdmin): """Admin for M2MThroughPerson polymorphic child.""" pass @register(M2MThroughSpecialPerson) class M2MThroughSpecialPersonAdmin(PolymorphicChildModelAdmin): """Admin for M2MThroughSpecialPerson polymorphic child.""" pass @register(DirectM2MContainer) class DirectM2MContainerAdmin(ModelAdmin): """ Test case for Issue #182: M2M field in model admin. DirectM2MContainer has a direct M2M field to polymorphic M2MThroughBase model. This should work without AttributeError: 'int' object has no attribute 'pk'. """ filter_horizontal = ("items",) # Issue #375: Admin with M2M through table on polymorphic model class M2MThroughMembershipInline(StackedPolymorphicInline): """ Polymorphic inline for Issue #375: M2M through table with polymorphic membership types. This tests creating different membership types inline based on person type. """ model = M2MThroughMembership extra = 1 class MembershipWithPersonChild(StackedPolymorphicInline.Child): """Inline for regular Person membership.""" model = M2MThroughMembershipWithPerson class MembershipWithSpecialPersonChild(StackedPolymorphicInline.Child): """Inline for SpecialPerson membership with special notes.""" model = M2MThroughMembershipWithSpecialPerson child_inlines = ( MembershipWithPersonChild, MembershipWithSpecialPersonChild, ) @register(M2MThroughProjectWithTeam) class M2MThroughProjectWithTeamAdmin(PolymorphicInlineSupportMixin, PolymorphicChildModelAdmin): """ Test case for Issue #375: Admin with M2M through table on polymorphic model. M2MThroughProjectWithTeam (polymorphic) has M2M to M2MThroughPerson (polymorphic) with custom through model M2MThroughMembership (now polymorphic). Uses polymorphic inlines to support different membership types. """ inlines = (M2MThroughMembershipInline,) django-polymorphic-4.10.2/src/polymorphic/tests/admintestcase.py000066400000000000000000000206641513173623500251150ustar00rootroot00000000000000from django.conf import settings from django.contrib.admin import AdminSite from django.contrib.admin.templatetags.admin_urls import admin_urlname from django.contrib.auth.models import User from django.contrib.messages.middleware import MessageMiddleware from django.http.response import HttpResponse from django.test import RequestFactory, TestCase from django.urls import clear_url_caches, include, path, reverse, set_urlconf class AdminTestCase(TestCase): """ Testing the admin site """ #: The model to test model = None #: The admin class to test admin_class = None @classmethod def setUpClass(cls): super().setUpClass() cls.admin_user = User.objects.create_superuser( "admin", "admin@example.org", password="admin" ) def setUp(self): super().setUp() # Have a separate site, to avoid dependency on polymorphic wrapping or standard admin configuration self.admin_site = AdminSite() if self.model is not None: self.admin_register(self.model, self.admin_class) def tearDown(self): clear_url_caches() set_urlconf(None) def register(self, model): """Decorator, like admin.register()""" def _dec(admin_class): self.admin_register(model, admin_class) return admin_class return _dec def admin_register(self, model, admin_site): """Register an model with admin to the test case, test client and URL reversing code.""" self.admin_site.register(model, admin_site) # Make sure the URLs are reachable by reverse() clear_url_caches() set_urlconf(tuple([path("tmp-admin/", self.admin_site.urls)])) def get_admin_instance(self, model): try: return self.admin_site._registry[model] except KeyError: raise ValueError(f"Model not registered with admin: {model}") @classmethod def tearDownClass(cls): super().tearDownClass() clear_url_caches() set_urlconf(None) def get_add_url(self, model): admin_instance = self.get_admin_instance(model) return reverse(admin_urlname(admin_instance.opts, "add")) def get_changelist_url(self, model): admin_instance = self.get_admin_instance(model) return reverse(admin_urlname(admin_instance.opts, "changelist")) def get_change_url(self, model, object_id): admin_instance = self.get_admin_instance(model) return reverse(admin_urlname(admin_instance.opts, "change"), args=(object_id,)) def get_history_url(self, model, object_id): admin_instance = self.get_admin_instance(model) return reverse(admin_urlname(admin_instance.opts, "history"), args=(object_id,)) def get_delete_url(self, model, object_id): admin_instance = self.get_admin_instance(model) return reverse(admin_urlname(admin_instance.opts, "delete"), args=(object_id,)) def admin_get_add(self, model, qs=""): """ Make a direct "add" call to the admin page, circumvening login checks. """ admin_instance = self.get_admin_instance(model) request = self.create_admin_request("get", self.get_add_url(model) + qs) response = admin_instance.add_view(request) assert response.status_code == 200 return response def admin_post_add(self, model, formdata, qs=""): """ Make a direct "add" call to the admin page, circumvening login checks. """ admin_instance = self.get_admin_instance(model) request = self.create_admin_request("post", self.get_add_url(model) + qs, data=formdata) response = admin_instance.add_view(request) self.assertFormSuccess(request.path, response) return response def admin_get_changelist(self, model): """ Make a direct "add" call to the admin page, circumvening login checks. """ admin_instance = self.get_admin_instance(model) request = self.create_admin_request("get", self.get_changelist_url(model)) response = admin_instance.changelist_view(request) assert response.status_code == 200 return response def admin_get_change(self, model, object_id, query=None, **extra): """ Perform a GET request on the admin page """ admin_instance = self.get_admin_instance(model) request = self.create_admin_request( "get", self.get_change_url(model, object_id), data=query, **extra ) response = admin_instance.change_view(request, str(object_id)) assert response.status_code == 200 return response def admin_post_change(self, model, object_id, formdata, **extra): """ Make a direct "add" call to the admin page, circumvening login checks. """ admin_instance = self.get_admin_instance(model) request = self.create_admin_request( "post", self.get_change_url(model, object_id), data=formdata, **extra ) response = admin_instance.change_view(request, str(object_id)) self.assertFormSuccess(request.path, response) return response def admin_get_history(self, model, object_id, query=None, **extra): """ Perform a GET request on the admin page """ admin_instance = self.get_admin_instance(model) request = self.create_admin_request( "get", self.get_history_url(model, object_id), data=query, **extra ) response = admin_instance.history_view(request, str(object_id)) assert response.status_code == 200 return response def admin_get_delete(self, model, object_id, query=None, **extra): """ Perform a GET request on the admin delete page """ admin_instance = self.get_admin_instance(model) request = self.create_admin_request( "get", self.get_delete_url(model, object_id), data=query, **extra ) response = admin_instance.delete_view(request, str(object_id)) assert response.status_code == 200 return response def admin_post_delete(self, model, object_id, **extra): """ Make a direct "add" call to the admin page, circumvening login checks. """ if not extra: extra = {"data": {"post": "yes"}} admin_instance = self.get_admin_instance(model) request = self.create_admin_request("post", self.get_delete_url(model, object_id), **extra) response = admin_instance.delete_view(request, str(object_id)) assert response.status_code == 302, f"Form errors in calling {request.path}" return response def create_admin_request(self, method, url, data=None, **extra): """ Construct an Request instance for the admin view. """ factory_method = getattr(RequestFactory(), method) if data is not None: if method != "get": data["csrfmiddlewaretoken"] = "foo" dummy_request = factory_method(url, data=data) dummy_request.user = self.admin_user # Add the management form fields if needed. # base_data = self._get_management_form_data(dummy_request) # base_data.update(data) # data = base_data request = factory_method(url, data=data, **extra) request.COOKIES[settings.CSRF_COOKIE_NAME] = "foo" request.csrf_processing_done = True # Add properties which middleware would typically do request.session = {} request.user = self.admin_user MessageMiddleware(lambda r: HttpResponse("OK?")).process_request(request) return request def assertFormSuccess(self, request_url, response): """ Assert that the response was a redirect, not a form error. """ assert response.status_code in [200, 302] if response.status_code != 302: context_data = response.context_data if "errors" in context_data: errors = response.context_data["errors"] elif "form" in context_data: errors = context_data["form"].errors else: raise KeyError("Unknown field for errors in the TemplateResponse!") assert response.status_code == 302, ( f"Form errors in calling {request_url}:\n{errors.as_text()}" ) assert "/login/?next=" not in response["Location"], ( f"Received login response for {request_url}" ) django-polymorphic-4.10.2/src/polymorphic/tests/conftest.py000066400000000000000000000010031513173623500241000ustar00rootroot00000000000000from __future__ import annotations import pathlib import pytest INTEGRATION_DIR = pathlib.Path(__file__).resolve().parent / "examples" / "integrations" def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: for item in items: # item.path is pathlib.Path on modern pytest; fall back for older p = pathlib.Path(str(getattr(item, "path", item.fspath))).resolve() if INTEGRATION_DIR in p.parents: item.add_marker(pytest.mark.integration) django-polymorphic-4.10.2/src/polymorphic/tests/debug.py000066400000000000000000000000461513173623500233470ustar00rootroot00000000000000from .settings import * DEBUG = True django-polymorphic-4.10.2/src/polymorphic/tests/deletion/000077500000000000000000000000001513173623500235125ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/deletion/__init__.py000066400000000000000000000000001513173623500256110ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/deletion/migrations/000077500000000000000000000000001513173623500256665ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/deletion/migrations/0001_initial.py000066400000000000000000001132311513173623500303320ustar00rootroot00000000000000# Generated by Django 5.2 on 2026-01-13 13:59 import django.db.models.deletion import polymorphic.tests.deletion.models from decimal import Decimal from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ('contenttypes', '0002_remove_content_type_name'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='Poly1', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Poly2', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Poly4', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Poly4_1', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='A_160', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='A_160Plain', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='A_274', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='A_540', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ('self_referential', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='deletion.a_540')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Animal', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Answer', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='B_160', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('a', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.a_160')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='B_160Plain', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('a', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.a_160plain')), ], ), migrations.CreateModel( name='Base', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='Beneficiary', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('firstname', models.CharField(max_length=100)), ('lastname', models.CharField(max_length=100)), ], ), migrations.CreateModel( name='Payment', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('amount', models.DecimalField(blank=True, decimal_places=2, default=Decimal('0'), max_digits=10)), ('index', models.PositiveIntegerField(default=0)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'ordering': ('index',), }, ), migrations.CreateModel( name='CustomModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='Farm', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='Normal2', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='Normal3', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='Normal4', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='Normal5', fields=[ ('n_pk', models.AutoField(primary_key=True, serialize=False)), ], ), migrations.CreateModel( name='Order', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=200, verbose_name='Title')), ], options={ 'ordering': ('title',), }, ), migrations.CreateModel( name='PlainA', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='Poll', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='PolyDevice', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=64)), ], ), migrations.CreateModel( name='PolyInterface', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=64)), ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.polydevice')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Standalone', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='A1', fields=[ ('poly1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly1')), ('some_data', models.CharField(blank=True, default='', max_length=100)), ], options={ 'abstract': False, }, bases=('deletion.poly1',), ), migrations.CreateModel( name='B1', fields=[ ('poly1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly1')), ], options={ 'abstract': False, }, bases=('deletion.poly1',), ), migrations.CreateModel( name='C1', fields=[ ('poly1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly1')), ], options={ 'abstract': False, }, bases=('deletion.poly1',), ), migrations.CreateModel( name='A2', fields=[ ('poly2_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly2')), ], options={ 'abstract': False, }, bases=('deletion.poly2',), ), migrations.CreateModel( name='B2', fields=[ ('poly2_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly2')), ], options={ 'abstract': False, }, bases=('deletion.poly2',), ), migrations.CreateModel( name='C2', fields=[ ('poly2_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly2')), ], options={ 'abstract': False, }, bases=('deletion.poly2',), ), migrations.CreateModel( name='A4', fields=[ ('poly4_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly4')), ], options={ 'abstract': False, }, bases=('deletion.poly4',), ), migrations.CreateModel( name='B4', fields=[ ('poly4_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly4')), ], options={ 'abstract': False, }, bases=('deletion.poly4',), ), migrations.CreateModel( name='C4', fields=[ ('poly4_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly4')), ], options={ 'abstract': False, }, bases=('deletion.poly4',), ), migrations.CreateModel( name='A4_1', fields=[ ('poly4_1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly4_1')), ], options={ 'abstract': False, }, bases=('deletion.poly4_1',), ), migrations.CreateModel( name='B4_1', fields=[ ('poly4_1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly4_1')), ], options={ 'abstract': False, }, bases=('deletion.poly4_1',), ), migrations.CreateModel( name='C4_1', fields=[ ('poly4_1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly4_1')), ], options={ 'abstract': False, }, bases=('deletion.poly4_1',), ), migrations.CreateModel( name='B_274', fields=[ ('a_274_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.a_274')), ], options={ 'abstract': False, }, bases=('deletion.a_274',), ), migrations.CreateModel( name='D_274', fields=[ ('a_274_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.a_274')), ], options={ 'abstract': False, }, bases=('deletion.a_274',), ), migrations.CreateModel( name='B_540', fields=[ ('a_540_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.a_540')), ('name', models.CharField(max_length=256)), ], options={ 'abstract': False, }, bases=('deletion.a_540',), ), migrations.CreateModel( name='Cat', fields=[ ('animal_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.animal')), ('cat_param', models.CharField(max_length=100)), ], options={ 'abstract': False, }, bases=('deletion.animal',), ), migrations.CreateModel( name='Dog', fields=[ ('animal_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.animal')), ('dog_param', models.CharField(max_length=100)), ], options={ 'abstract': False, }, bases=('deletion.animal',), ), migrations.CreateModel( name='TextAnswer', fields=[ ('answer_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.answer')), ('answer', models.CharField(blank=True, default='', max_length=500)), ], options={ 'abstract': False, }, bases=('deletion.answer',), ), migrations.CreateModel( name='YesNoAnswer', fields=[ ('answer_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.answer')), ('answer', models.BooleanField(default=False, verbose_name='answer')), ], options={ 'abstract': False, }, bases=('deletion.answer',), ), migrations.CreateModel( name='B1_160', fields=[ ('b_160_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.b_160')), ], options={ 'abstract': False, }, bases=('deletion.b_160',), ), migrations.CreateModel( name='B2_160', fields=[ ('b_160_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.b_160')), ], options={ 'abstract': False, }, bases=('deletion.b_160',), ), migrations.CreateModel( name='B1_160Plain', fields=[ ('b_160plain_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.b_160plain')), ], bases=('deletion.b_160plain',), ), migrations.CreateModel( name='B2_160Plain', fields=[ ('b_160plain_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.b_160plain')), ], bases=('deletion.b_160plain',), ), migrations.CreateModel( name='Child', fields=[ ('base_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.base')), ], bases=('deletion.base',), ), migrations.CreateModel( name='CreditCardPayment', fields=[ ('payment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.payment')), ('card_type', models.CharField(max_length=32)), ], options={ 'abstract': False, }, bases=('deletion.payment',), ), migrations.CreateModel( name='DatasetFolder', fields=[ ('custommodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.custommodel')), ], bases=('deletion.custommodel',), ), migrations.CreateModel( name='OriginalFile', fields=[ ('custommodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.custommodel')), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ('content_type', models.CharField(max_length=100)), ('size', models.PositiveIntegerField()), ('dataset_folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.datasetfolder')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=('deletion.custommodel', models.Model), ), migrations.AddField( model_name='animal', name='farm', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='animals', to='deletion.farm'), ), migrations.CreateModel( name='Normal1', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('poly', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.poly1')), ], ), migrations.AddField( model_name='poly2', name='normal', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polies', to='deletion.normal2'), ), migrations.CreateModel( name='Poly3', fields=[ ('normal3_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.normal3')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=('deletion.normal3', models.Model), ), migrations.AddField( model_name='poly4', name='normals', field=models.ManyToManyField(blank=True, related_name='polies', to='deletion.normal4'), ), migrations.CreateModel( name='Normal4_1', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polies', models.ManyToManyField(blank=True, related_name='normals', to='deletion.poly4_1')), ], ), migrations.CreateModel( name='Poly5', fields=[ ('normal5_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='deletion.normal5')), ('p_pk', models.AutoField(primary_key=True, serialize=False)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=('deletion.normal5', models.Model), ), migrations.AddField( model_name='payment', name='order', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.order'), ), migrations.CreateModel( name='PlainB1', fields=[ ('plaina_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.plaina')), ('standalones', models.ManyToManyField(to='deletion.standalone')), ], bases=('deletion.plaina',), ), migrations.CreateModel( name='PolyEthernetInterface', fields=[ ('polyinterface_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.polyinterface')), ('ethernety_stuff', models.CharField(max_length=64)), ], options={ 'abstract': False, }, bases=('deletion.polyinterface',), ), migrations.CreateModel( name='PolyFCInterface', fields=[ ('polyinterface_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.polyinterface')), ('fc_stuff', models.CharField(max_length=64)), ], options={ 'abstract': False, }, bases=('deletion.polyinterface',), ), migrations.CreateModel( name='PolyWirelessInterface', fields=[ ('polyinterface_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.polyinterface')), ('wirelessy_stuff', models.CharField(max_length=64)), ], options={ 'abstract': False, }, bases=('deletion.polyinterface',), ), migrations.CreateModel( name='Question', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('poll', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.poll')), ], ), migrations.AddField( model_name='answer', name='question', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.question'), ), migrations.AddField( model_name='plaina', name='standalone_parent', field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='plainas', to='deletion.standalone'), ), migrations.CreateModel( name='C_274', fields=[ ('b_274_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.b_274')), ], options={ 'abstract': False, }, bases=('deletion.b_274',), ), migrations.CreateModel( name='E_274', fields=[ ('d_274_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.d_274')), ], options={ 'abstract': False, }, bases=('deletion.d_274',), ), migrations.CreateModel( name='C_160', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('b', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.b1_160')), ], ), migrations.CreateModel( name='C_160Plain', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('b', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.b1_160plain')), ], ), migrations.CreateModel( name='GrandChild', fields=[ ('child_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.child')), ], bases=('deletion.child',), ), migrations.CreateModel( name='RelatedToChild', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('child', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='relatives', to='deletion.child')), ], ), migrations.CreateModel( name='SepaPayment', fields=[ ('payment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.payment')), ('iban', models.CharField(max_length=34)), ('bic', models.CharField(max_length=11)), ('beneficiaries', models.ManyToManyField(blank=True, related_name='sepa', to='deletion.beneficiary')), ], options={ 'abstract': False, }, bases=('deletion.payment',), ), migrations.CreateModel( name='DatasetRelation', fields=[ ('originalfile_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.originalfile')), ('file', models.FileField(max_length=500, upload_to=polymorphic.tests.deletion.models.project_directory_path)), ('original_file_name', models.CharField(max_length=100)), ], options={ 'abstract': False, }, bases=('deletion.originalfile',), ), migrations.CreateModel( name='OriginalImage', fields=[ ('originalfile_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.originalfile')), ('original_file_name', models.CharField(max_length=100)), ('file', models.FileField(max_length=500, upload_to=polymorphic.tests.deletion.models.project_directory_path)), ], options={ 'abstract': False, }, bases=('deletion.originalfile',), ), migrations.CreateModel( name='Project', fields=[ ('custommodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.custommodel')), ('name', models.CharField(max_length=30)), ('created_at', models.DateTimeField(auto_now_add=True)), ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], bases=('deletion.custommodel',), ), migrations.AddField( model_name='datasetfolder', name='prjct', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.project'), ), migrations.CreateModel( name='A3', fields=[ ('poly3_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly3')), ], options={ 'abstract': False, }, bases=('deletion.poly3',), ), migrations.CreateModel( name='B3', fields=[ ('poly3_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly3')), ], options={ 'abstract': False, }, bases=('deletion.poly3',), ), migrations.CreateModel( name='A5', fields=[ ('poly5_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='deletion.poly5')), ('a_pk', models.AutoField(primary_key=True, serialize=False)), ], options={ 'abstract': False, }, bases=('deletion.poly5',), ), migrations.CreateModel( name='B5', fields=[ ('poly5_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='deletion.poly5')), ('b_pk', models.AutoField(primary_key=True, serialize=False)), ], options={ 'abstract': False, }, bases=('deletion.poly5',), ), migrations.CreateModel( name='PlainB2', fields=[ ('plaina_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.plaina')), ('standalone', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='plainb2s', to='deletion.standalone')), ], bases=('deletion.plaina',), ), migrations.CreateModel( name='PolyFixedInterface', fields=[ ('polyethernetinterface_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.polyethernetinterface')), ('fixed_stuff', models.CharField(max_length=64)), ], options={ 'abstract': False, }, bases=('deletion.polyethernetinterface',), ), migrations.CreateModel( name='PolyModularInterface', fields=[ ('polyethernetinterface_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.polyethernetinterface')), ('modular_stuff', models.CharField(max_length=64)), ], options={ 'abstract': False, }, bases=('deletion.polyethernetinterface',), ), migrations.CreateModel( name='RelatedToGrandChild', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('grand_child', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='grand_relatives', to='deletion.grandchild')), ], ), migrations.CreateModel( name='OriginalDataset', fields=[ ('originalfile_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.originalfile')), ('file', models.FileField(max_length=500, upload_to=polymorphic.tests.deletion.models.project_directory_path)), ('original_file_name', models.CharField(blank=True, max_length=100, null=True)), ('table_name', models.CharField(blank=True, max_length=100, null=True)), ('rows_number', models.PositiveIntegerField()), ('dataset_metadata', models.JSONField()), ('dataset_relation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='deletion.datasetrelation')), ], options={ 'abstract': False, }, bases=('deletion.originalfile',), ), migrations.CreateModel( name='PlainC1', fields=[ ('plainb1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.plainb1')), ('standalone', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='plainc1s', to='deletion.standalone')), ], bases=('deletion.plainb1',), ), ] django-polymorphic-4.10.2/src/polymorphic/tests/deletion/migrations/__init__.py000066400000000000000000000000001513173623500277650ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/deletion/models.py000066400000000000000000000241311513173623500253500ustar00rootroot00000000000000from django.db import models from django.conf import settings from polymorphic.models import PolymorphicModel from polymorphic.deletion import PolymorphicGuard from decimal import Decimal def project_directory_path(instance, filename): # just to satisfy upload_to; keep it deterministic for tests return f"p{instance.dataset_folder.prjct_id}/{filename}" CASCADE = models.CASCADE class Standalone(models.Model): pass class PlainA(models.Model): standalone_parent = models.ForeignKey( Standalone, on_delete=CASCADE, null=True, default=None, related_name="plainas" ) class PlainB1(PlainA): standalones = models.ManyToManyField(Standalone) class PlainB2(PlainA): standalone = models.ForeignKey( Standalone, on_delete=CASCADE, null=True, default=None, related_name="plainb2s" ) class PlainC1(PlainB1): standalone = models.ForeignKey( Standalone, on_delete=CASCADE, null=True, default=None, related_name="plainc1s" ) class RelatedToChild(models.Model): child = models.ForeignKey( "Child", on_delete=CASCADE, null=True, default=None, related_name="relatives" ) class Base(models.Model): pass class Child(Base): pass class GrandChild(Child): pass class RelatedToGrandChild(models.Model): grand_child = models.ForeignKey( GrandChild, on_delete=CASCADE, null=True, default=None, related_name="grand_relatives" ) ########################################################### """ Scenario 1 <-- cascade -- Normal1 ----- FK ----> Poly1 / | \ A1 B1 C1 """ class Normal1(models.Model): poly = models.ForeignKey("Poly1", on_delete=models.CASCADE) # <-- this is fine class Poly1(PolymorphicModel): pass class A1(Poly1): some_data = models.CharField(max_length=100, blank=True, default="") class B1(Poly1): pass class C1(Poly1): pass """ Scenario 2 -- cascade --> Normal2 <----- FK ---- Poly2 / | \ A2 B2 C2 """ class Normal2(models.Model): pass class Poly2(PolymorphicModel): normal = models.ForeignKey(Normal2, on_delete=CASCADE, related_name="polies") class A2(Poly2): pass class B2(Poly2): pass class C2(Poly2): pass """ Scenario 3 Normal3 | Poly3 | \ A3 B3 """ class Normal3(models.Model): pass class Poly3(PolymorphicModel, Normal3): pass class A3(Poly3): pass class B3(Poly3): pass """ Scenario 4 <--- cascade ---> Normal4 <----- M2M -----> Poly4 / | \ A4 B4 C4 """ class Normal4(models.Model): pass class Poly4(PolymorphicModel): normals = models.ManyToManyField(Normal4, related_name="polies", blank=True) class A4(Poly4): pass class B4(Poly4): pass class C4(Poly4): pass class Normal4_1(models.Model): polies = models.ManyToManyField("Poly4_1", related_name="normals", blank=True) class Poly4_1(PolymorphicModel): pass class A4_1(Poly4_1): pass class B4_1(Poly4_1): pass class C4_1(Poly4_1): pass """ Scenario 5 - scenario3 with custom/different PKs Normal3 | Poly3 | \ A3 B3 """ class Normal5(models.Model): n_pk = models.AutoField(primary_key=True) class Poly5(PolymorphicModel, Normal5): p_pk = models.AutoField(primary_key=True) class A5(Poly5): a_pk = models.AutoField(primary_key=True) class B5(Poly5): b_pk = models.AutoField(primary_key=True) ######################################################################################## # There were 15 years of deletion bug reports - many were duplicates but many also # included example models. We copy all of these provided tests in here not being too # concerned about redundancy - we just want to make sure we don't regress on any of # them. Each block is tagged with the root issue. In some cases we mirror the setup # with a plain django model parallel - mostly for debug/comparison purposes ########################################################### # https://github.com/jazzband/django-polymorphic/issues/160 class A_160(models.Model): pass class B_160(PolymorphicModel): a = models.ForeignKey(A_160, on_delete=CASCADE) class B1_160(B_160): pass class B2_160(B_160): pass class C_160(models.Model): b = models.ForeignKey(B1_160, on_delete=CASCADE) # Plain class A_160Plain(models.Model): pass class B_160Plain(models.Model): a = models.ForeignKey(A_160Plain, on_delete=CASCADE) class B1_160Plain(B_160Plain): pass class B2_160Plain(B_160Plain): pass class C_160Plain(models.Model): # test that guard misapplication is fine b = models.ForeignKey(B1_160Plain, on_delete=PolymorphicGuard(CASCADE)) ########################################################### ########################################################### # https://github.com/jazzband/django-polymorphic/issues/229 class Farm(models.Model): pass class Animal(PolymorphicModel): farm = models.ForeignKey( "Farm", on_delete=PolymorphicGuard(models.CASCADE), related_name="animals" ) name = models.CharField(max_length=100) class Dog(Animal): dog_param = models.CharField(max_length=100) class Cat(Animal): cat_param = models.CharField(max_length=100) ########################################################### # https://github.com/jazzband/django-polymorphic/issues/274 class A_274(PolymorphicModel): pass class B_274(A_274): pass class D_274(A_274): pass class E_274(D_274): pass class C_274(B_274): pass ########################################################### ########################################################### # https://github.com/jazzband/django-polymorphic/issues/357 class Order(models.Model): title = models.CharField("Title", max_length=200) class Meta: ordering = ("title",) def __str__(self): return self.title class Payment(PolymorphicModel): order = models.ForeignKey(Order, on_delete=CASCADE) amount = models.DecimalField(default=Decimal(0.0), blank=True, max_digits=10, decimal_places=2) index = models.PositiveIntegerField(default=0, blank=False) class Meta: ordering = ("index",) class CreditCardPayment(Payment): card_type = models.CharField(max_length=32) class Beneficiary(models.Model): firstname = models.CharField(max_length=100) lastname = models.CharField(max_length=100) class SepaPayment(Payment): iban = models.CharField(max_length=34) bic = models.CharField(max_length=11) beneficiaries = models.ManyToManyField(Beneficiary, "sepa", blank=True) ########################################################### ########################################################### # https://github.com/jazzband/django-polymorphic/issues/481 # no example given - how to? ########################################################### ########################################################### # https://github.com/jazzband/django-polymorphic/issues/540 class A_540(PolymorphicModel): self_referential = models.ForeignKey("self", null=True, blank=True, on_delete=CASCADE) class B_540(A_540): name = models.CharField(max_length=256) ########################################################### ########################################################### # https://github.com/jazzband/django-polymorphic/issues/547 class CustomModel(models.Model): pass class Project(CustomModel): name = models.CharField(max_length=30) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE) created_at = models.DateTimeField(auto_now_add=True) class DatasetFolder(CustomModel): prjct = models.ForeignKey(Project, on_delete=CASCADE) class OriginalFile(PolymorphicModel, CustomModel): dataset_folder = models.ForeignKey(DatasetFolder, on_delete=CASCADE) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) content_type = models.CharField(max_length=100) size = models.PositiveIntegerField() class DatasetRelation(OriginalFile): file = models.FileField(max_length=500, upload_to=project_directory_path) original_file_name = models.CharField(max_length=100) class OriginalDataset(OriginalFile): dataset_relation = models.ForeignKey( DatasetRelation, on_delete=models.SET_NULL, blank=True, null=True ) file = models.FileField(max_length=500, upload_to=project_directory_path) original_file_name = models.CharField(max_length=100, null=True, blank=True) table_name = models.CharField(max_length=100, null=True, blank=True) rows_number = models.PositiveIntegerField() dataset_metadata = models.JSONField() class OriginalImage(OriginalFile): original_file_name = models.CharField(max_length=100) file = models.FileField(max_length=500, upload_to=project_directory_path) ########################################################### ########################################################### # https://github.com/jazzband/django-polymorphic/issues/608 class PolyDevice(models.Model): name = models.CharField(max_length=64) class PolyInterface(PolymorphicModel): name = models.CharField(max_length=64) device = models.ForeignKey(to=PolyDevice, on_delete=CASCADE) class PolyEthernetInterface(PolyInterface): ethernety_stuff = models.CharField(max_length=64) class PolyModularInterface(PolyEthernetInterface): modular_stuff = models.CharField(max_length=64) class PolyFixedInterface(PolyEthernetInterface): fixed_stuff = models.CharField(max_length=64) class PolyWirelessInterface(PolyInterface): wirelessy_stuff = models.CharField(max_length=64) class PolyFCInterface(PolyInterface): fc_stuff = models.CharField(max_length=64) class Poll(models.Model): pass class Question(models.Model): poll = models.ForeignKey(to=Poll, on_delete=CASCADE) class Answer(PolymorphicModel): question = models.ForeignKey(to=Question, on_delete=CASCADE) class TextAnswer(Answer): answer = models.CharField(default="", blank=True, max_length=500) class YesNoAnswer(Answer): answer = models.BooleanField("answer", default=False) django-polymorphic-4.10.2/src/polymorphic/tests/deletion/test_deletion.py000066400000000000000000000764151513173623500267430ustar00rootroot00000000000000from django.test import TestCase import shutil import tempfile from django.contrib.auth import get_user_model from django.core.files.base import ContentFile from django.test.utils import CaptureQueriesContext from django.test import override_settings from django.db import connection @override_settings() class TestDeletion(TestCase): def setUp(self): super().setUp() self._media_root = tempfile.mkdtemp(prefix="test-media-") self._media_override = override_settings(MEDIA_ROOT=self._media_root) self._media_override.enable() def tearDown(self): self._media_override.disable() shutil.rmtree(self._media_root, ignore_errors=True) super().tearDown() def test_deletion_bug_160(self): """https://github.com/jazzband/django-polymorphic/issues/160""" from .models import A_160, B_160, B1_160, B2_160, C_160 from .models import A_160Plain, B_160Plain, C_160Plain, B1_160Plain, B2_160Plain a = A_160Plain.objects.create() a2 = A_160Plain.objects.create() b1 = B1_160Plain.objects.create(a=a) b2 = B2_160Plain.objects.create(a=a) b2_2 = B2_160Plain.objects.create(a=a2) c = C_160Plain.objects.create(b=b1) a.delete() assert [a2] == list(A_160Plain.objects.all()) assert B_160Plain.objects.count() == 1 assert B1_160Plain.objects.count() == 0 assert [b2_2] == list(B2_160Plain.objects.all()) assert C_160Plain.objects.count() == 0 a = A_160.objects.create() a2 = A_160.objects.create() b1 = B1_160.objects.create(a=a) b2 = B2_160.objects.create(a=a) b2_2 = B2_160.objects.create(a=a2) c = C_160.objects.create(b=b1) a.delete() assert [a2] == list(A_160.objects.all()) assert B_160.objects.count() == 1 assert B1_160.objects.count() == 0 assert [b2_2] == list(B2_160.objects.all()) assert C_160.objects.count() == 0 def test_deletion_bug_229(self): """ https://github.com/jazzband/django-polymorphic/issues/229 """ from .models import Farm, Animal, Dog, Cat farm = Farm.objects.create() Dog.objects.create(farm=farm, name="Rex", dog_param="kibble") Cat.objects.create(farm=farm, name="Misty", cat_param="whiskers") farm.delete() assert Animal.objects.count() == 0 assert Dog.objects.count() == 0 assert Cat.objects.count() == 0 assert Farm.objects.count() == 0 farm = Farm.objects.create() Dog.objects.create(farm=farm, name="Rex", dog_param="kibble") Cat.objects.create(farm=farm, name="Misty", cat_param="whiskers") farm2 = Farm.objects.create() hugo = Cat.objects.create(farm=farm2, name="Hugo", cat_param="10") marlo = Cat.objects.create(farm=farm2, name="Marlo", cat_param="14") assert Animal.objects.count() == 4 farm.delete() assert Animal.objects.count() == 2 assert hugo in Cat.objects.all() assert marlo in Cat.objects.all() assert hugo in Animal.objects.all() assert marlo in Animal.objects.all() assert hugo in farm2.animals.all() assert marlo in farm2.animals.all() Animal.objects.all().delete() assert Animal.objects.count() == 0 assert Dog.objects.count() == 0 assert Cat.objects.count() == 0 assert Farm.objects.count() == 1 assert farm2 in Farm.objects.all() assert farm2.animals.count() == 0 def test_deletion_bug_274(self): """ https://github.com/jazzband/django-polymorphic/issues/274 """ from .models import A_274, B_274, C_274, D_274, E_274 B_274.objects.create() D_274.objects.create() E_274.objects.create() A_274.objects.all().delete() assert A_274.objects.count() == 0 assert B_274.objects.count() == 0 assert D_274.objects.count() == 0 assert E_274.objects.count() == 0 assert C_274.objects.count() == 0 def test_deletion_bug_357(self): """ https://github.com/jazzband/django-polymorphic/issues/357 """ from .models import Order, Payment, CreditCardPayment, SepaPayment, Beneficiary order1 = Order.objects.create(title="Order 1") payment1 = SepaPayment.objects.create( order=order1, amount=100.00, iban="DE89370400440532013000", bic="COBADEFFXXX", ) CreditCardPayment.objects.create( order=order1, amount=100.00, card_type="VISA", ) bk = Beneficiary.objects.create(firstname="Brian", lastname="Kohan") ea = Beneficiary.objects.create(firstname="Edward", lastname="Abbey") payment1.beneficiaries.add(bk, ea) Order.objects.all().delete() assert Order.objects.count() == 0 assert Payment.objects.count() == 0 assert set(Beneficiary.objects.all()) == {bk, ea} def test_deletion_bug_540(self): """ https://github.com/jazzband/django-polymorphic/issues/540 """ from .models import A_540, B_540 b = B_540.objects.create(self_referential=None, name="b") a = A_540.objects.create(self_referential=b) A_540.objects.all().delete() assert A_540.objects.count() == 0 assert B_540.objects.count() == 0 def test_deletion_bug_547(self): """ https://github.com/jazzband/django-polymorphic/issues/547 """ from .models import Project, DatasetFolder, OriginalFile, DatasetRelation, OriginalDataset User = get_user_model() user = User.objects.create_user(username="u1", password="x") project = Project.objects.create(name="p1", created_by=user) folder = DatasetFolder.objects.create(prjct=project) rel_bytes = b"id,parent_id\n1,\n2,1\n" rel_file = ContentFile(rel_bytes, name="relations.csv") relation = DatasetRelation.objects.create( dataset_folder=folder, content_type="text/csv", size=len(rel_bytes), file=rel_file, original_file_name="relations.csv", ) ds_bytes = b"id,value\n1,foo\n2,bar\n" ds_file = ContentFile(ds_bytes, name="data.csv") OriginalDataset.objects.create( dataset_folder=folder, content_type="text/csv", size=len(ds_bytes), dataset_relation=relation, file=ds_file, original_file_name="data.csv", table_name="data", rows_number=2, dataset_metadata={"columns": ["id", "value"]}, ) # This is the operation that (per report) can crash with: # AttributeError: 'NoneType' object has no attribute 'attname' # # If the bug is present, this test will error here. project.delete() # If deletion succeeded, everything should be gone. assert Project.objects.count() == 0 assert DatasetFolder.objects.count() == 0 assert OriginalFile.objects.count() == 0 assert DatasetRelation.objects.count() == 0 assert OriginalDataset.objects.count() == 0 def test_deletion_bug_608(self): """ https://github.com/jazzband/django-polymorphic/issues/608 """ from .models import ( PolyDevice, PolyEthernetInterface, PolyFCInterface, PolyFixedInterface, PolyInterface, PolyModularInterface, PolyWirelessInterface, # NotPolyInterface ) device = PolyDevice.objects.create(name="Device 1") PolyEthernetInterface.objects.create(name="Eth0", device=device, ethernety_stuff="stuff") PolyFCInterface.objects.create(name="FC0", device=device, fc_stuff="stuff") PolyFixedInterface.objects.create(name="Fixed0", device=device, fixed_stuff="stuff") PolyModularInterface.objects.create(name="Modular0", device=device, modular_stuff="stuff") PolyWirelessInterface.objects.create( name="Wireless0", device=device, wirelessy_stuff="stuff" ) PolyDevice.objects.all().delete() assert PolyDevice.objects.count() == 0 def test_deletion_bug_608_2(self): """ https://github.com/jazzband/django-polymorphic/issues/608 """ from .models import Poll, Question, Answer, TextAnswer, YesNoAnswer poll = Poll.objects.create() question = Question.objects.create(poll=poll) answer1 = TextAnswer.objects.create(question=question, answer="test") answer2 = YesNoAnswer.objects.create(question=question, answer=True) poll.delete() assert Poll.objects.count() == 0 assert Question.objects.count() == 0 assert Answer.objects.count() == 0 assert TextAnswer.objects.count() == 0 assert YesNoAnswer.objects.count() == 0 def test_vanilla_deletion(self): """ Test Django's vanilla multi table inheritance deletion and signaling. PlainA *-----> Standalone / \ Standalone *------* PlainB1 PlainB2 *------> Standalone | PlainC1 *------> Standalone """ from .models import ( PlainA, PlainB1, PlainC1, PlainB2, Standalone, RelatedToChild, Base, Child, GrandChild, RelatedToGrandChild, ) print("---------------------------") PlainA.objects.create() with CaptureQueriesContext(connection) as ctx: """ SELECT "plaina"."id", "plaina"."standalone_parent_id" FROM "plaina" SELECT "plainb1"."plaina_ptr_id" FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (1) DELETE FROM "plainb2" WHERE "plainb2"."plaina_ptr_id" IN (1) DELETE FROM "plaina" WHERE "plaina"."id" IN (1) """ PlainA.objects.all().delete() # for q in ctx.captured_queries: # print(q["sql"]) print("---------------------------") PlainB1.objects.create() with CaptureQueriesContext(connection) as ctx: """ SELECT "plaina"."id", "plaina"."standalone_parent_id", "plainb1"."plaina_ptr_id" FROM "plainb1" INNER JOIN "plaina" ON ("plainb1"."plaina_ptr_id" = "plaina"."id") SELECT "plainb1"."plaina_ptr_id", "plainc1"."plainb1_ptr_id" FROM "plainc1" INNER JOIN "plainb1" ON ("plainc1"."plainb1_ptr_id" = "plainb1"."plaina_ptr_id") WHERE "plainc1"."plainb1_ptr_id" IN (2) DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."plainb1_id" IN (2) DELETE FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (2) DELETE FROM "plaina" WHERE "plaina"."id" IN (2) """ PlainB1.objects.all().delete() # for q in ctx.captured_queries: # print(q["sql"]) print("---------------------------") PlainC1.objects.create() with CaptureQueriesContext(connection) as ctx: """ SELECT "plaina"."id", "plaina"."standalone_parent_id", "plainb1"."plaina_ptr_id", "plainc1"."plainb1_ptr_id", "plainc1"."standalone_id" FROM "plainc1" INNER JOIN "plainb1" ON ("plainc1"."plainb1_ptr_id" = "plainb1"."plaina_ptr_id") INNER JOIN "plaina" ON ("plainb1"."plaina_ptr_id" = "plaina"."id") DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."plainb1_id" IN (3) DELETE FROM "plainc1" WHERE "plainc1"."plainb1_ptr_id" IN (3) DELETE FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (3) DELETE FROM "plaina" WHERE "plaina"."id" IN (3) """ PlainC1.objects.all().delete() # for q in ctx.captured_queries: # print(q["sql"]) print("---------------------------") PlainC1.objects.create() with CaptureQueriesContext(connection) as ctx: """ SELECT "plaina"."id", "plaina"."standalone_parent_id" FROM "plaina" SELECT "plainb1"."plaina_ptr_id" FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (4) SELECT "plaina"."id", "plaina"."standalone_parent_id" FROM "plaina" WHERE "plaina"."id" = 4 LIMIT 21 SELECT "plainb1"."plaina_ptr_id", "plainc1"."plainb1_ptr_id" FROM "plainc1" INNER JOIN "plainb1" ON ("plainc1"."plainb1_ptr_id" = "plainb1"."plaina_ptr_id") WHERE "plainc1"."plainb1_ptr_id" IN (4) SELECT "plaina"."id", "plaina"."standalone_parent_id", "plainb1"."plaina_ptr_id" FROM "plainb1" INNER JOIN "plaina" ON ("plainb1"."plaina_ptr_id" = "plaina"."id") WHERE "plainb1"."plaina_ptr_id" = 4 LIMIT 21 DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."plainb1_id" IN (4) DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."plainb1_id" IN (4) DELETE FROM "plainb2" WHERE "plainb2"."plaina_ptr_id" IN (4) DELETE FROM "plainc1" WHERE "plainc1"."plainb1_ptr_id" IN (4) DELETE FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (4) DELETE FROM "plaina" WHERE "plaina"."id" IN (4) """ PlainA.objects.all().delete() assert PlainC1.objects.count() == 0 # for q in ctx.captured_queries: # print(q["sql"]) print("---------------------------") s0 = Standalone.objects.create() PlainC1.objects.create(standalone=s0) PlainB2.objects.create(standalone=s0) with CaptureQueriesContext(connection) as ctx: """ SELECT "standalone"."id" FROM "standalone" SELECT "plaina"."id" FROM "plaina" WHERE "plaina"."standalone_parent_id" IN (1) SELECT "plaina"."id", "plaina"."standalone_parent_id", "plainb2"."plaina_ptr_id", "plainb2"."standalone_id" FROM "plainb2" INNER JOIN "plaina" ON ("plainb2"."plaina_ptr_id" = "plaina"."id") WHERE "plainb2"."standalone_id" IN (1) SELECT "plainb1"."plaina_ptr_id", "plainc1"."plainb1_ptr_id" FROM "plainc1" INNER JOIN "plainb1" ON ("plainc1"."plainb1_ptr_id" = "plainb1"."plaina_ptr_id") WHERE "plainc1"."standalone_id" IN (1) SELECT "plaina"."id", "plaina"."standalone_parent_id", "plainb1"."plaina_ptr_id" FROM "plainb1" INNER JOIN "plaina" ON ("plainb1"."plaina_ptr_id" = "plaina"."id") WHERE "plainb1"."plaina_ptr_id" = 5 LIMIT 21 DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."plainb1_id" IN (5) DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."standalone_id" IN (1) DELETE FROM "standalone" WHERE "standalone"."id" IN (1) DELETE FROM "plainb2" WHERE "plainb2"."plaina_ptr_id" IN (6) DELETE FROM "plainc1" WHERE "plainc1"."plainb1_ptr_id" IN (5) DELETE FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (5) DELETE FROM "plaina" WHERE "plaina"."id" IN (6, 5) """ Standalone.objects.all().delete() assert PlainC1.objects.count() == 0 assert PlainB2.objects.count() == 0 # for q in ctx.captured_queries: # print(q["sql"]) print("---------------------------") s0 = Standalone.objects.create() s1 = Standalone.objects.create() PlainB2.objects.create(standalone_parent=s0, standalone=s1) with CaptureQueriesContext(connection) as ctx: """ SELECT "plaina"."id" FROM "plaina" WHERE "plaina"."standalone_parent_id" IN (2) SELECT "plainb1"."plaina_ptr_id" FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (7) SELECT "plaina"."id", "plaina"."standalone_parent_id", "plainb2"."plaina_ptr_id", "plainb2"."standalone_id" FROM "plainb2" INNER JOIN "plaina" ON ("plainb2"."plaina_ptr_id" = "plaina"."id") WHERE "plainb2"."standalone_id" IN (2) SELECT "plainb1"."plaina_ptr_id", "plainc1"."plainb1_ptr_id" FROM "plainc1" INNER JOIN "plainb1" ON ("plainc1"."plainb1_ptr_id" = "plainb1"."plaina_ptr_id") WHERE "plainc1"."standalone_id" IN (2) DELETE FROM "plainb2" WHERE "plainb2"."plaina_ptr_id" IN (7) DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."standalone_id" IN (2) DELETE FROM "standalone" WHERE "standalone"."id" IN (2) DELETE FROM "plaina" WHERE "plaina"."id" IN (7) """ s0.delete() assert PlainB2.objects.count() == 0 assert list(Standalone.objects.all()) == [s1] # for q in ctx.captured_queries: # print(q["sql"]) print("---------------------------") grand_child = GrandChild.objects.create() RelatedToChild.objects.create(child=Child.objects.get(pk=grand_child.pk)) RelatedToGrandChild.objects.create(grand_child=grand_child) with CaptureQueriesContext(connection) as ctx: """ SELECT "base"."id" FROM "base" WHERE "base"."id" = 1 SELECT "child"."base_ptr_id" FROM "child" WHERE "child"."base_ptr_id" IN (1) SELECT "base"."id" FROM "base" WHERE "base"."id" = 1 LIMIT 21 SELECT "child"."base_ptr_id", "grandchild"."child_ptr_id" FROM "grandchild" INNER JOIN "child" ON ("grandchild"."child_ptr_id" = "child"."base_ptr_id") WHERE "grandchild"."child_ptr_id" IN (1) SELECT "base"."id", "child"."base_ptr_id" FROM "child" INNER JOIN "base" ON ("child"."base_ptr_id" = "base"."id") WHERE "child"."base_ptr_id" = 1 LIMIT 21 DELETE FROM "relatedtochild" WHERE "relatedtochild"."child_id" IN (1) DELETE FROM "relatedtograndchild" WHERE "relatedtograndchild"."grand_child_id" IN (1) DELETE FROM "relatedtochild" WHERE "relatedtochild"."child_id" IN (1) DELETE FROM "grandchild" WHERE "grandchild"."child_ptr_id" IN (1) DELETE FROM "child" WHERE "child"."base_ptr_id" IN (1) DELETE FROM "base" WHERE "base"."id" IN (1) """ Base.objects.filter(pk=grand_child.pk).delete() # cascade should reach relatives! assert Base.objects.count() == 0 assert Child.objects.count() == 0 assert GrandChild.objects.count() == 0 assert RelatedToChild.objects.count() == 0 assert RelatedToGrandChild.objects.count() == 0 # for q in ctx.captured_queries: # print(q["sql"]) def test_polymorphic_deletion_scenario1(self): """ Test the first polymorphic deletion scenario: A normal model holds a foreign key to a polymorphic base model with several children. <-- cascade -- Normal1 ----- FK ----> Poly1 / | \ A1 B1 C1 Tests that when you delete from a poly instance at any level of the poly hierarchy, cascading deletion propagates correctly to Normal1. And deleting Normal1 also works with no effects on the poly instances. """ from .models import ( Normal1, Poly1, A1, B1, C1, ) p1 = Poly1.objects.create() a1 = A1.objects.create() b1 = B1.objects.create() c1 = C1.objects.create() n1 = Normal1.objects.create(poly=p1) n2 = Normal1.objects.create(poly=c1) n3 = Normal1.objects.create(poly=a1) n4 = Normal1.objects.create(poly=b1) n5 = Normal1.objects.create(poly=p1) n6 = Normal1.objects.create(poly=b1) n7 = Normal1.objects.create(poly=b1) n8 = Normal1.objects.create(poly=c1) # test delete from parent Poly1.objects.filter(pk=a1.pk).delete() assert Normal1.objects.count() == 7 assert n3 not in Normal1.objects.all() assert A1.objects.count() == 0 Poly1.objects.filter(pk=p1.pk).delete() assert Normal1.objects.count() == 5 assert n1 not in Normal1.objects.all() assert n5 not in Normal1.objects.all() n6.delete() assert Normal1.objects.count() == 4 assert B1.objects.count() == 1 assert C1.objects.count() == 1 assert b1 in Poly1.objects.all() assert c1 in Poly1.objects.all() Poly1.objects.all().delete() assert Normal1.objects.count() == 0 assert Poly1.objects.count() == 0 def test_polymorphic_deletion_scenario2(self): """ Test the second polymorphic deletion scenario: A polymorphic model holds a foreign key to a base model with several children. -- cascade --> Normal2 <----- FK ---- Poly2 / | \ A2 B2 C2 Tests that when you delete a normal instance all related poly instances cascade correctly regardless of their concrete type in the hierarchy. This is the major collector failure mode. """ from .models import ( Normal2, Poly2, A2, B2, C2, ) n1, n2, n3, n4 = ( Normal2.objects.create(), Normal2.objects.create(), Normal2.objects.create(), Normal2.objects.create(), ) p1, p2 = Poly2.objects.create(normal=n1), Poly2.objects.create(normal=n4) a1, a2 = A2.objects.create(normal=n2), A2.objects.create(normal=n3) b1, b2 = B2.objects.create(normal=n4), B2.objects.create(normal=n2) c1, c2 = C2.objects.create(normal=n3), C2.objects.create(normal=n1) assert set(n1.polies.all()) == {p1, c2} assert set(n2.polies.all()) == {a1, b2} assert set(n3.polies.all()) == {a2, c1} assert set(n4.polies.all()) == {p2, b1} n2.delete() assert Poly2.objects.count() == 6 assert a1 not in Poly2.objects.all() assert b2 not in Poly2.objects.all() Normal2.objects.filter(pk__in=[n1.pk, n4.pk]).delete() assert Poly2.objects.count() == 2 assert p1 not in Poly2.objects.all() assert c2 not in Poly2.objects.all() assert p2 not in Poly2.objects.all() assert b1 not in Poly2.objects.all() n3.delete() assert Poly2.objects.count() == 0 def test_polymorphic_deletion_scenario3(self): """ Scenario 3 Normal3 | Poly3 | \ A3 B3 Deleting Poly3 should cascade delete Normal3 and deleting from Normal3 should cascade down to children. """ from .models import ( Normal3, Poly3, A3, B3, ) b1 = B3.objects.create() assert b1 in Poly3.objects.all() Normal3.objects.filter(pk=b1.pk).delete() assert b1 not in Poly3.objects.all() assert Poly3.objects.count() == 0 b2 = B3.objects.create() assert Normal3.objects.filter(pk=b2.pk).exists() assert b2 in Poly3.objects.all() b2.delete() assert not Normal3.objects.filter(pk=b2.pk).exists() assert Poly3.objects.count() == 0 def test_polymorphic_deletion_scenario4(self): """ Scenario 4 - M2Ms between normal/poly models <--- cascade ---> Normal4 <----- M2M -----> Poly4 / | \ A4 B4 C4 Ensure relations are appropriately cascaded on deletions from either side. """ from .models import ( Normal4, Poly4, A4, B4, C4, ) n1, n2, n3, n4 = ( Normal4.objects.create(), Normal4.objects.create(), Normal4.objects.create(), Normal4.objects.create(), ) p1, p2 = Poly4.objects.create(), Poly4.objects.create() a1, a2 = A4.objects.create(), A4.objects.create() b1, b2 = B4.objects.create(), B4.objects.create() c1, c2 = C4.objects.create(), C4.objects.create() n1.polies.add(p1, a1, b1, c1) n2.polies.add(p2, a2, b2, c2) n3.polies.add(b1, c1) n4.polies.add(p2, c2) assert set(n1.polies.all()) == {p1, a1, b1, c1} assert set(n2.polies.all()) == {p2, a2, b2, c2} assert set(n3.polies.all()) == {b1, c1} assert set(n4.polies.all()) == {p2, c2} a1.delete() assert set(n1.polies.all()) == {p1, b1, c1} assert set(n2.polies.all()) == {p2, a2, b2, c2} assert set(n3.polies.all()) == {b1, c1} assert set(n4.polies.all()) == {p2, c2} n4.delete() assert set(n1.polies.all()) == {p1, b1, c1} assert set(n2.polies.all()) == {p2, a2, b2, c2} assert set(n3.polies.all()) == {b1, c1} assert set(p2.normals.all()) == {n2} assert set(c2.normals.all()) == {n2} Poly4.objects.all().delete() assert n1.polies.count() == 0 assert n2.polies.count() == 0 assert n3.polies.count() == 0 def test_polymorphic_deletion_scenario4_1(self): """ Scenario 4 - M2Ms between normal/poly models <--- cascade ---> Normal4_1 <----- M2M -----> Poly4 / | \ A4 B4 C4 Ensure relations are appropriately cascaded on deletions from either side. """ from .models import ( Normal4_1, Poly4_1, A4_1, B4_1, C4_1, ) n1, n2, n3, n4 = ( Normal4_1.objects.create(), Normal4_1.objects.create(), Normal4_1.objects.create(), Normal4_1.objects.create(), ) p1, p2 = Poly4_1.objects.create(), Poly4_1.objects.create() a1, a2 = A4_1.objects.create(), A4_1.objects.create() b1, b2 = B4_1.objects.create(), B4_1.objects.create() c1, c2 = C4_1.objects.create(), C4_1.objects.create() n1.polies.add(p1, a1, b1, c1) n2.polies.add(p2, a2, b2, c2) n3.polies.add(b1, c1) n4.polies.add(p2, c2) assert set(n1.polies.all()) == {p1, a1, b1, c1} assert set(n2.polies.all()) == {p2, a2, b2, c2} assert set(n3.polies.all()) == {b1, c1} assert set(n4.polies.all()) == {p2, c2} a1.delete() assert set(n1.polies.all()) == {p1, b1, c1} assert set(n2.polies.all()) == {p2, a2, b2, c2} assert set(n3.polies.all()) == {b1, c1} assert set(n4.polies.all()) == {p2, c2} n4.delete() assert set(n1.polies.all()) == {p1, b1, c1} assert set(n2.polies.all()) == {p2, a2, b2, c2} assert set(n3.polies.all()) == {b1, c1} assert set(p2.normals.all()) == {n2} assert set(c2.normals.all()) == {n2} Poly4_1.objects.all().delete() assert n1.polies.count() == 0 assert n2.polies.count() == 0 assert n3.polies.count() == 0 def test_polymorphic_deletion_scenario5(self): """ Scenario 5 - scenario3 with custom/different PKs Normal5 | Poly5 | \ A5 B5 Deleting Poly5 should cascade delete Normal5 and deleting from Normal5 should cascade down to children. """ from .models import ( Normal5, Poly5, A5, B5, ) b1 = B5.objects.create() assert b1 in Poly5.objects.all() Normal5.objects.filter(pk=b1.pk).delete() assert b1 not in Poly5.objects.all() assert Poly5.objects.count() == 0 b2 = B5.objects.create() assert Normal5.objects.filter(pk=b2.pk).exists() assert b2 in Poly5.objects.all() b2.delete() assert not Normal5.objects.filter(pk=b2.pk).exists() assert Poly5.objects.count() == 0 # FIXME: django-polymorphic assumes all rows share the same PK value # n = Normal5.objects.create(n_pk=100) # p = Poly5.objects.create_from_super(n, p_pk=200) # A5.objects.create_from_super(p, a_pk=300) # b = B5.objects.create_from_super(p, b_pk=400) # assert Poly5.objects.count() == 1 # assert b in Poly5.objects.all() # Normal5.objects.filter(pk=n.pk).delete() # assert Poly5.objects.count() == 0 # n1 = Normal5.objects.create(n_pk=101) # p1 = Poly5.objects.create_from_super(n1, p_pk=201) # A5.objects.create_from_super(p1, a_pk=301) # b1 = B5.objects.create_from_super(p1, b_pk=401) # assert Poly5.objects.count() == 1 # assert b1 in Poly5.objects.all() # b1.delete() # assert Poly5.objects.count() == 0 # assert Normal5.objects.count() == 0 def test_raw_delete_results(self): """ Test what happens when you delete a child row with raw SQL then try to access polymorphic objects. With best effort approach, when a polymorphic_ctype_id points to a non-existing derived row, the parent object is returned instead of being filtered out. """ from .models import Poly1, A1 a1 = A1.objects.create(some_data="test") p1 = Poly1.objects.non_polymorphic().get(pk=a1.pk) p2 = Poly1.objects.create() with connection.cursor() as cursor: cursor.execute( f"DELETE FROM {A1._meta.db_table} WHERE {A1._meta.pk.column} = %s", [a1.pk] ) # Best effort: parent object is returned when child is deleted via raw SQL result = list(Poly1.objects.all()) assert len(result) == 2 assert p2 in result # p1 is returned as Poly1 (parent) since A1 (child) was deleted assert any( obj.pk == p1.pk and isinstance(obj, Poly1) and not isinstance(obj, A1) for obj in result ) assert set(Poly1.objects.non_polymorphic().all()) == {p1, p2} p1_fetched = Poly1.objects.non_polymorphic().get(pk=a1.pk) assert p1_fetched.get_real_instance().__class__ is Poly1 def test_delete_keep_parents(self): """ Test that delete(keep_parents=True) works as expected in polymorphic models by updating the relevant parent row ctypes. """ from .models import Poly3, A3, B3, Normal3 a1 = A3.objects.create() b1 = B3.objects.create() p1 = Poly3.objects.create() Normal3.objects.create() a1_pk = a1.pk b1_pk = b1.pk p1_pk = p1.pk a1.delete(keep_parents=True) assert A3.objects.count() == 0 assert B3.objects.count() == 1 assert Poly3.objects.count() == 3 assert Normal3.objects.count() == 4 assert Poly3.objects.get(pk=a1_pk).__class__ is Poly3 p1.delete(keep_parents=True) assert A3.objects.count() == 0 assert B3.objects.count() == 1 assert Poly3.objects.count() == 2 assert Normal3.objects.count() == 4 assert Normal3.objects.get(pk=p1_pk).__class__ is Normal3 # deleting an instance with more derived tables from a class higher up in its # hierarchy will delete all child rows below that level. b1_base = Poly3.objects.non_polymorphic().get(pk=b1_pk) b1_base.delete(keep_parents=True) assert A3.objects.count() == 0 assert B3.objects.count() == 0 assert Poly3.objects.count() == 1 assert Normal3.objects.count() == 4 assert Normal3.objects.get(pk=b1_pk).__class__ is Normal3 assert not Poly3.objects.filter(pk=b1_pk).exists() django-polymorphic-4.10.2/src/polymorphic/tests/errata/000077500000000000000000000000001513173623500231655ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/errata/__init__.py000066400000000000000000000000001513173623500252640ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/errata/migrations/000077500000000000000000000000001513173623500253415ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/errata/migrations/__init__.py000066400000000000000000000000001513173623500274400ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/errata/models.py000066400000000000000000000016121513173623500250220ustar00rootroot00000000000000from polymorphic.models import PolymorphicModel from polymorphic.managers import PolymorphicManager from django.db import models from django.db.models import Manager from django.db.models.query import QuerySet class BadModel(PolymorphicModel): instance_of = models.CharField(max_length=100) not_instance_of = models.IntegerField() class PolymorphicMigrationManager(PolymorphicManager): use_in_migrations = True class OkMigrationManager(Manager): use_in_migrations = True class GoodMigrationManager(PolymorphicModel): objects = PolymorphicManager() migration_manager = OkMigrationManager() class BadMigrationManager(PolymorphicModel): objects = PolymorphicMigrationManager() class BadManager(PolymorphicModel): objects = models.Manager() # not polymorphic class BadQuerySet(PolymorphicModel): default_objects = PolymorphicManager().from_queryset(QuerySet) django-polymorphic-4.10.2/src/polymorphic/tests/errata/settings.py000066400000000000000000000001441513173623500253760ustar00rootroot00000000000000from ..settings import * INSTALLED_APPS = [ *INSTALLED_APPS, "polymorphic.tests.errata", ] django-polymorphic-4.10.2/src/polymorphic/tests/errata/test_errata.py000066400000000000000000000053471513173623500260650ustar00rootroot00000000000000from django.core.checks import Error, run_checks from django.test.utils import override_settings from django.test import SimpleTestCase, TestCase from django.core.exceptions import FieldError @override_settings( INSTALLED_APPS=[ "polymorphic.tests.errata", "django.contrib.contenttypes", "django.contrib.auth", ] ) class TestErrata(SimpleTestCase): def test_system_checks(self): """Test that using reserved field names triggers polymorphic.E001 system check.""" # Run the check function directly on the model errors = run_checks() assert len(errors) == 5, f"Expected 12 system check errors but got {len(errors)}: {errors}" assert errors[0].id == "polymorphic.E001" assert errors[0].msg == "Field 'instance_of' on model 'BadModel' is a reserved name." assert errors[1].id == "polymorphic.E001" assert errors[1].msg == "Field 'not_instance_of' on model 'BadModel' is a reserved name." assert errors[2].id == "polymorphic.E002" assert ( errors[2].msg == "The migration manager 'errata.BadMigrationManager.objects' is polymorphic." ) assert errors[3].id == "polymorphic.W001" assert ( errors[3].msg == "The default manager errata.BadManager.objects' is not polymorphic." ) assert errors[4].id == "polymorphic.W002" assert ( errors[4].msg == "The default manager errata.BadManager.objects' is not using a PolymorphicQuerySet." ) def test_polymorphic_guard_requires_callable(self): """Test that PolymorphicGuard raises TypeError if initialized with non-callable.""" from polymorphic.deletion import PolymorphicGuard non_callable_values = [42, "not a function", None, 3.14, [], {}] for value in non_callable_values: try: PolymorphicGuard(value) except TypeError as e: assert str(e) == "action must be callable", ( f"Expected TypeError with message 'action must be callable' but got: {e}" ) else: assert False, f"Expected TypeError when initializing PolymorphicGuard with {value}" class TestFilterErrata(TestCase): def test_invalid_field_lookup_raises_field_error(self): from polymorphic.tests.models import Participant with self.assertRaises(FieldError): Participant.objects.get(tests__Model2C___field3="userprofile1") with self.assertRaises(FieldError): Participant.objects.get(notreal__Model2C___field3="userprofile1") with self.assertRaises(FieldError): Participant.objects.get(tests__NotReal___field3="userprofile1") django-polymorphic-4.10.2/src/polymorphic/tests/examples/000077500000000000000000000000001513173623500235255ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/__init__.py000066400000000000000000000000001513173623500256240ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/000077500000000000000000000000001513173623500262335ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/__init__.py000066400000000000000000000000001513173623500303320ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/apps.py000066400000000000000000000003121513173623500275440ustar00rootroot00000000000000from django.apps import AppConfig class IntegrationsExampleConfig(AppConfig): name = "polymorphic.tests.examples.integrations" label = "integrations" verbose_name = "Integration Examples" django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/000077500000000000000000000000001513173623500270065ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/__init__.py000066400000000000000000000000001513173623500311050ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/apps.py000066400000000000000000000002741513173623500303260ustar00rootroot00000000000000from django.apps import AppConfig class DRFExampleConfig(AppConfig): name = "polymorphic.tests.examples.integrations.drf" label = "drf_example" verbose_name = "DRF Examples" django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/example_serializers.py000066400000000000000000000016461513173623500334360ustar00rootroot00000000000000from rest_framework import serializers from polymorphic.contrib.drf.serializers import PolymorphicSerializer from .models import Project, ArtProject, ResearchProject class ProjectSerializer(serializers.ModelSerializer): class Meta: model = Project fields = ("topic",) class ArtProjectSerializer(serializers.ModelSerializer): class Meta: model = ArtProject fields = ("topic", "artist", "url") extra_kwargs = { "url": {"view_name": "drf:project-detail", "lookup_field": "pk"}, } class ResearchProjectSerializer(serializers.ModelSerializer): class Meta: model = ResearchProject fields = ("topic", "supervisor") class ProjectPolymorphicSerializer(PolymorphicSerializer): model_serializer_mapping = { Project: ProjectSerializer, ArtProject: ArtProjectSerializer, ResearchProject: ResearchProjectSerializer, } django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/filter_serializers.py000066400000000000000000000020121513173623500332540ustar00rootroot00000000000000from .models import AiModelAnnotator, UserAnnotator, Annotator, Data from rest_framework import serializers from polymorphic.contrib.drf.serializers import PolymorphicSerializer class AiModelAnnotatorSerializer(serializers.ModelSerializer): class Meta: model = AiModelAnnotator fields = "__all__" class UserAnnotatorSerializer(serializers.ModelSerializer): class Meta: model = UserAnnotator fields = "__all__" class AnnotatorSerializer(serializers.ModelSerializer): class Meta: model = Annotator fields = "__all__" class AnnotatorPolymorphicSerializer(PolymorphicSerializer): model_serializer_mapping = { Annotator: AnnotatorSerializer, AiModelAnnotator: AiModelAnnotatorSerializer, UserAnnotator: UserAnnotatorSerializer, } class AnnotationSerializer(serializers.ModelSerializer): annotator = serializers.PrimaryKeyRelatedField(queryset=Annotator.objects.all()) class Meta: model = Data fields = "__all__" django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/filter_views.py000066400000000000000000000020541513173623500320630ustar00rootroot00000000000000from .filter_serializers import AnnotationSerializer from .models import Data, AiModelAnnotator from rest_framework import viewsets, mixins from django_filters.rest_framework import DjangoFilterBackend import django_filters class DataFilterSet(django_filters.FilterSet): """FilterSet for Data model with polymorphic annotator filtering.""" annotator__ai_model = django_filters.CharFilter(method="filter_by_ai_model") class Meta: model = Data fields = ["annotator"] def filter_by_ai_model(self, queryset, name, value): return queryset.filter(annotator__in=AiModelAnnotator.objects.filter(ai_model=value)) class AnnotationTrainingViewSet( mixins.CreateModelMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet, ): queryset = Data.objects.all() serializer_class = AnnotationSerializer filter_backends = [DjangoFilterBackend] filterset_class = DataFilterSet # this does not work # filterset_fields = ["annotator", "annotator___AiModelAnnotator__ai_model"] django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/migrations/000077500000000000000000000000001513173623500311625ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/migrations/0001_initial.py000066400000000000000000000136731513173623500336370ustar00rootroot00000000000000# Generated by Django 5.2 on 2026-01-13 13:59 import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ('contenttypes', '0002_remove_content_type_name'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='Annotator', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Project', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('topic', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='BlogBase', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=10)), ('slug', models.SlugField(max_length=255, unique=True)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='AiModelAnnotator', fields=[ ('annotator_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='drf_example.annotator')), ('ai_model', models.CharField(max_length=255)), ('version', models.CharField(default=None, max_length=16, null=True)), ], options={ 'abstract': False, }, bases=('drf_example.annotator',), ), migrations.CreateModel( name='ArtProject', fields=[ ('project_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='drf_example.project')), ('artist', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('drf_example.project',), ), migrations.CreateModel( name='ResearchProject', fields=[ ('project_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='drf_example.project')), ('supervisor', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('drf_example.project',), ), migrations.CreateModel( name='BlogOne', fields=[ ('blogbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='drf_example.blogbase')), ('info', models.CharField(max_length=10)), ], options={ 'abstract': False, }, bases=('drf_example.blogbase',), ), migrations.CreateModel( name='BlogTwo', fields=[ ('blogbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='drf_example.blogbase')), ], options={ 'abstract': False, }, bases=('drf_example.blogbase',), ), migrations.CreateModel( name='Data', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('annotator', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='drf_example.annotator')), ], ), migrations.CreateModel( name='UserAnnotator', fields=[ ('annotator_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='drf_example.annotator')), ('user', models.ForeignKey(default=None, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), ], options={ 'abstract': False, }, bases=('drf_example.annotator',), ), migrations.CreateModel( name='BlogThree', fields=[ ('blogbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='drf_example.blogbase')), ('info', models.CharField(max_length=255)), ('about', models.CharField(max_length=255)), ], options={ 'unique_together': {('info', 'about')}, }, bases=('drf_example.blogbase',), ), ] django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/migrations/__init__.py000066400000000000000000000000001513173623500332610ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/models/000077500000000000000000000000001513173623500302715ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/models/__init__.py000066400000000000000000000006741513173623500324110ustar00rootroot00000000000000from .models_test import BlogBase, BlogOne, BlogTwo, BlogThree from .example_models import ( Project, ArtProject, ResearchProject, ) from .filters import ( Annotator, UserAnnotator, AiModelAnnotator, Data, ) __all__ = [ "BlogBase", "BlogOne", "BlogTwo", "BlogThree", "Project", "ArtProject", "ResearchProject", "Annotator", "UserAnnotator", "AiModelAnnotator", "Data", ] django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/models/example_models.py000066400000000000000000000004721513173623500336440ustar00rootroot00000000000000from django.db import models from polymorphic.models import PolymorphicModel class Project(PolymorphicModel): topic = models.CharField(max_length=30) class ArtProject(Project): artist = models.CharField(max_length=30) class ResearchProject(Project): supervisor = models.CharField(max_length=30) django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/models/filters.py000066400000000000000000000011451513173623500323140ustar00rootroot00000000000000""" https://github.com/jazzband/django-polymorphic/issues/520 """ from polymorphic.models import PolymorphicModel from django.db import models from django.contrib.auth import get_user_model class Annotator(PolymorphicModel): pass class UserAnnotator(Annotator): user = models.ForeignKey(get_user_model(), on_delete=models.PROTECT, default=None) class AiModelAnnotator(Annotator): ai_model = models.CharField(max_length=255) version = models.CharField(max_length=16, default=None, null=True) class Data(models.Model): annotator = models.ForeignKey(Annotator, on_delete=models.PROTECT) django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/models/models_test.py000066400000000000000000000007661513173623500331760ustar00rootroot00000000000000from django.db import models from polymorphic.models import PolymorphicModel class BlogBase(PolymorphicModel): name = models.CharField(max_length=10) slug = models.SlugField(max_length=255, unique=True) class BlogOne(BlogBase): info = models.CharField(max_length=10) class BlogTwo(BlogBase): pass class BlogThree(BlogBase): info = models.CharField(max_length=255) about = models.CharField(max_length=255) class Meta: unique_together = (("info", "about"),) django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/serializers.py000066400000000000000000000017071513173623500317210ustar00rootroot00000000000000from rest_framework import serializers from polymorphic.contrib.drf.serializers import PolymorphicSerializer from .models import BlogBase, BlogOne, BlogTwo, BlogThree class BlogBaseSerializer(serializers.ModelSerializer): class Meta: model = BlogBase fields = ("name", "slug") class BlogOneSerializer(serializers.ModelSerializer): class Meta: model = BlogOne fields = ("name", "slug", "info") class BlogTwoSerializer(serializers.ModelSerializer): class Meta: model = BlogTwo fields = ("name", "slug") class BlogThreeSerializer(serializers.ModelSerializer): class Meta: model = BlogThree fields = ("name", "slug", "info", "about") class BlogPolymorphicSerializer(PolymorphicSerializer): model_serializer_mapping = { BlogBase: BlogBaseSerializer, BlogOne: BlogOneSerializer, BlogTwo: BlogTwoSerializer, BlogThree: BlogThreeSerializer, } django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/test.py000066400000000000000000000626321513173623500303500ustar00rootroot00000000000000from django.core.exceptions import ImproperlyConfigured import pytest try: from rest_framework import serializers from rest_framework.test import APIClient from polymorphic.contrib.drf.serializers import PolymorphicSerializer from .serializers import ( BlogBaseSerializer, BlogOneSerializer, BlogPolymorphicSerializer, ) from .models import ( BlogBase, BlogOne, BlogTwo, Project, ArtProject, ResearchProject, ) except ImportError: pytest.skip("djangorestframework is not installed", allow_module_level=True) pytestmark = pytest.mark.django_db class TestPolymorphicSerializer: def test_model_serializer_mapping_is_none(self): class EmptyPolymorphicSerializer(PolymorphicSerializer): pass with pytest.raises(ImproperlyConfigured) as excinfo: EmptyPolymorphicSerializer() assert str(excinfo.value) == ( "`EmptyPolymorphicSerializer` is missing a " "`EmptyPolymorphicSerializer.model_serializer_mapping` attribute" ) def test_resource_type_field_name_is_not_string(self, mocker): class NotStringPolymorphicSerializer(PolymorphicSerializer): model_serializer_mapping = mocker.MagicMock resource_type_field_name = 1 with pytest.raises(ImproperlyConfigured) as excinfo: NotStringPolymorphicSerializer() assert str(excinfo.value) == ( "`NotStringPolymorphicSerializer.resource_type_field_name` must be a string" ) def test_each_serializer_has_context(self, mocker): context = mocker.MagicMock() serializer = BlogPolymorphicSerializer(context=context) for inner_serializer in serializer.model_serializer_mapping.values(): assert inner_serializer.context == context def test_non_callable_serializer_in_mapping(self): # Test the case where serializer is already instantiated (not callable) # This tests the else branch of the callable(serializer) check # Create an already-instantiated serializer blog_base_serializer_instance = BlogBaseSerializer() class TestPolymorphicSerializer(PolymorphicSerializer): model_serializer_mapping = { BlogBase: blog_base_serializer_instance, # Already an instance BlogOne: BlogOneSerializer, # Still a class (callable) } serializer = TestPolymorphicSerializer() # The instance should be used directly without re-instantiation assert serializer.model_serializer_mapping[BlogBase] is blog_base_serializer_instance # The callable should be instantiated assert isinstance(serializer.model_serializer_mapping[BlogOne], BlogOneSerializer) assert serializer.model_serializer_mapping[BlogOne] is not BlogOneSerializer # Both should be in resource_type_model_mapping assert serializer.resource_type_model_mapping["BlogBase"] == BlogBase assert serializer.resource_type_model_mapping["BlogOne"] == BlogOne # Now test that serialization actually works with the non-callable serializer base_instance = BlogBase.objects.create(name="base", slug="base-slug") one_instance = BlogOne.objects.create(name="one", slug="one-slug", info="info") # Serialize BlogBase (using the pre-instantiated serializer) base_serializer = TestPolymorphicSerializer(base_instance) base_data = base_serializer.data assert base_data == { "name": "base", "slug": "base-slug", "resourcetype": "BlogBase", } # Serialize BlogOne (using the callable serializer that was instantiated) one_serializer = TestPolymorphicSerializer(one_instance) one_data = one_serializer.data assert one_data == { "name": "one", "slug": "one-slug", "info": "info", "resourcetype": "BlogOne", } # Test serialization of multiple instances (many=True) instances = [base_instance, one_instance] many_serializer = TestPolymorphicSerializer(instances, many=True) many_data = many_serializer.data assert len(many_data) == 2 assert many_data[0]["resourcetype"] == "BlogBase" assert many_data[1]["resourcetype"] == "BlogOne" assert many_data[0]["name"] == "base" assert many_data[1]["name"] == "one" assert many_data[1]["info"] == "info" def test_serialize(self): instance = BlogBase.objects.create(name="blog", slug="blog") serializer = BlogPolymorphicSerializer(instance) assert serializer.data == { "name": "blog", "slug": "blog", "resourcetype": "BlogBase", } def test_deserialize(self): data = { "name": "blog", "slug": "blog", "resourcetype": "BlogBase", } serializers = BlogPolymorphicSerializer(data=data) assert serializers.is_valid() assert serializers.data == data def test_deserialize_with_invalid_resourcetype(self): data = { "name": "blog", "resourcetype": "Invalid", } serializers = BlogPolymorphicSerializer(data=data) assert not serializers.is_valid() def test_create(self): data = [ {"name": "a", "slug": "a", "resourcetype": "BlogBase"}, {"name": "b", "slug": "b", "info": "info", "resourcetype": "BlogOne"}, {"name": "c", "slug": "c", "resourcetype": "BlogTwo"}, ] serializer = BlogPolymorphicSerializer(data=data, many=True) assert serializer.is_valid() instances = serializer.save() assert len(instances) == 3 assert [item.name for item in instances] == ["a", "b", "c"] assert BlogBase.objects.count() == 3 assert BlogBase.objects.instance_of(BlogOne).count() == 1 assert BlogBase.objects.instance_of(BlogTwo).count() == 1 assert serializer.data == data def test_update(self): instance = BlogBase.objects.create(name="blog", slug="blog") data = {"name": "new-blog", "slug": "blog", "resourcetype": "BlogBase"} serializer = BlogPolymorphicSerializer(instance, data=data) assert serializer.is_valid() serializer.save() assert instance.name == "new-blog" assert instance.slug == "blog" def test_partial_update(self): instance = BlogBase.objects.create(name="blog", slug="blog") data = {"name": "new-blog", "resourcetype": "BlogBase"} serializer = BlogPolymorphicSerializer(instance, data=data, partial=True) assert serializer.is_valid() serializer.save() assert instance.name == "new-blog" assert instance.slug == "blog" def test_partial_update_without_resourcetype(self): instance = BlogBase.objects.create(name="blog", slug="blog") data = {"name": "new-blog"} serializer = BlogPolymorphicSerializer(instance, data=data, partial=True) assert serializer.is_valid() serializer.save() assert instance.name == "new-blog" assert instance.slug == "blog" def test_object_validators_are_applied(self): data = { "name": "test-blog", "slug": "test-blog-slug", "info": "test-blog-info", "about": "test-blog-about", "resourcetype": "BlogThree", } serializer = BlogPolymorphicSerializer(data=data) assert serializer.is_valid() serializer.save() data["slug"] = "test-blog-slug-new" duplicate = BlogPolymorphicSerializer(data=data) assert not duplicate.is_valid() assert "non_field_errors" in duplicate.errors err = duplicate.errors["non_field_errors"] assert err == ["The fields info, about must make a unique set."] def test_to_internal_value_with_valid_data(self): data = { "name": "blog", "slug": "blog", "resourcetype": "BlogBase", } serializer = BlogPolymorphicSerializer(data=data) internal_value = serializer.to_internal_value(data) assert internal_value["name"] == "blog" assert internal_value["slug"] == "blog" assert internal_value["resourcetype"] == "BlogBase" def test_to_internal_value_with_missing_resourcetype(self): from rest_framework.exceptions import ValidationError data = { "name": "blog", "slug": "blog", } serializer = BlogPolymorphicSerializer(data=data) with pytest.raises(ValidationError) as excinfo: serializer.to_internal_value(data) assert "resourcetype" in excinfo.value.detail assert excinfo.value.detail["resourcetype"] == "This field is required" def test_to_internal_value_with_partial_update(self): instance = BlogBase.objects.create(name="blog", slug="blog") data = {"name": "new-blog"} serializer = BlogPolymorphicSerializer(instance, data=data, partial=True) internal_value = serializer.to_internal_value(data) assert internal_value["name"] == "new-blog" assert internal_value["resourcetype"] == "BlogBase" def test_get_serializer_from_model_or_instance_raises_keyerror(self): from polymorphic.models import PolymorphicModel # Create a model that is not in the mapping class UnmappedModel(PolymorphicModel): class Meta: app_label = "drf" serializer = BlogPolymorphicSerializer() with pytest.raises(KeyError) as excinfo: serializer._get_serializer_from_model_or_instance(UnmappedModel) assert "model_serializer_mapping" in str(excinfo.value) assert "UnmappedModel" in str(excinfo.value) def test_get_serializer_from_resource_type_keyerror_propagation(self): # This tests the case where _get_serializer_from_resource_type # successfully finds a resource_type in the mapping, but then # _get_serializer_from_model_or_instance raises a KeyError # when trying to find the serializer for that model. # # However, looking at the code, this scenario is actually not possible # in normal operation because resource_type_model_mapping and # model_serializer_mapping are populated together in __init__. # # The KeyError in _get_serializer_from_resource_type would only # occur if the resource_type is not in resource_type_model_mapping, # which is already caught and converted to ValidationError at line 149. # # So we'll test that the ValidationError is raised properly instead. from rest_framework.exceptions import ValidationError data = { "name": "blog", "slug": "blog", "resourcetype": "InvalidResourceType", } serializer = BlogPolymorphicSerializer(data=data) with pytest.raises(ValidationError) as excinfo: serializer._get_serializer_from_resource_type("InvalidResourceType") assert "resourcetype" in excinfo.value.detail assert "Invalid resourcetype" in str(excinfo.value.detail["resourcetype"]) def test_validate_method_modifications_are_preserved(self): """Test that modifications made in child serializer's validate() method are preserved.""" # Track whether the extra_field was present during create created_with_extra_field = [] # Create a custom serializer that adds a field in validate() class CustomBlogOneSerializer(BlogOneSerializer): extra_field = serializers.CharField(required=False, allow_null=True) class Meta(BlogOneSerializer.Meta): fields = BlogOneSerializer.Meta.fields + ("extra_field",) def validate(self, attrs): attrs = super().validate(attrs) # Simulate adding data in validate(), like adding the current user attrs["extra_field"] = "added_in_validate" return attrs def create(self, validated_data): # Record whether extra_field was in validated_data created_with_extra_field.append("extra_field" in validated_data) # Remove extra_field before creating the model instance validated_data.pop("extra_field", None) return super().create(validated_data) class CustomBlogPolymorphicSerializer(PolymorphicSerializer): model_serializer_mapping = { BlogBase: BlogBaseSerializer, BlogOne: CustomBlogOneSerializer, } # Create data without the extra_field data = { "name": "test", "slug": "test-slug", "info": "test-info", "resourcetype": "BlogOne", } serializer = CustomBlogPolymorphicSerializer(data=data) assert serializer.is_valid(), f"Validation errors: {serializer.errors}" # Verify that the extra_field added in validate() is in validated_data assert "extra_field" in serializer.validated_data assert serializer.validated_data["extra_field"] == "added_in_validate" # Verify that resource_type field is still preserved in parent's validated_data assert "resourcetype" in serializer.validated_data assert serializer.validated_data["resourcetype"] == "BlogOne" # Save and verify that the field was present during create # Note: This would fail before the fix because the parent's _validated_data # wasn't updated with the child's _validated_data after calling child.is_valid() instance = serializer.save() # Verify that extra_field was indeed present when create() was called assert created_with_extra_field == [True], ( "extra_field should have been in validated_data when create() was called" ) # Verify the instance was created successfully assert instance.name == "test" assert instance.slug == "test-slug" assert instance.info == "test-info" class TestProjectViewSet: """Test the example Project ViewSet with polymorphic serializers.""" @pytest.fixture def client(self): return APIClient() @pytest.fixture def base_project(self): return Project.objects.create(topic="General Project") @pytest.fixture def art_project(self): return ArtProject.objects.create(topic="Art", artist="Picasso") @pytest.fixture def research_project(self): return ResearchProject.objects.create(topic="Research", supervisor="Dr. Smith") def test_list_projects(self, client, base_project, art_project, research_project): response = client.get("/examples/integrations/drf/projects/") assert response.status_code == 200 assert len(response.data) == 3 topics = {item["topic"] for item in response.data} assert topics == {"General Project", "Art", "Research"} def test_retrieve_base_project(self, client, base_project): response = client.get(f"/examples/integrations/drf/projects/{base_project.pk}/") assert response.status_code == 200 assert response.data["topic"] == "General Project" assert response.data["resourcetype"] == "Project" def test_retrieve_art_project(self, client, art_project): response = client.get(f"/examples/integrations/drf/projects/{art_project.pk}/") assert response.status_code == 200 assert response.data["topic"] == "Art" assert response.data["artist"] == "Picasso" assert response.data["resourcetype"] == "ArtProject" assert "url" in response.data def test_retrieve_research_project(self, client, research_project): response = client.get(f"/examples/integrations/drf/projects/{research_project.pk}/") assert response.status_code == 200 assert response.data["topic"] == "Research" assert response.data["supervisor"] == "Dr. Smith" assert response.data["resourcetype"] == "ResearchProject" def test_create_base_project(self, client): data = {"topic": "New Project", "resourcetype": "Project"} response = client.post("/examples/integrations/drf/projects/", data, format="json") assert response.status_code == 201 assert response.data["topic"] == "New Project" assert response.data["resourcetype"] == "Project" assert Project.objects.count() == 1 project = Project.objects.first() assert project.topic == "New Project" assert type(project) is Project def test_create_art_project(self, client): data = { "topic": "Sculpture", "artist": "Michelangelo", "resourcetype": "ArtProject", } response = client.post("/examples/integrations/drf/projects/", data, format="json") assert response.status_code == 201 assert response.data["topic"] == "Sculpture" assert response.data["artist"] == "Michelangelo" assert response.data["resourcetype"] == "ArtProject" assert "url" in response.data assert Project.objects.count() == 1 project = Project.objects.first() assert isinstance(project, ArtProject) assert project.artist == "Michelangelo" def test_create_research_project(self, client): data = { "topic": "AI Research", "supervisor": "Dr. Johnson", "resourcetype": "ResearchProject", } response = client.post("/examples/integrations/drf/projects/", data, format="json") assert response.status_code == 201 assert response.data["topic"] == "AI Research" assert response.data["supervisor"] == "Dr. Johnson" assert response.data["resourcetype"] == "ResearchProject" assert Project.objects.count() == 1 project = Project.objects.first() assert isinstance(project, ResearchProject) assert project.supervisor == "Dr. Johnson" def test_update_project(self, client, base_project): data = {"topic": "Updated Project", "resourcetype": "Project"} response = client.put( f"/examples/integrations/drf/projects/{base_project.pk}/", data, format="json" ) assert response.status_code == 200 assert response.data["topic"] == "Updated Project" base_project.refresh_from_db() assert base_project.topic == "Updated Project" def test_partial_update_art_project(self, client, art_project): data = {"artist": "Van Gogh"} response = client.patch( f"/examples/integrations/drf/projects/{art_project.pk}/", data, format="json" ) assert response.status_code == 200 assert response.data["artist"] == "Van Gogh" assert response.data["topic"] == "Art" # unchanged art_project.refresh_from_db() assert art_project.artist == "Van Gogh" assert art_project.topic == "Art" def test_partial_update_research_project(self, client, research_project): data = {"supervisor": "Dr. Williams"} response = client.patch( f"/examples/integrations/drf/projects/{research_project.pk}/", data, format="json" ) assert response.status_code == 200 assert response.data["supervisor"] == "Dr. Williams" assert response.data["topic"] == "Research" # unchanged research_project.refresh_from_db() assert research_project.supervisor == "Dr. Williams" assert research_project.topic == "Research" def test_delete_project(self, client, base_project): project_id = base_project.pk response = client.delete(f"/examples/integrations/drf/projects/{project_id}/") assert response.status_code == 204 assert not Project.objects.filter(pk=project_id).exists() def test_create_with_invalid_resourcetype(self, client): data = {"topic": "Test", "resourcetype": "InvalidType"} response = client.post("/examples/integrations/drf/projects/", data, format="json") assert response.status_code == 400 class TestDjangoFiltersViewSet: """Test django-filter integration with polymorphic models (issue #520).""" @pytest.fixture def client(self): return APIClient() @pytest.fixture def user(self, django_user_model): return django_user_model.objects.create_user(username="testuser", password="testpass") @pytest.fixture def user_annotator(self, user): from .models import UserAnnotator return UserAnnotator.objects.create(user=user) @pytest.fixture def ai_annotator_gpt4(self): from .models import AiModelAnnotator return AiModelAnnotator.objects.create(ai_model="gpt-4", version="1.0") @pytest.fixture def ai_annotator_claude(self): from .models import AiModelAnnotator return AiModelAnnotator.objects.create(ai_model="claude-3", version="2.0") @pytest.fixture def data_by_user(self, user_annotator): from .models import Data return Data.objects.create(annotator=user_annotator) @pytest.fixture def data_by_gpt4(self, ai_annotator_gpt4): from .models import Data return Data.objects.create(annotator=ai_annotator_gpt4) @pytest.fixture def data_by_claude(self, ai_annotator_claude): from .models import Data return Data.objects.create(annotator=ai_annotator_claude) def test_list_all_annotations(self, client, data_by_user, data_by_gpt4, data_by_claude): """Test listing all annotation data without filters.""" response = client.get("/examples/integrations/drf/annotations/") assert response.status_code == 200 assert len(response.data) == 3 def test_filter_by_annotator(self, client, data_by_user, data_by_gpt4, ai_annotator_gpt4): """Test filtering by annotator ID.""" response = client.get( f"/examples/integrations/drf/annotations/?annotator={ai_annotator_gpt4.pk}" ) assert response.status_code == 200 assert len(response.data) == 1 assert response.data[0]["id"] == data_by_gpt4.pk def test_filter_by_ai_model(self, client, data_by_user, data_by_gpt4, data_by_claude): """Test filtering by annotator__ai_model field (issue #520).""" # This is the key test - filtering by a field on the polymorphic child model response = client.get("/examples/integrations/drf/annotations/?annotator__ai_model=gpt-4") assert response.status_code == 200 assert len(response.data) == 1 assert response.data[0]["id"] == data_by_gpt4.pk def test_filter_by_different_ai_model( self, client, data_by_user, data_by_gpt4, data_by_claude ): """Test filtering by a different AI model.""" response = client.get( "/examples/integrations/drf/annotations/?annotator__ai_model=claude-3" ) assert response.status_code == 200 assert len(response.data) == 1 assert response.data[0]["id"] == data_by_claude.pk def test_filter_by_nonexistent_ai_model( self, client, data_by_user, data_by_gpt4, data_by_claude ): """Test filtering by an AI model that doesn't exist.""" response = client.get( "/examples/integrations/drf/annotations/?annotator__ai_model=nonexistent" ) assert response.status_code == 200 assert len(response.data) == 0 def test_filter_excludes_non_ai_annotators( self, client, data_by_user, data_by_gpt4, data_by_claude ): """Test that filtering by ai_model excludes UserAnnotator instances.""" # When filtering by annotator__ai_model, only AiModelAnnotator results should be returned # UserAnnotator doesn't have ai_model field, so data_by_user should not appear response = client.get("/examples/integrations/drf/annotations/?annotator__ai_model=gpt-4") assert response.status_code == 200 assert len(response.data) == 1 # Verify the user-annotated data is not in results assert all(item["id"] != data_by_user.pk for item in response.data) def test_retrieve_annotation(self, client, data_by_gpt4): """Test retrieving a single annotation.""" response = client.get(f"/examples/integrations/drf/annotations/{data_by_gpt4.pk}/") assert response.status_code == 200 assert response.data["id"] == data_by_gpt4.pk def test_create_annotation_with_user_annotator(self, client, user_annotator): """Test creating annotation data with a UserAnnotator.""" data = {"annotator": user_annotator.pk} response = client.post("/examples/integrations/drf/annotations/", data, format="json") assert response.status_code == 201 assert response.data["annotator"] == user_annotator.pk from .models import Data assert Data.objects.count() == 1 created = Data.objects.first() assert created.annotator.pk == user_annotator.pk def test_create_annotation_with_ai_annotator(self, client, ai_annotator_gpt4): """Test creating annotation data with an AiModelAnnotator.""" data = {"annotator": ai_annotator_gpt4.pk} response = client.post("/examples/integrations/drf/annotations/", data, format="json") assert response.status_code == 201 assert response.data["annotator"] == ai_annotator_gpt4.pk from .models import Data assert Data.objects.count() == 1 created = Data.objects.first() assert created.annotator.pk == ai_annotator_gpt4.pk django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/urls.py000066400000000000000000000004671513173623500303540ustar00rootroot00000000000000from rest_framework.routers import DefaultRouter from .views import ProjectViewSet from .filter_views import AnnotationTrainingViewSet app_name = "drf" router = DefaultRouter() router.register(r"projects", ProjectViewSet) router.register(r"annotations", AnnotationTrainingViewSet) urlpatterns = router.urls django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/drf/views.py000066400000000000000000000004071513173623500305160ustar00rootroot00000000000000from rest_framework import viewsets from .models import Project from .example_serializers import ProjectPolymorphicSerializer class ProjectViewSet(viewsets.ModelViewSet): queryset = Project.objects.all() serializer_class = ProjectPolymorphicSerializer django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/extra_views/000077500000000000000000000000001513173623500305735ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/extra_views/__init__.py000066400000000000000000000000001513173623500326720ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/extra_views/apps.py000066400000000000000000000003451513173623500321120ustar00rootroot00000000000000from django.apps import AppConfig class ExtraViewsExampleConfig(AppConfig): name = "polymorphic.tests.examples.integrations.extra_views" label = "extra_views" verbose_name = "django-extra-views Integration Example" django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/extra_views/templates/000077500000000000000000000000001513173623500325715ustar00rootroot00000000000000extra_views/000077500000000000000000000000001513173623500350525ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/extra_views/templatesarticle_formset.html000066400000000000000000000007621513173623500411270ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/extra_views/templates/extra_views{% load extra_views_tags %} Article Formset

    Article Formset

    {% csrf_token %} {{ formset.management_form }} {% for form in formset %}

    {{ form.instance|model_name }}

    {{ form.as_p }}
    {% endfor %}
    django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/extra_views/templatetags/000077500000000000000000000000001513173623500332655ustar00rootroot00000000000000__init__.py000066400000000000000000000000001513173623500353050ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/extra_views/templatetagsextra_views_tags.py000066400000000000000000000003121513173623500371320ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/extra_views/templatetagsfrom django import template register = template.Library() @register.filter def model_name(instance): """Get the model class name of an instance.""" return instance._meta.verbose_name.title() django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/extra_views/test.py000066400000000000000000000541301513173623500321270ustar00rootroot00000000000000from unittest import skipUnless from django.contrib.contenttypes.models import ContentType from django.test import TestCase, override_settings from django.urls import reverse from playwright.sync_api import expect try: import extra_views EXTRA_VIEWS_INSTALLED = True except ImportError: EXTRA_VIEWS_INSTALLED = False from ..models import Article, BlogPost, NewsArticle from polymorphic.tests.utils import _GenericUITest @skipUnless(EXTRA_VIEWS_INSTALLED, "django-extra-views is not installed") class ExtraViewsIntegrationTests(TestCase): """ Tests for django-extra-views integration with polymorphic models. These tests verify that: 1. PolymorphicFormSetView works with polymorphic models 2. Formsets can create and update polymorphic child instances 3. Child forms are correctly rendered for different model types """ def test_formset_children_configuration(self): """Test that formset children are properly configured.""" from .views import ArticleFormSetView view = ArticleFormSetView() children = view.get_formset_children() self.assertEqual(len(children), 2) self.assertEqual(children[0].model, BlogPost) self.assertEqual(children[1].model, NewsArticle) def test_formset_generation(self): """Test that the formset is properly generated with child forms.""" from .views import ArticleFormSetView view = ArticleFormSetView() formset_class = view.get_formset() # Verify child_forms are attached self.assertTrue(hasattr(formset_class, "child_forms")) self.assertEqual(len(formset_class.child_forms), 2) self.assertIn(BlogPost, formset_class.child_forms) self.assertIn(NewsArticle, formset_class.child_forms) def test_formset_with_existing_objects(self): """Test formset with existing polymorphic objects.""" # Create test objects blog_post = BlogPost.objects.create( title="Test Blog", content="Blog content", author="Blog Author" ) news_article = NewsArticle.objects.create( title="Test News", content="News content", source="News Source" ) from .views import ArticleFormSetView view = ArticleFormSetView() formset_class = view.get_formset() # Create formset instance with queryset formset = formset_class(queryset=Article.objects.all()) # Verify formset contains both objects self.assertEqual(len(formset.forms), 4) # Verify polymorphic types are preserved form_models = [form.instance.__class__ for form in formset.forms] self.assertIn(BlogPost, form_models) self.assertIn(NewsArticle, form_models) def test_formset_extra_forms_configuration(self): """Test that formset generates correct extra forms when empty.""" from .views import ArticleFormSetView # Ensure no objects exist Article.objects.all().delete() view = ArticleFormSetView() formset_class = view.get_formset() # Create empty formset formset = formset_class(queryset=Article.objects.none()) # Verify we have 2 extra forms (as configured in factory_kwargs) self.assertEqual(len(formset.forms), 2) # Verify the forms cycle through child types # Form 0 should be BlogPost (first child) self.assertEqual(formset.forms[0]._meta.model, BlogPost) # Form 1 should be NewsArticle (second child) self.assertEqual(formset.forms[1]._meta.model, NewsArticle) def test_formset_saving_new_objects(self): """Test creating new polymorphic objects through formset.""" from .views import ArticleFormSetView view = ArticleFormSetView() formset_class = view.get_formset() # Create formset with POST data for new objects data = { "form-TOTAL_FORMS": "2", "form-INITIAL_FORMS": "0", "form-MIN_NUM_FORMS": "0", "form-MAX_NUM_FORMS": "1000", # BlogPost "form-0-polymorphic_ctype": str( ContentType.objects.get_for_model(BlogPost, for_concrete_model=False).pk ), "form-0-title": "New Blog Post", "form-0-content": "Blog post content", "form-0-author": "Blog Author", # NewsArticle "form-1-polymorphic_ctype": str( ContentType.objects.get_for_model(NewsArticle, for_concrete_model=False).pk ), "form-1-title": "New News Article", "form-1-content": "News article content", "form-1-source": "News Source", } formset = formset_class(data) # Verify formset is valid self.assertTrue(formset.is_valid(), formset.errors) # Save the formset instances = formset.save() # Verify objects were created self.assertEqual(len(instances), 2) self.assertEqual(Article.objects.count(), 2) # Verify polymorphic types blog_post = BlogPost.objects.get(title="New Blog Post") self.assertEqual(blog_post.author, "Blog Author") news_article = NewsArticle.objects.get(title="New News Article") self.assertEqual(news_article.source, "News Source") def test_formset_updating_objects(self): """Test updating existing polymorphic objects through formset.""" # Create existing objects blog_post = BlogPost.objects.create( title="Original Blog", content="Original content", author="Original Author" ) from .views import ArticleFormSetView view = ArticleFormSetView() formset_class = view.get_formset() # Create formset with update data data = { "form-TOTAL_FORMS": "1", "form-INITIAL_FORMS": "1", "form-MIN_NUM_FORMS": "0", "form-MAX_NUM_FORMS": "1000", "form-0-id": str(blog_post.pk), "form-0-polymorphic_ctype": str( ContentType.objects.get_for_model(BlogPost, for_concrete_model=False).pk ), "form-0-title": "Updated Blog", "form-0-content": "Updated content", "form-0-author": "Updated Author", } formset = formset_class(data, queryset=Article.objects.all()) # Verify formset is valid self.assertTrue(formset.is_valid(), formset.errors) # Save the formset formset.save() # Verify object was updated blog_post.refresh_from_db() self.assertEqual(blog_post.title, "Updated Blog") self.assertEqual(blog_post.author, "Updated Author") def test_formset_deleting_objects(self): """Test deleting polymorphic objects through formset.""" # Create object to delete blog_post = BlogPost.objects.create(title="To Delete", content="Content", author="Author") from .views import ArticleFormSetView view = ArticleFormSetView() formset_class = view.get_formset() # Create formset with delete data data = { "form-TOTAL_FORMS": "1", "form-INITIAL_FORMS": "1", "form-MIN_NUM_FORMS": "0", "form-MAX_NUM_FORMS": "1000", "form-0-id": str(blog_post.pk), "form-0-polymorphic_ctype": str( ContentType.objects.get_for_model(BlogPost, for_concrete_model=False).pk ), "form-0-title": blog_post.title, "form-0-content": blog_post.content, "form-0-author": blog_post.author, "form-0-DELETE": "on", } formset = formset_class(data, queryset=Article.objects.all()) # Verify formset is valid self.assertTrue(formset.is_valid(), formset.errors) # Save the formset formset.save() # Verify object was deleted self.assertFalse(BlogPost.objects.filter(pk=blog_post.pk).exists()) def test_formset_mixed_operations(self): """Test creating, updating, and deleting in single formset submission.""" # Create existing object to update blog_post = BlogPost.objects.create( title="Existing Blog", content="Existing content", author="Existing Author" ) # Create object to delete news_to_delete = NewsArticle.objects.create( title="To Delete", content="Delete content", source="Delete Source" ) from .views import ArticleFormSetView view = ArticleFormSetView() formset_class = view.get_formset() data = { "form-TOTAL_FORMS": "3", "form-INITIAL_FORMS": "2", "form-MIN_NUM_FORMS": "0", "form-MAX_NUM_FORMS": "1000", # Update existing blog post "form-0-id": str(blog_post.pk), "form-0-polymorphic_ctype": str( ContentType.objects.get_for_model(BlogPost, for_concrete_model=False).pk ), "form-0-title": "Updated Existing Blog", "form-0-content": "Updated content", "form-0-author": "Updated Author", # Delete news article "form-1-id": str(news_to_delete.pk), "form-1-polymorphic_ctype": str( ContentType.objects.get_for_model(NewsArticle, for_concrete_model=False).pk ), "form-1-title": news_to_delete.title, "form-1-content": news_to_delete.content, "form-1-source": news_to_delete.source, "form-1-DELETE": "on", # Create new blog post "form-2-polymorphic_ctype": str( ContentType.objects.get_for_model(BlogPost, for_concrete_model=False).pk ), "form-2-title": "New Blog Post", "form-2-content": "New content", "form-2-author": "New Author", } formset = formset_class(data, queryset=Article.objects.all()) self.assertTrue(formset.is_valid(), formset.errors) formset.save() # Verify update blog_post.refresh_from_db() self.assertEqual(blog_post.title, "Updated Existing Blog") # Verify deletion self.assertFalse(NewsArticle.objects.filter(pk=news_to_delete.pk).exists()) # Verify creation new_blog = BlogPost.objects.get(title="New Blog Post") self.assertEqual(new_blog.author, "New Author") # Verify total count self.assertEqual(Article.objects.count(), 2) @skipUnless(EXTRA_VIEWS_INSTALLED, "django-extra-views is not installed") class ExtraViewsUITests(TestCase): """Test django-extra-views functionality through the Client API.""" def test_formset_view_get_request(self): """Test that the formset view handles GET requests correctly.""" from .views import ArticleFormSetView # Create some existing objects BlogPost.objects.create(title="Existing Blog", content="Content", author="Author") NewsArticle.objects.create(title="Existing News", content="Content", source="Source") view = ArticleFormSetView.as_view() from django.test import RequestFactory factory = RequestFactory() request = factory.get("/articles/") response = view(request) response.render() # Verify response self.assertEqual(response.status_code, 200) self.assertIn(b"Article Formset", response.content) def test_formset_view_post_request(self): """Test creating new polymorphic objects through POST request.""" from .views import ArticleFormSetView view = ArticleFormSetView.as_view() from django.test import RequestFactory factory = RequestFactory() # Create POST data data = { "form-TOTAL_FORMS": "1", "form-INITIAL_FORMS": "0", "form-MIN_NUM_FORMS": "0", "form-MAX_NUM_FORMS": "1000", "form-0-polymorphic_ctype": str( ContentType.objects.get_for_model(BlogPost, for_concrete_model=False).pk ), "form-0-title": "New Blog Post", "form-0-content": "Blog content", "form-0-author": "Author Name", } request = factory.post("/articles/", data) response = view(request) # Verify redirect on success self.assertEqual(response.status_code, 302) # Verify object was created blog_post = BlogPost.objects.get(title="New Blog Post") self.assertEqual(blog_post.author, "Author Name") @skipUnless(EXTRA_VIEWS_INSTALLED, "django-extra-views is not installed") class ExtraViewsLiveServerTests(_GenericUITest): """Test django-extra-views functionality through the live test server.""" def setUp(self): """Create a page without admin login (not needed for formset view).""" self.page = self.browser.new_page() def tearDown(self): """Close the page after each test.""" if self.page: self.page.close() def test_formset_view_renders_existing_objects(self): """Test that existing polymorphic objects are rendered in the formset.""" # Create existing objects blog_post = BlogPost.objects.create( title="Existing Blog", content="Blog content", author="Blog Author" ) news_article = NewsArticle.objects.create( title="Existing News", content="News content", source="News Source" ) # Navigate to formset view url = f"{self.live_server_url}{reverse('extra_views:articles')}" self.page.goto(url) # Verify page loaded expect(self.page.locator("h1")).to_contain_text("Article Formset") # Verify form is rendered form = self.page.locator("form") expect(form).to_be_visible() # Verify existing objects are shown in the formset # The formset should have inputs for the existing objects expect(self.page.locator(f"input[value='{blog_post.title}']")).to_be_visible() expect(self.page.locator(f"input[value='{news_article.title}']")).to_be_visible() def test_formset_update_existing_object(self): """Test updating an existing object through the formset.""" # Create an object to update blog_post = BlogPost.objects.create( title="Original Title", content="Original content", author="Original Author" ) # Navigate to formset view url = f"{self.live_server_url}{reverse('extra_views:articles')}" self.page.goto(url) # Find the title input for this blog post # The form should have a hidden id field and visible title field title_input = self.page.locator(f"input[value='{blog_post.title}']").first title_input.fill("Updated Title") # Find and update the author field # The author field should be in the same form container author_input = self.page.locator(f"input[value='{blog_post.author}']").first author_input.fill("Updated Author") # Submit the form with self.page.expect_navigation(timeout=30000): self.page.locator("button[type='submit']").click() # Verify the object was updated blog_post.refresh_from_db() self.assertEqual(blog_post.title, "Updated Title") self.assertEqual(blog_post.author, "Updated Author") def test_formset_delete_existing_object(self): """Test deleting an existing object through the formset.""" # Create an object to delete blog_post = BlogPost.objects.create( title="To Delete", content="Delete content", author="Delete Author" ) blog_post_id = blog_post.id # Navigate to formset view url = f"{self.live_server_url}{reverse('extra_views:articles')}" self.page.goto(url) # Find the DELETE checkbox for this object # Django formsets add a DELETE checkbox for each form when can_delete=True delete_checkbox = self.page.locator("input[type='checkbox'][name*='DELETE']").first # Check if the checkbox is visible, if not we need to handle it differently if delete_checkbox.is_visible(): delete_checkbox.check() # Submit the form with self.page.expect_navigation(timeout=30000): self.page.locator("button[type='submit']").click() # Verify the object was deleted self.assertFalse(BlogPost.objects.filter(id=blog_post_id).exists()) else: # If DELETE checkboxes aren't visible, skip this test self.skipTest("DELETE checkboxes not visible in UI") def test_formset_displays_multiple_polymorphic_types(self): """Test that the formset correctly displays forms for different polymorphic types.""" # Create instances of both child models blog_post = BlogPost.objects.create( title="Blog Post", content="Blog content", author="Blog Author" ) news_article = NewsArticle.objects.create( title="News Article", content="News content", source="News Source" ) # Navigate to formset view url = f"{self.live_server_url}{reverse('extra_views:articles')}" self.page.goto(url) # Verify both types are displayed # Blog post should have author field expect(self.page.locator(f"input[value='{blog_post.author}']")).to_be_visible() # News article should have source field expect(self.page.locator(f"input[value='{news_article.source}']")).to_be_visible() # Verify the formset contains both forms # Each form should have an id field id_inputs = self.page.locator("input[type='hidden'][name$='-id']").all() self.assertGreaterEqual(len(id_inputs), 2) def test_formset_mixed_operations_through_ui(self): """Test creating, updating, and deleting in a single formset submission.""" # Create objects blog_to_update = BlogPost.objects.create( title="Update Me", content="Update content", author="Update Author" ) news_to_delete = NewsArticle.objects.create( title="Delete Me", content="Delete content", source="Delete Source" ) news_to_delete_id = news_to_delete.id # Navigate to formset view url = f"{self.live_server_url}{reverse('extra_views:articles')}" self.page.goto(url) # Update the blog post title_input = self.page.locator(f"input[value='{blog_to_update.title}']").first title_input.fill("Updated Blog Title") # Try to delete the news article delete_checkboxes = self.page.locator("input[type='checkbox'][name*='DELETE']").all() if delete_checkboxes: # Check the second DELETE checkbox (for the news article) if len(delete_checkboxes) > 1: delete_checkboxes[1].check() # Submit the form with self.page.expect_navigation(timeout=30000): self.page.locator("button[type='submit']").click() # Verify the blog post was updated blog_to_update.refresh_from_db() self.assertEqual(blog_to_update.title, "Updated Blog Title") # Verify the news article was deleted (if DELETE checkbox was available) if delete_checkboxes and len(delete_checkboxes) > 1: self.assertFalse(NewsArticle.objects.filter(id=news_to_delete_id).exists()) def test_formset_empty_state_shows_extra_forms(self): """Test that the formset shows extra forms for adding new objects.""" # Ensure no objects exist Article.objects.all().delete() # Navigate to formset view url = f"{self.live_server_url}{reverse('extra_views:articles')}" self.page.goto(url) # Verify page loaded expect(self.page.locator("h1")).to_contain_text("Article Formset") # Verify form is rendered form = self.page.locator("form") expect(form).to_be_visible() # The formset should have management form fields (they're hidden) self.assertIsNotNone(self.page.locator("input[name='form-TOTAL_FORMS']").first) self.assertIsNotNone(self.page.locator("input[name='form-INITIAL_FORMS']").first) # Verify we have 2 extra forms (one for each child type) formset_forms = self.page.locator(".formset-form").all() self.assertEqual(len(formset_forms), 2, "Should have 2 extra forms") # Verify we have headers indicating the form types expect(self.page.locator("h3:has-text('Blog Post')")).to_be_visible() expect(self.page.locator("h3:has-text('News Article')")).to_be_visible() # Verify BlogPost form has author field expect(self.page.locator("input[name='form-0-author']")).to_be_visible() # Verify NewsArticle form has source field expect(self.page.locator("input[name='form-1-source']")).to_be_visible() def test_formset_create_new_objects_via_extra_forms(self): """Test creating new objects using the extra forms in the UI.""" # Ensure no objects exist Article.objects.all().delete() # Navigate to formset view url = f"{self.live_server_url}{reverse('extra_views:articles')}" self.page.goto(url) # Fill in the BlogPost form (form-0) self.page.locator("input[name='form-0-title']").fill("New Blog from UI") self.page.locator("textarea[name='form-0-content']").fill("Blog content from UI") self.page.locator("input[name='form-0-author']").fill("UI Author") # Fill in the NewsArticle form (form-1) self.page.locator("input[name='form-1-title']").fill("New News from UI") self.page.locator("textarea[name='form-1-content']").fill("News content from UI") self.page.locator("input[name='form-1-source']").fill("UI Source") # Submit the form with self.page.expect_navigation(timeout=30000): self.page.locator("button[type='submit']").click() # Verify objects were created self.assertEqual(Article.objects.count(), 2) # Verify BlogPost was created blog_post = BlogPost.objects.get(title="New Blog from UI") self.assertEqual(blog_post.author, "UI Author") # Verify NewsArticle was created news_article = NewsArticle.objects.get(title="New News from UI") self.assertEqual(news_article.source, "UI Source") django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/extra_views/urls.py000066400000000000000000000002661513173623500321360ustar00rootroot00000000000000from django.urls import path from .views import ArticleFormSetView app_name = "extra_views" urlpatterns = [ path("articles/", ArticleFormSetView.as_view(), name="articles"), ] django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/extra_views/views.py000066400000000000000000000013411513173623500323010ustar00rootroot00000000000000from polymorphic.contrib.extra_views import PolymorphicFormSetView from polymorphic.formsets import PolymorphicFormSetChild from django.urls import reverse_lazy from ..models import Article, BlogPost, NewsArticle class ArticleFormSetView(PolymorphicFormSetView): model = Article template_name = "extra_views/article_formset.html" success_url = reverse_lazy("extra_views:articles") fields = "__all__" # extra will add two empty forms for models in the order of their appearance # in formset_children factory_kwargs = {"extra": 2, "can_delete": True} formset_children = [ PolymorphicFormSetChild(BlogPost, fields="__all__"), PolymorphicFormSetChild(NewsArticle, fields="__all__"), ] django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/guardian/000077500000000000000000000000001513173623500300255ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/guardian/__init__.py000066400000000000000000000000001513173623500321240ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/guardian/apps.py000066400000000000000000000003461513173623500313450ustar00rootroot00000000000000from django.apps import AppConfig class GuardianExampleConfig(AppConfig): name = "polymorphic.tests.examples.integrations.guardian" label = "guardian_integration" verbose_name = "django-guardian Integration Example" django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/guardian/test.py000066400000000000000000000207751513173623500313710ustar00rootroot00000000000000from unittest import skipUnless from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.test import TestCase try: import guardian # noqa: F401 GUARDIAN_INSTALLED = True except ImportError: GUARDIAN_INSTALLED = False from ..models import Article, BlogPost, NewsArticle from ....models import PlainD from polymorphic.contrib.guardian import get_polymorphic_base_content_type User = get_user_model() @skipUnless(GUARDIAN_INSTALLED, "django-guardian is not installed") class GuardianIntegrationTests(TestCase): """ Tests for django-guardian integration with polymorphic models. These tests verify that get_polymorphic_base_content_type returns the correct base content type for polymorphic models, which is essential for django-guardian to work correctly with polymorphic inheritance. When configured with GUARDIAN_GET_CONTENT_TYPE pointing to this function, django-guardian will assign permissions to the base polymorphic model rather than the specific child model, ensuring consistent permission handling across the polymorphic hierarchy. """ def setUp(self): """Create test objects.""" self.user = User.objects.create_user(username="testuser", password="testpass") self.blog_post = BlogPost.objects.create( title="Test Blog", content="Blog content", author="Blog Author" ) self.news_article = NewsArticle.objects.create( title="Test News", content="News content", source="News Source" ) def test_non_polymorphic_model(self): """Test that non-polymorphic models return their own content type.""" plain_d = PlainD.objects.create(field1="Plain D", field2="Plain D") ctype = get_polymorphic_base_content_type(plain_d) expected_ctype = ContentType.objects.get_for_model(PlainD) self.assertEqual(ctype, expected_ctype) self.assertEqual(ctype.model, "plaind") def test_get_polymorphic_base_content_type_for_child_model(self): """Test that get_polymorphic_base_content_type returns base type for child models.""" # Get content type for BlogPost instance blog_ctype = get_polymorphic_base_content_type(self.blog_post) # Should return Article content type, not BlogPost article_ctype = ContentType.objects.get_for_model(Article, for_concrete_model=False) self.assertEqual(blog_ctype, article_ctype) self.assertNotEqual(blog_ctype.model, "blogpost") self.assertEqual(blog_ctype.model, "article") def test_get_polymorphic_base_content_type_for_different_child_models(self): """Test that different child models return the same base content type.""" blog_ctype = get_polymorphic_base_content_type(self.blog_post) news_ctype = get_polymorphic_base_content_type(self.news_article) # Both should return the same Article content type self.assertEqual(blog_ctype, news_ctype) self.assertEqual(blog_ctype.model, "article") def test_get_polymorphic_base_content_type_for_base_model(self): """Test that base polymorphic models return their own content type.""" article = Article.objects.create(title="Plain Article", content="Plain content") article_ctype = get_polymorphic_base_content_type(article) expected_ctype = ContentType.objects.get_for_model(Article, for_concrete_model=False) self.assertEqual(article_ctype, expected_ctype) self.assertEqual(article_ctype.model, "article") def test_get_polymorphic_base_content_type_for_non_polymorphic_model(self): """Test that non-polymorphic models return their own content type.""" user_ctype = get_polymorphic_base_content_type(self.user) expected_ctype = ContentType.objects.get_for_model(User) self.assertEqual(user_ctype, expected_ctype) def test_get_polymorphic_base_content_type_with_model_class(self): """Test that the function works with model classes, not just instances.""" # Test with a model class instead of instance blog_class_ctype = get_polymorphic_base_content_type(BlogPost) article_ctype = ContentType.objects.get_for_model(Article, for_concrete_model=False) self.assertEqual(blog_class_ctype, article_ctype) def test_content_type_consistency_across_inheritance_chain(self): """Test that content type is consistent across the inheritance chain.""" # Create multiple levels if they exist blog_ctype = get_polymorphic_base_content_type(self.blog_post) news_ctype = get_polymorphic_base_content_type(self.news_article) # All should point to the same base Article type article_ctype = ContentType.objects.get_for_model(Article, for_concrete_model=False) self.assertEqual(blog_ctype, article_ctype) self.assertEqual(news_ctype, article_ctype) def test_get_polymorphic_base_content_type_with_instance_and_class(self): """Test that the function works consistently with instances and classes.""" # Get content type from instance instance_ctype = get_polymorphic_base_content_type(self.blog_post) # Get content type from class class_ctype = get_polymorphic_base_content_type(BlogPost) # Both should return the same Article content type self.assertEqual(instance_ctype, class_ctype) self.assertEqual(instance_ctype.model, "article") def test_get_polymorphic_base_content_type_returns_content_type_object(self): """Test that the function returns a ContentType instance.""" self.assertIsInstance(get_polymorphic_base_content_type(self.blog_post), ContentType) def test_guardian_permissions_use_base_model_namespace(self): """ Test that guardian permissions are assigned to base model when configured. This test verifies that when GUARDIAN_GET_CONTENT_TYPE is set to use get_polymorphic_base_content_type, permissions assigned to child model instances (BlogPost, NewsArticle) are stored against the base Article content type, creating a single permission namespace for the entire polymorphic model hierarchy. """ from guardian.shortcuts import assign_perm, remove_perm from guardian.models import UserObjectPermission # The setting is configured in settings.py when guardian is installed # GUARDIAN_GET_CONTENT_TYPE = "polymorphic.contrib.guardian..." # Assign permission to a BlogPost instance assign_perm("add_article", self.user, self.blog_post) # Get the stored permission object perm = UserObjectPermission.objects.filter( user=self.user, object_pk=str(self.blog_post.pk) ).first() # Verify permission was created self.assertIsNotNone(perm, "Permission should be created") # The critical assertion: permission should use Article content type, # NOT BlogPost content type article_ctype = ContentType.objects.get_for_model(Article, for_concrete_model=False) self.assertEqual( perm.content_type, article_ctype, "Permission should be stored against Article, not BlogPost", ) # Verify the permission codename is correct self.assertEqual(perm.permission.codename, "add_article") # Now assign permission to a NewsArticle instance assign_perm("change_article", self.user, self.news_article) news_perm = UserObjectPermission.objects.filter( user=self.user, object_pk=str(self.news_article.pk) ).first() # NewsArticle permission should ALSO use Article content type self.assertEqual( news_perm.content_type, article_ctype, "NewsArticle permission should also use Article content type", ) # All permissions for this polymorphic tree should share the same # content type, creating a unified permission namespace all_perms = UserObjectPermission.objects.filter(user=self.user) content_types = set(p.content_type for p in all_perms) self.assertEqual( len(content_types), 1, "All permissions should use the same Article content type", ) self.assertEqual( content_types.pop(), article_ctype, "The shared content type should be Article", ) # Clean up remove_perm("add_article", self.user, self.blog_post) remove_perm("change_article", self.user, self.news_article) django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/migrations/000077500000000000000000000000001513173623500304075ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/migrations/0001_initial.py000066400000000000000000000036521513173623500330600ustar00rootroot00000000000000# Generated by Django 5.2 on 2026-01-13 13:59 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ('contenttypes', '0002_remove_content_type_name'), ] operations = [ migrations.CreateModel( name='Article', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=100)), ('content', models.TextField()), ('created', models.DateTimeField(auto_now_add=True)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='BlogPost', fields=[ ('article_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='integrations.article')), ('author', models.CharField(max_length=100)), ], options={ 'abstract': False, }, bases=('integrations.article',), ), migrations.CreateModel( name='NewsArticle', fields=[ ('article_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='integrations.article')), ('source', models.CharField(max_length=100)), ], options={ 'abstract': False, }, bases=('integrations.article',), ), ] django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/migrations/__init__.py000066400000000000000000000000001513173623500325060ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/models.py000066400000000000000000000006731513173623500300760ustar00rootroot00000000000000from django.db import models from polymorphic.models import PolymorphicModel class Article(PolymorphicModel): title = models.CharField(max_length=100) content = models.TextField() created = models.DateTimeField(auto_now_add=True) def __str__(self): return self.title class BlogPost(Article): author = models.CharField(max_length=100) class NewsArticle(Article): source = models.CharField(max_length=100) django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/reversion/000077500000000000000000000000001513173623500302475ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/reversion/__init__.py000066400000000000000000000000001513173623500323460ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/reversion/admin.py000066400000000000000000000015471513173623500317200ustar00rootroot00000000000000from django.contrib import admin from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin from reversion.admin import VersionAdmin from ..models import Article, BlogPost, NewsArticle class ArticleChildAdmin(PolymorphicChildModelAdmin, VersionAdmin): base_model = Article @admin.register(BlogPost) class BlogPostAdmin(ArticleChildAdmin): pass @admin.register(NewsArticle) class NewsArticleAdmin(ArticleChildAdmin): pass class ArticleParentAdmin(VersionAdmin, PolymorphicParentModelAdmin): """ Parent admin for Article model with reversion support. Note: VersionAdmin must come before PolymorphicParentModelAdmin in the inheritance order. """ base_model = Article child_models = (BlogPost, NewsArticle) list_display = ("title", "created") admin.site.register(Article, ArticleParentAdmin) django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/reversion/apps.py000066400000000000000000000003371513173623500315670ustar00rootroot00000000000000from django.apps import AppConfig class ReversionExampleConfig(AppConfig): name = "polymorphic.tests.examples.integrations.reversion" label = "reversion_example" verbose_name = "Reversion Integration Example" django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/reversion/templates/000077500000000000000000000000001513173623500322455ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/reversion/templates/admin/000077500000000000000000000000001513173623500333355ustar00rootroot00000000000000polymorphic/000077500000000000000000000000001513173623500356235ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/reversion/templates/adminobject_history.html000066400000000000000000000003071513173623500415400ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/reversion/templates/admin/polymorphic{% extends 'reversion/object_history.html' %} {% load polymorphic_admin_tags %} {% block breadcrumbs %} {% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %} {% endblock %} django-polymorphic-4.10.2/src/polymorphic/tests/examples/integrations/reversion/test.py000066400000000000000000000530371513173623500316100ustar00rootroot00000000000000from unittest import skipUnless from django.test import TestCase from django.urls import reverse from playwright.sync_api import expect try: from reversion import revisions from reversion.models import Version REVERSION_INSTALLED = True except ImportError: REVERSION_INSTALLED = False revisions = None Version = None from ..models import Article, BlogPost, NewsArticle from polymorphic.tests.utils import _GenericUITest @skipUnless(REVERSION_INSTALLED, "django-reversion is not installed") class ReversionIntegrationTests(TestCase): """ Tests for django-reversion integration with polymorphic models. These tests verify that: 1. Polymorphic models can be versioned 2. The follow parameter correctly tracks parent model changes 3. Revisions are created and can be retrieved """ def test_blogpost_versioning(self): """Test that BlogPost instances are properly versioned.""" # Create a blog post with reversion with revisions.create_revision(): blog_post = BlogPost.objects.create( title="First Post", content="This is my first blog post.", author="John Doe" ) revisions.set_comment("Initial version") # Verify a version was created versions = Version.objects.get_for_object(blog_post) self.assertEqual(versions.count(), 1) self.assertEqual(versions[0].revision.comment, "Initial version") # Update the blog post with revisions.create_revision(): blog_post.title = "Updated Post" blog_post.content = "This is my updated blog post." blog_post.save() revisions.set_comment("Updated title and content") # Verify we now have two versions versions = Version.objects.get_for_object(blog_post) self.assertEqual(versions.count(), 2) # Verify we can retrieve the old version old_version = versions[1] old_data = old_version.field_dict self.assertEqual(old_data["title"], "First Post") self.assertEqual(old_data["content"], "This is my first blog post.") def test_newsarticle_versioning(self): """Test that NewsArticle instances are properly versioned.""" # Create a news article with reversion with revisions.create_revision(): news_article = NewsArticle.objects.create( title="Breaking News", content="Important news story.", source="Daily News" ) revisions.set_comment("Published article") # Verify a version was created versions = Version.objects.get_for_object(news_article) self.assertEqual(versions.count(), 1) # Update the news article with revisions.create_revision(): news_article.content = "Updated news story with more details." news_article.save() revisions.set_comment("Added more details") # Verify we now have two versions versions = Version.objects.get_for_object(news_article) self.assertEqual(versions.count(), 2) def test_polymorphic_queryset_with_versioned_objects(self): """Test that polymorphic queries work correctly with versioned objects.""" # Create instances of both child models with revisions.create_revision(): BlogPost.objects.create(title="Blog Post", content="Blog content", author="Jane Smith") NewsArticle.objects.create( title="News Article", content="News content", source="News Corp" ) # Query using the polymorphic base model articles = Article.objects.all() self.assertEqual(articles.count(), 2) # Verify polymorphic behavior self.assertIsInstance(articles[0], (BlogPost, NewsArticle)) self.assertIsInstance(articles[1], (BlogPost, NewsArticle)) # Verify both have versions for article in articles: versions = Version.objects.get_for_object(article) self.assertGreaterEqual(versions.count(), 1) def test_revert_to_previous_version(self): """Test that we can revert an object to a previous version.""" # Create initial version with revisions.create_revision(): blog_post = BlogPost.objects.create( title="Original Title", content="Original content", author="Author One" ) # Make several updates with revisions.create_revision(): blog_post.title = "Second Title" blog_post.save() with revisions.create_revision(): blog_post.title = "Third Title" blog_post.author = "Author Two" blog_post.save() # Verify current state blog_post.refresh_from_db() self.assertEqual(blog_post.title, "Third Title") self.assertEqual(blog_post.author, "Author Two") # Revert to first version versions = Version.objects.get_for_object(blog_post) first_version = versions[2] # Versions are in reverse chronological order first_version.revision.revert() # Verify reverted state blog_post.refresh_from_db() self.assertEqual(blog_post.title, "Original Title") self.assertEqual(blog_post.author, "Author One") def test_manual_reversion_workflow(self): """Test complete manual reversion workflow without admin interface.""" # Create a blog post with initial version with revisions.create_revision(): blog_post = BlogPost.objects.create( title="Manual Test Post", content="Initial content for manual testing.", author="Test Author", ) revisions.set_user(None) revisions.set_comment("Manual creation") original_id = blog_post.id # Make first update with revisions.create_revision(): blog_post.title = "Updated Manual Test Post" blog_post.content = "First update to content." blog_post.save() revisions.set_comment("First manual update") # Make second update with revisions.create_revision(): blog_post.author = "Updated Author" blog_post.content = "Second update to content." blog_post.save() revisions.set_comment("Second manual update") # Verify we have 3 versions versions = Version.objects.get_for_object(blog_post) self.assertEqual(versions.count(), 3) # Test getting version data latest_version = versions[0] self.assertEqual(latest_version.field_dict["author"], "Updated Author") self.assertEqual(latest_version.field_dict["content"], "Second update to content.") middle_version = versions[1] self.assertEqual(middle_version.field_dict["title"], "Updated Manual Test Post") self.assertEqual(middle_version.field_dict["content"], "First update to content.") original_version = versions[2] self.assertEqual(original_version.field_dict["title"], "Manual Test Post") self.assertEqual(original_version.field_dict["author"], "Test Author") # Test reverting to middle version manually middle_version.revision.revert() blog_post.refresh_from_db() self.assertEqual(blog_post.title, "Updated Manual Test Post") self.assertEqual(blog_post.content, "First update to content.") self.assertEqual(blog_post.author, "Test Author") # Should be from original # Test accessing revision metadata revision = middle_version.revision self.assertEqual(revision.comment, "First manual update") # Test that polymorphic type is preserved across reversion self.assertIsInstance(blog_post, BlogPost) self.assertEqual(blog_post.id, original_id) def test_manual_newsarticle_reversion_with_deletion(self): """Test manual reversion including object deletion and recovery.""" # Create a news article with revisions.create_revision(): news = NewsArticle.objects.create( title="Breaking News", content="Important breaking news.", source="Test News Network", ) revisions.set_comment("Initial news article") news_id = news.id # Update the article with revisions.create_revision(): news.content = "Updated breaking news with more details." news.source = "Updated News Network" news.save() revisions.set_comment("Updated news details") # Get versions before deletion versions = Version.objects.get_for_object(news) self.assertEqual(versions.count(), 2) original_version = versions[1] # Delete the object with revisions.create_revision(): news.delete() revisions.set_comment("Deleted news article") # Verify object is deleted self.assertFalse(NewsArticle.objects.filter(id=news_id).exists()) # Manually recover from deletion by reverting to a previous version original_version.revision.revert() # Verify object is restored recovered_news = NewsArticle.objects.get(id=news_id) self.assertEqual(recovered_news.title, "Breaking News") self.assertEqual(recovered_news.content, "Important breaking news.") self.assertEqual(recovered_news.source, "Test News Network") self.assertIsInstance(recovered_news, NewsArticle) def test_manual_batch_reversion(self): """Test reverting multiple polymorphic objects in a single revision.""" # Create multiple objects in one revision with revisions.create_revision(): blog1 = BlogPost.objects.create(title="Blog 1", content="Content 1", author="Author 1") blog2 = BlogPost.objects.create(title="Blog 2", content="Content 2", author="Author 2") news = NewsArticle.objects.create( title="News 1", content="News content", source="Source 1" ) revisions.set_comment("Batch creation") # Update all objects in another revision with revisions.create_revision(): blog1.title = "Updated Blog 1" blog1.save() blog2.title = "Updated Blog 2" blog2.save() news.title = "Updated News 1" news.save() revisions.set_comment("Batch update") # Verify updated state blog1.refresh_from_db() blog2.refresh_from_db() news.refresh_from_db() self.assertEqual(blog1.title, "Updated Blog 1") self.assertEqual(blog2.title, "Updated Blog 2") self.assertEqual(news.title, "Updated News 1") # Get the original revision (should contain all three objects) from reversion.models import Revision original_revision = Revision.objects.order_by("date_created")[0] self.assertEqual(original_revision.comment, "Batch creation") # Revert the entire revision original_revision.revert() # Verify all objects reverted blog1.refresh_from_db() blog2.refresh_from_db() news.refresh_from_db() self.assertEqual(blog1.title, "Blog 1") self.assertEqual(blog2.title, "Blog 2") self.assertEqual(news.title, "News 1") @skipUnless(REVERSION_INSTALLED, "django-reversion is not installed") class ReversionAdminUITests(_GenericUITest): """Test reversion functionality through the admin interface.""" def test_blogpost_admin_reversion(self): """Test BlogPost admin integration: creating, updating, versioning, and reverting through UI.""" # Navigate to BlogPost add page add_url = self.add_url(BlogPost) self.page.goto(add_url) # Create initial BlogPost self.page.fill("input[name='title']", "Admin Test Post") self.page.fill("textarea[name='content']", "Initial admin content") self.page.fill("input[name='author']", "Admin Author") # Save the object with self.page.expect_navigation(timeout=30000): self.page.click("input[name='_save']") # Verify BlogPost was created blog_post = BlogPost.objects.get(title="Admin Test Post") self.assertEqual(blog_post.author, "Admin Author") self.assertEqual(blog_post.content, "Initial admin content") # Verify we have 1 version (created by admin) versions = Version.objects.get_for_object(blog_post) self.assertEqual(versions.count(), 1) first_version = versions[0] self.assertEqual(first_version.field_dict["title"], "Admin Test Post") self.assertEqual(first_version.field_dict["author"], "Admin Author") # Navigate to change page and update the BlogPost change_url = self.change_url(BlogPost, blog_post.pk) self.page.goto(change_url) self.page.fill("input[name='title']", "Updated Admin Test Post") self.page.fill("textarea[name='content']", "Updated admin content") self.page.fill("input[name='author']", "Updated Admin Author") with self.page.expect_navigation(timeout=30000): self.page.click("input[name='_save']") # Verify update blog_post.refresh_from_db() self.assertEqual(blog_post.title, "Updated Admin Test Post") self.assertEqual(blog_post.author, "Updated Admin Author") # Verify we now have 2 versions (admin creates version on each save) versions = Version.objects.get_for_object(blog_post) self.assertEqual(versions.count(), 2) latest_version = versions[0] self.assertEqual(latest_version.field_dict["title"], "Updated Admin Test Post") self.assertEqual(latest_version.field_dict["author"], "Updated Admin Author") # Navigate to history page and verify it's accessible history_url = f"{self.live_server_url}{reverse('admin:integrations_blogpost_history', args=[blog_post.pk])}" self.page.goto(history_url) # Verify we can see the history page expect(self.page.locator("#content h1")).to_contain_text("Change history") # Verify history table shows version information history_table = self.page.locator("table#change-history, div#change-history") expect(history_table).to_be_visible() # Use the UI to revert: Click on the oldest version's date/time link # The history table typically has links in each row - we want the last row (oldest) history_links = self.page.locator("table#change-history a").all() self.assertGreater(len(history_links), 1, "Should have history links") # Click on the last link (oldest version) to view that revision with self.page.expect_navigation(timeout=30000): history_links[0].click() # We're now on the specific version's history page (history//) current_url = self.page.url self.assertIn( "/history/", current_url, f"Should be on history detail page, got: {current_url}" ) # Wait for page to fully load self.page.wait_for_load_state("domcontentloaded", timeout=10000) # The page should show the old version's data page_content = self.page.content() self.assertIn( "Admin Test Post", page_content, "Should see original title on the version page" ) # Find and click the submit button to revert submit_button = self.page.locator("input[type='submit']").first with self.page.expect_navigation(timeout=30000): submit_button.click() # Verify the object was reverted blog_post.refresh_from_db() self.assertEqual(blog_post.title, "Admin Test Post") self.assertEqual(blog_post.author, "Admin Author") self.assertIsInstance(blog_post, BlogPost) def test_article_admin_reversion(self): """Test Article (polymorphic parent) admin versioning and reversion through UI.""" # Create an article first via API, then test admin updates with revisions.create_revision(): article = BlogPost.objects.create( title="Parent Article Test", content="Parent article content", author="Parent Author", ) # Verify version created versions = Version.objects.get_for_object(article) self.assertEqual(versions.count(), 1) # Update through parent Article admin interface change_url = self.change_url(Article, article.pk) self.page.goto(change_url) # Verify we're on the change page for the parent admin expect(self.page.locator("#content h1")).to_contain_text("Change") self.page.fill("input[name='title']", "Updated Parent Article") self.page.fill("textarea[name='content']", "Updated parent content") with self.page.expect_navigation(timeout=30000): self.page.click("input[name='_save']") # Verify update article.refresh_from_db() self.assertEqual(article.title, "Updated Parent Article") # Verify we now have 2 versions (1 from API, 1 from admin) versions = Version.objects.get_for_object(article) self.assertEqual(versions.count(), 2) self.assertEqual(versions[0].field_dict["title"], "Updated Parent Article") self.assertEqual(versions[1].field_dict["title"], "Parent Article Test") # Navigate to history page through parent admin history_url = f"{self.live_server_url}{reverse('admin:integrations_article_history', args=[article.pk])}" self.page.goto(history_url) expect(self.page.locator("#content h1")).to_contain_text("Change history") # Use the UI to revert: Click on the oldest version history_links = self.page.locator("table#change-history a").all() self.assertGreater(len(history_links), 1) with self.page.expect_navigation(timeout=30000): history_links[0].click() # Wait for page to load self.page.wait_for_load_state("domcontentloaded", timeout=10000) # Verify we're on the revert page page_content = self.page.content() self.assertIn("Revert", page_content, "Should be on a revert page") # Click the submit button to revert submit_button = self.page.locator("input[type='submit']").first with self.page.expect_navigation(timeout=30000): submit_button.click() # Verify the object was reverted article.refresh_from_db() self.assertEqual(article.title, "Parent Article Test") self.assertEqual(article.content, "Parent article content") self.assertIsInstance(article, BlogPost) def test_newsarticle_admin_reversion(self): """Test NewsArticle admin versioning and reversion through UI.""" # Navigate to NewsArticle add page add_url = self.add_url(NewsArticle) self.page.goto(add_url) # Create NewsArticle self.page.fill("input[name='title']", "Breaking Admin News") self.page.fill("textarea[name='content']", "Admin news content") self.page.fill("input[name='source']", "Admin News Network") with self.page.expect_navigation(timeout=30000): self.page.click("input[name='_save']") # Verify creation news = NewsArticle.objects.get(title="Breaking Admin News") self.assertEqual(news.source, "Admin News Network") # Verify version created versions = Version.objects.get_for_object(news) self.assertEqual(versions.count(), 1) # Update change_url = self.change_url(NewsArticle, news.pk) self.page.goto(change_url) self.page.fill("input[name='title']", "Updated Breaking News") self.page.fill("input[name='source']", "Updated Network") with self.page.expect_navigation(timeout=30000): self.page.click("input[name='_save']") news.refresh_from_db() self.assertEqual(news.title, "Updated Breaking News") # Verify we have 2 versions from admin operations versions = Version.objects.get_for_object(news) self.assertEqual(versions.count(), 2) self.assertEqual(versions[0].field_dict["title"], "Updated Breaking News") self.assertEqual(versions[1].field_dict["title"], "Breaking Admin News") # Verify history page is accessible history_url = f"{self.live_server_url}{reverse('admin:integrations_newsarticle_history', args=[news.pk])}" self.page.goto(history_url) expect(self.page.locator("#content h1")).to_contain_text("Change history") # Use the UI to revert: Click on the oldest version history_links = self.page.locator("table#change-history a").all() self.assertGreater(len(history_links), 1) with self.page.expect_navigation(timeout=30000): history_links[0].click() # Wait for page to load self.page.wait_for_load_state("domcontentloaded", timeout=10000) # Verify we're on the revert page page_content = self.page.content() self.assertIn("Revert", page_content, "Should be on a revert page") # Try to revert through UI - click the save button submit_buttons = self.page.locator("input[type='submit']").all() with self.page.expect_navigation(timeout=30000): submit_buttons[0].click() # Check if reversion worked news.refresh_from_db() # Reversion worked! Verify values self.assertEqual(news.source, "Admin News Network") self.assertIsInstance(news, NewsArticle) # Verify a new version was created for the revert operation versions_after_revert = Version.objects.get_for_object(news) self.assertEqual(versions_after_revert.count(), 3) django-polymorphic-4.10.2/src/polymorphic/tests/examples/views/000077500000000000000000000000001513173623500246625ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/views/__init__.py000066400000000000000000000000001513173623500267610ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/views/migrations/000077500000000000000000000000001513173623500270365ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/views/migrations/0001_initial.py000066400000000000000000000034361513173623500315070ustar00rootroot00000000000000# Generated by Django 5.2 on 2026-01-13 13:59 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ('contenttypes', '0002_remove_content_type_name'), ] operations = [ migrations.CreateModel( name='Project', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('topic', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='ArtProject', fields=[ ('project_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='views.project')), ('artist', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('views.project',), ), migrations.CreateModel( name='ResearchProject', fields=[ ('project_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='views.project')), ('supervisor', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('views.project',), ), ] django-polymorphic-4.10.2/src/polymorphic/tests/examples/views/migrations/__init__.py000066400000000000000000000000001513173623500311350ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/views/models.py000066400000000000000000000004711513173623500265210ustar00rootroot00000000000000from django.db import models from polymorphic.models import PolymorphicModel class Project(PolymorphicModel): topic = models.CharField(max_length=30) class ArtProject(Project): artist = models.CharField(max_length=30) class ResearchProject(Project): supervisor = models.CharField(max_length=30) django-polymorphic-4.10.2/src/polymorphic/tests/examples/views/templates/000077500000000000000000000000001513173623500266605ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/examples/views/templates/project_form.html000066400000000000000000000002211513173623500322320ustar00rootroot00000000000000
    {% csrf_token %} {{ form.as_p }}
    django-polymorphic-4.10.2/src/polymorphic/tests/examples/views/templates/project_type_select.html000066400000000000000000000001561513173623500336160ustar00rootroot00000000000000
    {% csrf_token %} {{ form.as_p }}
    django-polymorphic-4.10.2/src/polymorphic/tests/examples/views/test.py000066400000000000000000000071641513173623500262230ustar00rootroot00000000000000from django.urls import reverse from playwright.sync_api import expect from polymorphic.tests.utils import _GenericUITest from .models import Project, ArtProject, ResearchProject class ViewExampleTests(_GenericUITest): def test_view_example(self): """Test that the example view code works correctly.""" # Step 1: Navigate to the type selection page select_url = f"{self.live_server_url}{reverse('project-select')}" self.page.goto(select_url) # Verify the page loaded expect(self.page).to_have_url(select_url) # Get model labels for the child models art_project_label = ArtProject._meta.label research_project_label = ResearchProject._meta.label # Verify radio buttons for both types exist art_radio = self.page.locator(f"input[type='radio'][value='{art_project_label}']") research_radio = self.page.locator( f"input[type='radio'][value='{research_project_label}']" ) expect(art_radio).to_be_visible() expect(research_radio).to_be_visible() # Step 2: Select ArtProject and submit art_radio.click() self.page.click("button[type='submit']") # Should redirect to the create view with model parameter create_url_pattern = ( f"{self.live_server_url}{reverse('project-create')}?model={art_project_label}" ) expect(self.page).to_have_url(create_url_pattern) # Step 3: Fill in the ArtProject form # The form should have fields: topic (from Project) and artist (from ArtProject) self.page.fill("input[name='topic']", "Modern Art") self.page.fill("input[name='artist']", "Picasso") # Submit the form with self.page.expect_navigation(timeout=10000): self.page.click("button[type='submit']") # Verify the object was created art_project = ArtProject.objects.filter(topic="Modern Art", artist="Picasso").first() assert art_project is not None, "ArtProject was not created" assert art_project.topic == "Modern Art" assert art_project.artist == "Picasso" # Step 4: Test creating a ResearchProject self.page.goto(select_url) research_radio = self.page.locator( f"input[type='radio'][value='{research_project_label}']" ) research_radio.click() self.page.click("button[type='submit']") # Verify redirect to create view create_url_pattern = ( f"{self.live_server_url}{reverse('project-create')}?model={research_project_label}" ) expect(self.page).to_have_url(create_url_pattern) # Fill in the ResearchProject form # Should have fields: topic and supervisor self.page.fill("input[name='topic']", "Quantum Computing") self.page.fill("input[name='supervisor']", "Dr. Smith") # Submit the form with self.page.expect_navigation(timeout=10000): self.page.click("button[type='submit']") # Verify the object was created research_project = ResearchProject.objects.filter( topic="Quantum Computing", supervisor="Dr. Smith" ).first() assert research_project is not None, "ResearchProject was not created" assert research_project.topic == "Quantum Computing" assert research_project.supervisor == "Dr. Smith" # Verify polymorphic querying works all_projects = Project.objects.all() assert all_projects.count() == 2 assert isinstance(all_projects[0], (ArtProject, ResearchProject)) assert isinstance(all_projects[1], (ArtProject, ResearchProject)) django-polymorphic-4.10.2/src/polymorphic/tests/examples/views/urls.py000066400000000000000000000004021513173623500262150ustar00rootroot00000000000000from django.urls import path from .views import ProjectTypeSelectView, ProjectCreateView urlpatterns = [ path("select/", ProjectTypeSelectView.as_view(), name="project-select"), path("create/", ProjectCreateView.as_view(), name="project-create"), ] django-polymorphic-4.10.2/src/polymorphic/tests/examples/views/views.py000066400000000000000000000051451513173623500263760ustar00rootroot00000000000000from django.apps import apps from django.shortcuts import redirect from django.urls import reverse from django.views.generic import FormView, CreateView from .models import Project, ArtProject, ResearchProject from django import forms from django.utils.translation import gettext_lazy as _ class ProjectTypeChoiceForm(forms.Form): model_type = forms.ChoiceField( label=_("Project Type"), widget=forms.RadioSelect(attrs={"class": "radiolist"}), ) class ProjectTypeSelectView(FormView): form_class = ProjectTypeChoiceForm template_name = "project_type_select.html" def get_form_kwargs(self): kwargs = super().get_form_kwargs() # Build choices using model labels: [(model_label, verbose_name), ...] choices = [ (model._meta.label, model._meta.verbose_name) for model in [ArtProject, ResearchProject] ] kwargs["initial"] = {"model_type": choices[0][0] if choices else None} return kwargs def get_form(self, form_class=None): form = super().get_form(form_class) # Populate the choices for the form using model labels choices = [ (model._meta.label, model._meta.verbose_name) for model in [ArtProject, ResearchProject] ] form.fields["model_type"].choices = choices return form def form_valid(self, form): model_label = form.cleaned_data["model_type"] return redirect(f"{reverse('project-create')}?model={model_label}") class ProjectCreateView(CreateView): model = Project template_name = "project_form.html" def get_success_url(self): return reverse("project-select") def get_form_class(self): # Get the requested model label from query parameter model_label = self.request.GET.get("model") if not model_label: # Fallback or redirect to selection view return super().get_form_class() # Get the model class using the app registry model_class = apps.get_model(model_label) # Create a form for this model # You can also use a factory or a dict mapping if you have custom forms class SpecificForm(forms.ModelForm): class Meta: model = model_class fields = "__all__" # Or specify fields return SpecificForm def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # Pass the model label to the template so it can be preserved # in the form action context["model_label"] = self.request.GET.get("model") return context django-polymorphic-4.10.2/src/polymorphic/tests/migrations/000077500000000000000000000000001513173623500240635ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/migrations/0001_initial.py000066400000000000000000002533671513173623500265460ustar00rootroot00000000000000# Generated by Django 5.2 on 2026-01-13 13:59 import django.db.models.deletion import django.db.models.manager import polymorphic.showfields import polymorphic.tests.models import uuid from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ('auth', '0012_alter_user_first_name_max_length'), ('contenttypes', '0002_remove_content_type_name'), ] operations = [ migrations.CreateModel( name='Account', fields=[ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='account', serialize=False, to=settings.AUTH_USER_MODEL)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Bookmark', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('url', models.URLField()), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Author', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='Base', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field_b', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldType, models.Model), ), migrations.CreateModel( name='BetMultiple', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='BlogBase', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), ), migrations.CreateModel( name='Duck', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Book', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.author')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='ChoiceBlank', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='CustomPkBase', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('b', models.CharField(max_length=1)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), ), migrations.CreateModel( name='DeepCopyTester', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('binary_field', models.BinaryField()), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='DerivedManagerTest', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('abstract_field', models.CharField(max_length=32)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='DisparateKeysParent', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('text', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='RelatedKeyModel', fields=[ ('custom_id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ], ), migrations.CreateModel( name='Enhance_Plain', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field_p', models.CharField(max_length=30)), ], ), migrations.CreateModel( name='InitTestModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('bar', models.CharField(max_length=300)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldType, models.Model), ), migrations.CreateModel( name='InlineParent', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=30)), ], ), migrations.CreateModel( name='M2MAdminTest', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='M2MThroughBase', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=50)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='M2MThroughMembership', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('role', models.CharField(max_length=50)), ('joined_date', models.DateField(auto_now_add=True)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Top', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=50)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Model2A', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldType, models.Model), ), migrations.CreateModel( name='ModelArticle', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('sales_points', models.IntegerField()), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='ModelExtraA', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), ), migrations.CreateModel( name='ModelExtraExternal', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('topic', models.CharField(max_length=30)), ], ), migrations.CreateModel( name='ModelShow1_plain', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='MROBase1', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldType, models.Model), ), migrations.CreateModel( name='MROBase3', fields=[ ('base_3_id', models.AutoField(primary_key=True, serialize=False)), ], ), migrations.CreateModel( name='MultiTableBase', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='MyBaseModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='NatKeyParent', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('slug', models.SlugField(unique=True)), ('content', models.CharField(blank=True, max_length=100)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='ProxyBase', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('some_data', models.CharField(max_length=128)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='NormalBase', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('nb_field', models.IntegerField()), ], ), migrations.CreateModel( name='One2OneRelatingModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ('one2one', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='tests.model2a')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Participant', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='PlainA', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ], ), migrations.CreateModel( name='PlainParentModelWithManager', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='Regression295Related', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('_real_field', models.CharField(max_length=10)), ], ), migrations.CreateModel( name='RelatedManagerTest', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='RelationBase', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field_base', models.CharField(max_length=30)), ('fk', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='relationbase_set', to='tests.relationbase')), ('m2m', models.ManyToManyField(to='tests.relationbase')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), ), migrations.CreateModel( name='SubclassSelectorAbstractBaseModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('base_field', models.CharField(default='test_bf', max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='UUIDProject', fields=[ ('uuid_primary_key', models.UUIDField(default=uuid.uuid1, primary_key=True, serialize=False)), ('topic', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), ), migrations.CreateModel( name='UUIDPlainA', fields=[ ('uuid_primary_key', models.UUIDField(default=uuid.uuid1, primary_key=True, serialize=False)), ('field1', models.CharField(max_length=30)), ], ), migrations.CreateModel( name='SwappableModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'swappable': 'POLYMORPHIC_TEST_SWAPPABLE', }, ), migrations.CreateModel( name='SpecialAccount1', fields=[ ('account_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.account')), ('extra1', models.IntegerField(blank=True, default=None, null=True)), ], options={ 'abstract': False, }, bases=('tests.account',), ), migrations.CreateModel( name='SpecialAccount2', fields=[ ('account_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.account')), ('extra1', models.CharField(blank=True, default='', max_length=30)), ], options={ 'abstract': False, }, bases=('tests.account',), ), migrations.CreateModel( name='ArtProject', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('topic', models.CharField(max_length=30)), ('artist', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Assignment', fields=[ ('bookmark_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.bookmark')), ('assigned_to', models.CharField(max_length=100)), ], options={ 'abstract': False, }, bases=('tests.bookmark',), ), migrations.CreateModel( name='Bar', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(auto_now_add=True)), ('modified_at', models.DateTimeField(auto_now=True)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='FKTestChild', fields=[ ('base_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.base')), ], options={ 'abstract': False, }, bases=('tests.base',), ), migrations.CreateModel( name='ModelX', fields=[ ('base_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.base')), ('field_x', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.base',), ), migrations.CreateModel( name='ModelY', fields=[ ('base_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.base')), ('field_y', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.base',), ), migrations.CreateModel( name='Baz', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(auto_now_add=True)), ('modified_at', models.DateTimeField(auto_now=True)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='BlogA', fields=[ ('blogbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.blogbase')), ('info', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.blogbase',), ), migrations.CreateModel( name='BlogB', fields=[ ('blogbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.blogbase')), ], options={ 'abstract': False, }, bases=('tests.blogbase',), ), migrations.CreateModel( name='BlogEntry_limit_choices_to', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('text', models.CharField(max_length=30)), ('blog', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.blogbase')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), ), migrations.CreateModel( name='BlueHeadDuck', fields=[ ('duck_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.duck')), ], options={ 'abstract': False, }, bases=('tests.duck',), ), migrations.CreateModel( name='RedheadDuck', fields=[ ], options={ 'proxy': True, 'indexes': [], 'constraints': [], }, bases=('tests.duck',), ), migrations.CreateModel( name='RubberDuck', fields=[ ], options={ 'proxy': True, 'indexes': [], 'constraints': [], }, bases=('tests.duck',), ), migrations.CreateModel( name='SpecialBook', fields=[ ('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.book')), ], options={ 'abstract': False, }, bases=('tests.book',), ), migrations.CreateModel( name='ChoiceAthlete', fields=[ ('choiceblank_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.choiceblank')), ('choice', models.CharField(max_length=100)), ], options={ 'abstract': False, }, bases=('tests.choiceblank',), ), migrations.CreateModel( name='CustomPkInherit', fields=[ ('custompkbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tests.custompkbase')), ('custom_id', models.AutoField(primary_key=True, serialize=False)), ('i', models.CharField(max_length=1)), ], options={ 'abstract': False, }, bases=('tests.custompkbase',), ), migrations.CreateModel( name='DateModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('date', models.DateTimeField()), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='DeepCopyTester2', fields=[ ('deepcopytester_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.deepcopytester')), ('binary_field2', models.BinaryField()), ], options={ 'abstract': False, }, bases=('tests.deepcopytester',), ), migrations.CreateModel( name='DerivedManagerTest2', fields=[ ('derivedmanagertest_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.derivedmanagertest')), ], options={ 'abstract': False, }, bases=('tests.derivedmanagertest',), ), migrations.CreateModel( name='DisparateKeysChild2', fields=[ ('disparatekeysparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tests.disparatekeysparent')), ('text_child2', models.CharField(max_length=30)), ('key', models.PositiveIntegerField(primary_key=True, serialize=False)), ], options={ 'abstract': False, }, bases=('tests.disparatekeysparent',), ), migrations.CreateModel( name='DisparateKeysChild1', fields=[ ('disparatekeysparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tests.disparatekeysparent')), ('key', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='tests.relatedkeymodel')), ('text_child1', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.disparatekeysparent',), ), migrations.CreateModel( name='DucksLake', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('time', models.CharField(max_length=10)), ('duck', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.duck')), ], ), migrations.CreateModel( name='Enhance_Base', fields=[ ('base_id', models.AutoField(primary_key=True, serialize=False)), ('field_b', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), ), migrations.CreateModel( name='Enhance_Inherit', fields=[ ('enhance_plain_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tests.enhance_plain')), ('enhance_base_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.enhance_base')), ('field_i', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.enhance_base', 'tests.enhance_plain'), ), migrations.CreateModel( name='FKTest', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('fk', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='tests.base')), ], ), migrations.CreateModel( name='FKTestBase', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Foo', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='InitTestModelSubclass', fields=[ ('inittestmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.inittestmodel')), ], options={ 'abstract': False, }, bases=('tests.inittestmodel',), ), migrations.CreateModel( name='InlineModelA', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inline_children', to='tests.inlineparent')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Lake', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('ducks', models.ManyToManyField(to='tests.duck')), ], ), migrations.CreateModel( name='LakeWithThrough', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('ducks', models.ManyToManyField(through='tests.DucksLake', to='tests.duck')), ], ), migrations.AddField( model_name='duckslake', name='lake', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.lakewiththrough'), ), migrations.CreateModel( name='M2MAdminTestChildA', fields=[ ('m2madmintest_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2madmintest')), ], options={ 'abstract': False, }, bases=('tests.m2madmintest',), ), migrations.CreateModel( name='M2MAdminTestChildB', fields=[ ('m2madmintest_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2madmintest')), ('child_as', models.ManyToManyField(blank=True, related_name='related_bs', to='tests.m2madmintestchilda')), ], options={ 'abstract': False, }, bases=('tests.m2madmintest',), ), migrations.CreateModel( name='M2MThroughPerson', fields=[ ('m2mthroughbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2mthroughbase')), ('email', models.EmailField(blank=True, max_length=254)), ], options={ 'abstract': False, }, bases=('tests.m2mthroughbase',), ), migrations.CreateModel( name='M2MThroughProject', fields=[ ('m2mthroughbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2mthroughbase')), ('description', models.TextField(blank=True)), ], options={ 'abstract': False, }, bases=('tests.m2mthroughbase',), ), migrations.CreateModel( name='DirectM2MContainer', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=50)), ('items', models.ManyToManyField(blank=True, related_name='containers', to='tests.m2mthroughbase')), ], ), migrations.CreateModel( name='M2MThroughMembershipWithPerson', fields=[ ('m2mthroughmembership_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2mthroughmembership')), ], options={ 'abstract': False, }, bases=('tests.m2mthroughmembership',), ), migrations.CreateModel( name='M2MThroughMembershipWithSpecialPerson', fields=[ ('m2mthroughmembership_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2mthroughmembership')), ('special_notes', models.TextField(blank=True, default='')), ], options={ 'abstract': False, }, bases=('tests.m2mthroughmembership',), ), migrations.CreateModel( name='Middle', fields=[ ('top_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.top')), ('description', models.TextField()), ], options={ 'abstract': False, }, bases=('tests.top',), ), migrations.CreateModel( name='Model2B', fields=[ ('model2a_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.model2a')), ('field2', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=(polymorphic.tests.models.RandomMixinB, 'tests.model2a'), ), migrations.CreateModel( name='ModelWithMyManager', fields=[ ('model2a_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.model2a')), ('field4', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, 'tests.model2a'), ), migrations.CreateModel( name='ModelWithMyManager2', fields=[ ('model2a_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.model2a')), ('field4', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, 'tests.model2a'), ), migrations.CreateModel( name='ModelWithMyManagerDefault', fields=[ ('model2a_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.model2a')), ('field4', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, 'tests.model2a'), managers=[ ('my_objects', django.db.models.manager.Manager()), ('objects', django.db.models.manager.Manager()), ], ), migrations.CreateModel( name='ModelWithMyManagerNoDefault', fields=[ ('model2a_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.model2a')), ('field4', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, 'tests.model2a'), ), migrations.CreateModel( name='ModelComponent', fields=[ ('modelarticle_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.modelarticle')), ('name', models.CharField(max_length=300)), ], options={ 'abstract': False, }, bases=('tests.modelarticle',), ), migrations.CreateModel( name='ModelPackage', fields=[ ('modelarticle_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.modelarticle')), ('name', models.CharField(max_length=300)), ], options={ 'abstract': False, }, bases=('tests.modelarticle',), ), migrations.CreateModel( name='ModelExtraB', fields=[ ('modelextraa_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.modelextraa')), ('field2', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.modelextraa',), ), migrations.CreateModel( name='ModelFieldNameTest', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('modelfieldnametest', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldType, models.Model), ), migrations.CreateModel( name='ModelOrderLine', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('articles', models.ManyToManyField(related_name='orderline', to='tests.modelarticle')), ], ), migrations.CreateModel( name='ModelShow1', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ('m2m', models.ManyToManyField(to='tests.modelshow1')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldType, models.Model), ), migrations.CreateModel( name='ModelShow2_plain', fields=[ ('modelshow1_plain_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.modelshow1_plain')), ('field2', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.modelshow1_plain',), ), migrations.CreateModel( name='ParentLinkAndRelatedName', fields=[ ('superclass', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='related_name_subclass', serialize=False, to='tests.modelshow1_plain')), ], options={ 'abstract': False, }, bases=('tests.modelshow1_plain',), ), migrations.CreateModel( name='ModelShow2', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ('m2m', models.ManyToManyField(to='tests.modelshow2')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldContent, models.Model), ), migrations.CreateModel( name='ModelShow3', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ('m2m', models.ManyToManyField(to='tests.modelshow3')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), ), migrations.CreateModel( name='ModelUnderRelParent', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ('_private', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='ModelUnderRelChild', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('_private2', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='children', to='tests.modelunderrelparent')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='ModelWithPolyFK', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100)), ('poly_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tests.model2a')), ], ), migrations.CreateModel( name='MROBase2', fields=[ ('mrobase1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.mrobase1')), ], options={ 'abstract': False, }, bases=('tests.mrobase1',), ), migrations.CreateModel( name='MultiTableDerived', fields=[ ('multitablebase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.multitablebase')), ('field2', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.multitablebase',), ), migrations.CreateModel( name='MyChild1Model', fields=[ ('mybasemodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.mybasemodel')), ('fieldA', models.IntegerField()), ], options={ 'abstract': False, }, bases=('tests.mybasemodel',), ), migrations.CreateModel( name='MyChild2Model', fields=[ ('mybasemodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.mybasemodel')), ('fieldB', models.IntegerField()), ], options={ 'abstract': False, }, bases=('tests.mybasemodel',), ), migrations.CreateModel( name='NatKeyChild', fields=[ ('foo', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.natkeyparent')), ('val', models.IntegerField(default=0)), ], options={ 'abstract': False, }, bases=('tests.natkeyparent',), ), migrations.CreateModel( name='NoChildren', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=12)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='NonPolymorphicParent', fields=[ ('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.group')), ('test', models.CharField(default='test_non_polymorphic_parent', max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=('auth.group', models.Model), ), migrations.CreateModel( name='NonProxyChild', fields=[ ('proxybase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.proxybase')), ('name', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.proxybase',), ), migrations.CreateModel( name='ProxyChild', fields=[ ], options={ 'proxy': True, 'indexes': [], 'constraints': [], }, bases=('tests.proxybase',), ), migrations.CreateModel( name='NormalExtension', fields=[ ('normalbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.normalbase')), ('ne_field', models.CharField(max_length=50)), ], bases=('tests.normalbase',), ), migrations.CreateModel( name='One2OneRelatingModelDerived', fields=[ ('one2onerelatingmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.one2onerelatingmodel')), ('field2', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.one2onerelatingmodel',), ), migrations.CreateModel( name='ParentModelWithManager', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='ChildModelWithManager', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field1', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ('fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='childmodel_set', to='tests.parentmodelwithmanager')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='UserProfile', fields=[ ('participant_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.participant')), ('name', models.CharField(max_length=100)), ], options={ 'abstract': False, }, bases=('tests.participant',), ), migrations.CreateModel( name='PlainB', fields=[ ('plaina_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.plaina')), ('field2', models.CharField(max_length=30)), ], bases=('tests.plaina',), ), migrations.CreateModel( name='PlainD', fields=[ ('plaina_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.plaina')), ('field2', models.CharField(max_length=30)), ], bases=('tests.plaina',), ), migrations.CreateModel( name='PlainChildModelWithManager', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='childmodel_set', to='tests.plainparentmodelwithmanager')), ], ), migrations.CreateModel( name='ProxiedBase', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), ), migrations.CreateModel( name='ProxyModelBase', fields=[ ], options={ 'proxy': True, 'indexes': [], 'constraints': [], }, bases=('tests.proxiedbase',), ), migrations.CreateModel( name='RankedAthlete', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('rank', models.IntegerField()), ('bet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.betmultiple')), ('choiceAthlete', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.choiceblank')), ], ), migrations.AddField( model_name='betmultiple', name='answer', field=models.ManyToManyField(blank=True, through='tests.RankedAthlete', to='tests.choiceblank'), ), migrations.CreateModel( name='RecursionBug', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ('status', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recursions', to='tests.plaina')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Regression295Parent', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ('related_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.regression295related')), ], options={ 'abstract': False, }, ), migrations.AddField( model_name='derivedmanagertest', name='related_test', field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='derived', to='tests.relatedmanagertest'), ), migrations.CreateModel( name='RelatedNameClash', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldType, models.Model), ), migrations.CreateModel( name='RelatingModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('many2many', models.ManyToManyField(to='tests.model2a')), ], ), migrations.CreateModel( name='RelationA', fields=[ ('relationbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.relationbase')), ('field_a', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.relationbase',), ), migrations.CreateModel( name='RelationB', fields=[ ('relationbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.relationbase')), ('field_b', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.relationbase',), ), migrations.CreateModel( name='SubclassSelectorAbstractConcreteModel', fields=[ ('subclassselectorabstractbasemodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.subclassselectorabstractbasemodel')), ('abstract_field', models.CharField(default='test_af', max_length=30)), ('concrete_field', models.CharField(default='test_cf', max_length=30)), ], options={ 'abstract': False, }, bases=('tests.subclassselectorabstractbasemodel',), ), migrations.CreateModel( name='SubclassSelectorProxyBaseModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('base_field', models.CharField(default='test_bf', max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='SubclassSelectorProxyModel', fields=[ ], options={ 'proxy': True, 'indexes': [], 'constraints': [], }, bases=('tests.subclassselectorproxybasemodel',), ), migrations.CreateModel( name='SwappedModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='TaggedItem', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('tag', models.SlugField()), ('object_id', models.PositiveIntegerField()), ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), ], ), migrations.CreateModel( name='UUIDArtProject', fields=[ ('uuidproject_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.uuidproject')), ('artist', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.uuidproject',), ), migrations.CreateModel( name='UUIDResearchProject', fields=[ ('uuidproject_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.uuidproject')), ('supervisor', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.uuidproject',), ), migrations.CreateModel( name='UUIDPlainB', fields=[ ('uuidplaina_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.uuidplaina')), ('field2', models.CharField(max_length=30)), ], bases=('tests.uuidplaina',), ), migrations.CreateModel( name='SpecialAccount1_1', fields=[ ('specialaccount1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.specialaccount1')), ('extra2', models.IntegerField(blank=True, default=None, null=True)), ], options={ 'abstract': False, }, bases=('tests.specialaccount1',), ), migrations.CreateModel( name='BlogEntry', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('text', models.CharField(max_length=30)), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ('blog', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.bloga')), ], options={ 'abstract': False, }, bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), ), migrations.CreateModel( name='PurpleHeadDuck', fields=[ ], options={ 'proxy': True, 'indexes': [], 'constraints': [], }, bases=('tests.blueheadduck', models.Model), ), migrations.CreateModel( name='DisparateKeysGrandChild2', fields=[ ('disparatekeyschild2_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.disparatekeyschild2')), ('text_grand_child', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.disparatekeyschild2',), ), migrations.CreateModel( name='DisparateKeysGrandChild', fields=[ ('disparatekeyschild1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.disparatekeyschild1')), ('text_grand_child', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.disparatekeyschild1',), ), migrations.CreateModel( name='InlineModelB', fields=[ ('inlinemodela_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.inlinemodela')), ('field2', models.CharField(max_length=30)), ('file_upload', models.FileField(blank=True, default=None, null=True, upload_to='test_uploads/')), ('plain_a', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inline_bs', to='tests.plaina')), ], options={ 'abstract': False, }, bases=('tests.inlinemodela',), ), migrations.CreateModel( name='M2MAdminTestChildC', fields=[ ('m2madmintestchildb_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2madmintestchildb')), ], options={ 'abstract': False, }, bases=('tests.m2madmintestchildb',), ), migrations.AddField( model_name='m2madmintestchilda', name='child_bs', field=models.ManyToManyField(blank=True, related_name='related_as', to='tests.m2madmintestchildb'), ), migrations.CreateModel( name='M2MThroughSpecialPerson', fields=[ ('m2mthroughperson_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2mthroughperson')), ('special_code', models.CharField(blank=True, max_length=20)), ], options={ 'abstract': False, }, bases=('tests.m2mthroughperson',), ), migrations.AddField( model_name='m2mthroughmembership', name='person', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.m2mthroughperson'), ), migrations.CreateModel( name='Bottom', fields=[ ('middle_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.middle')), ('author', models.CharField(max_length=50)), ], options={ 'abstract': False, }, bases=('tests.middle',), ), migrations.CreateModel( name='Model2BFiltered', fields=[ ('model2b_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.model2b')), ], options={ 'abstract': False, }, bases=('tests.model2b',), ), migrations.CreateModel( name='Model2C', fields=[ ('model2b_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.model2b')), ('field3', models.CharField(blank=True, default='', max_length=30)), ], options={ 'abstract': False, }, bases=(polymorphic.tests.models.RandomMixinC, 'tests.model2b'), ), migrations.CreateModel( name='ModelExtraC', fields=[ ('modelextrab_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.modelextrab')), ('field3', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.modelextrab',), ), migrations.CreateModel( name='MRODerived', fields=[ ('mrobase3_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tests.mrobase3')), ('mrobase2_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.mrobase2')), ], options={ 'abstract': False, }, bases=('tests.mrobase2', 'tests.mrobase3'), ), migrations.CreateModel( name='PolyExtension', fields=[ ('normalextension_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.normalextension')), ('poly_ext_field', models.IntegerField()), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), ], options={ 'abstract': False, }, bases=('tests.normalextension', models.Model), ), migrations.CreateModel( name='Team', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('team_name', models.CharField(max_length=100)), ('user_profiles', models.ManyToManyField(related_name='user_teams', to='tests.userprofile')), ], ), migrations.CreateModel( name='PlainC', fields=[ ('plainb_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.plainb')), ('field3', models.CharField(max_length=30)), ], bases=('tests.plainb',), ), migrations.CreateModel( name='ProxyModelA', fields=[ ('proxiedbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.proxiedbase')), ('field1', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.proxymodelbase',), ), migrations.CreateModel( name='ProxyModelB', fields=[ ('proxiedbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.proxiedbase')), ('field2', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.proxymodelbase',), ), migrations.CreateModel( name='RelationBC', fields=[ ('relationb_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.relationb')), ('field_c', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.relationb',), ), migrations.CreateModel( name='SubclassSelectorProxyConcreteModel', fields=[ ('subclassselectorproxybasemodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.subclassselectorproxybasemodel')), ('concrete_field', models.CharField(default='test_cf', max_length=30)), ], options={ 'abstract': False, }, bases=('tests.subclassselectorproxymodel',), ), migrations.CreateModel( name='UUIDArtProjectA', fields=[ ('uuidartproject_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.uuidartproject')), ], options={ 'abstract': False, }, bases=('tests.uuidartproject',), ), migrations.CreateModel( name='UUIDPlainC', fields=[ ('uuidplainb_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.uuidplainb')), ('field3', models.CharField(max_length=30)), ], bases=('tests.uuidplainb',), ), migrations.CreateModel( name='M2MThroughProjectWithTeam', fields=[ ('m2mthroughproject_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2mthroughproject')), ('team', models.ManyToManyField(blank=True, related_name='projects', through='tests.M2MThroughMembership', to='tests.m2mthroughperson')), ], options={ 'abstract': False, }, bases=('tests.m2mthroughproject',), ), migrations.AddField( model_name='m2mthroughmembership', name='project', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.m2mthroughprojectwithteam'), ), migrations.CreateModel( name='Model2CFiltered', fields=[ ('model2bfiltered_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.model2bfiltered')), ('field3', models.CharField(blank=True, default='', max_length=30)), ], options={ 'abstract': False, }, bases=('tests.model2bfiltered',), ), migrations.CreateModel( name='Model2D', fields=[ ('model2c_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.model2c')), ('field4', models.CharField(max_length=30)), ], options={ 'abstract': False, }, bases=('tests.model2c',), ), migrations.CreateModel( name='PolyExtChild', fields=[ ('polyextension_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.polyextension')), ('poly_child_field', models.CharField(max_length=50)), ], options={ 'abstract': False, }, bases=('tests.polyextension',), ), migrations.CreateModel( name='UUIDArtProjectB', fields=[ ('uuidartprojecta_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.uuidartprojecta')), ], options={ 'abstract': False, }, bases=('tests.uuidartprojecta',), ), migrations.CreateModel( name='Model2CNamedDefault', fields=[ ('model2cfiltered_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.model2cfiltered')), ], options={ 'default_manager_name': 'custom_objects', }, bases=('tests.model2cfiltered',), managers=[ ('custom_objects', django.db.models.manager.Manager()), ], ), migrations.CreateModel( name='Model2CNamedManagers', fields=[ ('model2cfiltered_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.model2cfiltered')), ], options={ 'base_manager_name': 'all_objects', 'default_manager_name': 'filtered_objects', }, bases=('tests.model2cfiltered',), managers=[ ('all_objects', django.db.models.manager.Manager()), ('filtered_objects', django.db.models.manager.Manager()), ], ), migrations.CreateModel( name='UUIDArtProjectC', fields=[ ('uuidartprojectb_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.uuidartprojectb')), ], options={ 'abstract': False, }, bases=('tests.uuidartprojectb',), ), migrations.CreateModel( name='UUIDArtProjectD', fields=[ ('uuidartprojectc_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.uuidartprojectc')), ], options={ 'abstract': False, }, bases=('tests.uuidartprojectc',), ), ] django-polymorphic-4.10.2/src/polymorphic/tests/migrations/__init__.py000066400000000000000000000000001513173623500261620ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/models.py000066400000000000000000000640411513173623500235510ustar00rootroot00000000000000import uuid from django.contrib.auth.models import Group from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.db.models import Manager from django.db import models from django.db.models.query import QuerySet from django.db.models import F from django.db.models.functions import Upper from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from polymorphic.managers import PolymorphicManager from polymorphic.models import PolymorphicModel from polymorphic.query import PolymorphicQuerySet from polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent class PlainA(models.Model): field1 = models.CharField(max_length=30) def __str__(self): return self.field1 class PlainB(PlainA): field2 = models.CharField(max_length=30) class PlainC(PlainB): field3 = models.CharField(max_length=30) class PlainD(PlainA): field2 = models.CharField(max_length=30) class Model2A(ShowFieldType, PolymorphicModel): field1 = models.CharField(max_length=30) polymorphic_showfield_deferred = True class RandomMixinB: def random_method(self): return "random b" class Model2B(RandomMixinB, Model2A): field2 = models.CharField(max_length=30) class RandomMixinC: def random_method(self): return "random c" class Model2C(RandomMixinC, Model2B): field3 = models.CharField(max_length=30, blank=True, default="") class Model2D(Model2C): field4 = models.CharField(max_length=30) class ModelExtraA(ShowFieldTypeAndContent, PolymorphicModel): field1 = models.CharField(max_length=30) class ModelExtraB(ModelExtraA): field2 = models.CharField(max_length=30) class ModelExtraC(ModelExtraB): field3 = models.CharField(max_length=30) class ModelExtraExternal(models.Model): topic = models.CharField(max_length=30) class ModelShow1(ShowFieldType, PolymorphicModel): field1 = models.CharField(max_length=30) m2m = models.ManyToManyField("self") class ModelShow2(ShowFieldContent, PolymorphicModel): field1 = models.CharField(max_length=30) m2m = models.ManyToManyField("self") class ModelShow3(ShowFieldTypeAndContent, PolymorphicModel): field1 = models.CharField(max_length=30) m2m = models.ManyToManyField("self") class ModelShow1_plain(PolymorphicModel): field1 = models.CharField(max_length=30) class ModelShow2_plain(ModelShow1_plain): field2 = models.CharField(max_length=30) class Base(ShowFieldType, PolymorphicModel): polymorphic_showfield_deferred = True field_b = models.CharField(max_length=30) class ModelX(Base): field_x = models.CharField(max_length=30) class ModelY(Base): field_y = models.CharField(max_length=30) class Enhance_Plain(models.Model): field_p = models.CharField(max_length=30) class Enhance_Base(ShowFieldTypeAndContent, PolymorphicModel): base_id = models.AutoField(primary_key=True) field_b = models.CharField(max_length=30) class Enhance_Inherit(Enhance_Base, Enhance_Plain): field_i = models.CharField(max_length=30) class RelationAbstractModel(models.Model): class Meta: abstract = True class RelationBase(RelationAbstractModel, ShowFieldTypeAndContent, PolymorphicModel): field_base = models.CharField(max_length=30) fk = models.ForeignKey( "self", on_delete=models.CASCADE, null=True, related_name="relationbase_set" ) m2m = models.ManyToManyField("self") class RelationA(RelationBase): field_a = models.CharField(max_length=30) class RelationB(RelationBase): field_b = models.CharField(max_length=30) class RelationBC(RelationB): field_c = models.CharField(max_length=30) class RelatingModel(models.Model): many2many = models.ManyToManyField(Model2A) class One2OneRelatingModel(PolymorphicModel): one2one = models.OneToOneField(Model2A, on_delete=models.CASCADE) field1 = models.CharField(max_length=30) class One2OneRelatingModelDerived(One2OneRelatingModel): field2 = models.CharField(max_length=30) class ModelUnderRelParent(PolymorphicModel): field1 = models.CharField(max_length=30) _private = models.CharField(max_length=30) class ModelUnderRelChild(PolymorphicModel): parent = models.ForeignKey( ModelUnderRelParent, on_delete=models.CASCADE, related_name="children" ) _private2 = models.CharField(max_length=30) class MyManagerQuerySet(PolymorphicQuerySet): def my_queryset_foo(self): # Just a method to prove the existence of the custom queryset. return self.all() class MyManager(PolymorphicManager): queryset_class = MyManagerQuerySet def get_queryset(self): return super().get_queryset().order_by("-field1") def my_queryset_foo(self): return self.all().my_queryset_foo() class ModelWithMyManager(ShowFieldTypeAndContent, Model2A): objects = MyManager() field4 = models.CharField(max_length=30) class ModelWithMyManagerNoDefault(ShowFieldTypeAndContent, Model2A): objects = PolymorphicManager() my_objects = MyManager() field4 = models.CharField(max_length=30) class ModelWithMyManagerDefault(ShowFieldTypeAndContent, Model2A): my_objects = MyManager() objects = PolymorphicManager() field4 = models.CharField(max_length=30) class ModelWithMyManager2(ShowFieldTypeAndContent, Model2A): objects = MyManagerQuerySet.as_manager() field4 = models.CharField(max_length=30) class ModelArticle(PolymorphicModel): sales_points = models.IntegerField() class ModelPackage(ModelArticle): name = models.CharField(max_length=300) class ModelComponent(ModelArticle): name = models.CharField(max_length=300) class ModelOrderLine(models.Model): articles = models.ManyToManyField(ModelArticle, related_name="orderline") class MROBase1(ShowFieldType, PolymorphicModel): objects = MyManager() field1 = models.CharField(max_length=30) # needed as MyManager uses it class MROBase2(MROBase1): pass # No manager_inheritance_from_future or Meta set. test that polymorphic restores that. class MROBase3(models.Model): # make sure 'id' field doesn't clash, detected by Django 1.11 base_3_id = models.AutoField(primary_key=True) objects = models.Manager() class MRODerived(MROBase2, MROBase3): pass class ParentModelWithManager(PolymorphicModel): pass class ChildModelWithManager(PolymorphicModel): # Also test whether foreign keys receive the manager: field1 = models.CharField(max_length=30) # needed as MyManager uses it fk = models.ForeignKey( ParentModelWithManager, on_delete=models.CASCADE, related_name="childmodel_set" ) objects = MyManager() class PlainMyManagerQuerySet(QuerySet): def my_queryset_foo(self): # Just a method to prove the existence of the custom queryset. return self.all() class PlainMyManager(models.Manager): def my_queryset_foo(self): return self.get_queryset().my_queryset_foo() def get_queryset(self): return PlainMyManagerQuerySet(self.model, using=self._db) class PlainParentModelWithManager(models.Model): pass class PlainChildModelWithManager(models.Model): fk = models.ForeignKey( PlainParentModelWithManager, on_delete=models.CASCADE, related_name="childmodel_set", ) objects = PlainMyManager() class BlogBase(ShowFieldTypeAndContent, PolymorphicModel): name = models.CharField(max_length=30) class BlogA(BlogBase): info = models.CharField(max_length=30) class BlogB(BlogBase): pass class BlogEntry(ShowFieldTypeAndContent, PolymorphicModel): blog = models.ForeignKey(BlogA, on_delete=models.CASCADE) text = models.CharField(max_length=30) class BlogEntry_limit_choices_to(ShowFieldTypeAndContent, PolymorphicModel): blog = models.ForeignKey(BlogBase, on_delete=models.CASCADE) text = models.CharField(max_length=30) class ModelFieldNameTest(ShowFieldType, PolymorphicModel): modelfieldnametest = models.CharField(max_length=30) class InitTestModel(ShowFieldType, PolymorphicModel): bar = models.CharField(max_length=300) def __init__(self, *args, **kwargs): kwargs["bar"] = self.x() super().__init__(*args, **kwargs) class InitTestModelSubclass(InitTestModel): def x(self): return "XYZ" # models from github issue class Top(PolymorphicModel): name = models.CharField(max_length=50) class Middle(Top): description = models.TextField() class Bottom(Middle): author = models.CharField(max_length=50) class UUIDProject(ShowFieldTypeAndContent, PolymorphicModel): uuid_primary_key = models.UUIDField(primary_key=True, default=uuid.uuid1) topic = models.CharField(max_length=30) class UUIDArtProject(UUIDProject): artist = models.CharField(max_length=30) class UUIDResearchProject(UUIDProject): supervisor = models.CharField(max_length=30) class UUIDArtProjectA(UUIDArtProject): ... class UUIDArtProjectB(UUIDArtProjectA): ... class UUIDArtProjectC(UUIDArtProjectB): ... class UUIDArtProjectD(UUIDArtProjectC): ... class UUIDPlainA(models.Model): uuid_primary_key = models.UUIDField(primary_key=True, default=uuid.uuid1) field1 = models.CharField(max_length=30) class UUIDPlainB(UUIDPlainA): field2 = models.CharField(max_length=30) class UUIDPlainC(UUIDPlainB): field3 = models.CharField(max_length=30) # base -> proxy class ProxyBase(PolymorphicModel): some_data = models.CharField(max_length=128) class ProxyChild(ProxyBase): class Meta: proxy = True class NonProxyChild(ProxyBase): name = models.CharField(max_length=30) # base -> proxy -> real models class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel): name = models.CharField(max_length=30) class ProxyModelBase(ProxiedBase): class Meta: proxy = True class ProxyModelA(ProxyModelBase): field1 = models.CharField(max_length=30) class ProxyModelB(ProxyModelBase): field2 = models.CharField(max_length=30) # test bad field name # class TestBadFieldModel(ShowFieldType, PolymorphicModel): # instance_of = models.CharField(max_length=30) # validation error: "polymorphic.relatednameclash: Accessor for field 'polymorphic_ctype' clashes # with related field 'ContentType.relatednameclash_set'." (reported by Andrew Ingram) # fixed with related_name class RelatedNameClash(ShowFieldType, PolymorphicModel): ctype = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, editable=False) # class with a parent_link to superclass, and a related_name back to subclass class ParentLinkAndRelatedName(ModelShow1_plain): superclass = models.OneToOneField( ModelShow1_plain, on_delete=models.CASCADE, parent_link=True, related_name="related_name_subclass", ) class CustomPkBase(ShowFieldTypeAndContent, PolymorphicModel): b = models.CharField(max_length=1) class CustomPkInherit(CustomPkBase): custom_id = models.AutoField(primary_key=True) i = models.CharField(max_length=1) class DateModel(PolymorphicModel): date = models.DateTimeField() # Define abstract and swappable (being swapped for SwappedModel) models # To test manager validation (should be skipped for such models) class AbstractModel(PolymorphicModel): class Meta: abstract = True class SwappableModel(AbstractModel): class Meta: swappable = "POLYMORPHIC_TEST_SWAPPABLE" class SwappedModel(AbstractModel): pass class InlineParent(models.Model): title = models.CharField(max_length=30) class InlineModelA(PolymorphicModel): parent = models.ForeignKey( InlineParent, related_name="inline_children", on_delete=models.CASCADE ) field1 = models.CharField(max_length=30) class InlineModelB(InlineModelA): field2 = models.CharField(max_length=30) plain_a = models.ForeignKey( PlainA, null=True, blank=True, default=None, on_delete=models.SET_NULL, related_name="inline_bs", ) # File field for testing multipart encoding in polymorphic inlines (issue #380) file_upload = models.FileField(upload_to="test_uploads/", null=True, blank=True, default=None) class AbstractProject(PolymorphicModel): topic = models.CharField(max_length=30) class Meta: abstract = True class ArtProject(AbstractProject): artist = models.CharField(max_length=30) class Duck(PolymorphicModel): name = models.CharField(max_length=30) class RedheadDuck(Duck): class Meta: proxy = True class RubberDuck(Duck): class Meta: proxy = True class MultiTableBase(PolymorphicModel): field1 = models.CharField(max_length=30) class MultiTableDerived(MultiTableBase): field2 = models.CharField(max_length=30) class SubclassSelectorAbstractBaseModel(PolymorphicModel): base_field = models.CharField(max_length=30, default="test_bf") class SubclassSelectorAbstractModel(SubclassSelectorAbstractBaseModel): abstract_field = models.CharField(max_length=30, default="test_af") class Meta: abstract = True class SubclassSelectorAbstractConcreteModel(SubclassSelectorAbstractModel): concrete_field = models.CharField(max_length=30, default="test_cf") class SubclassSelectorProxyBaseModel(PolymorphicModel): base_field = models.CharField(max_length=30, default="test_bf") class SubclassSelectorProxyModel(SubclassSelectorProxyBaseModel): class Meta: proxy = True class SubclassSelectorProxyConcreteModel(SubclassSelectorProxyModel): concrete_field = models.CharField(max_length=30, default="test_cf") class NonPolymorphicParent(PolymorphicModel, Group): test = models.CharField(max_length=30, default="test_non_polymorphic_parent") class Participant(PolymorphicModel): pass class UserProfile(Participant): name = models.CharField(max_length=100) def __str__(self): return self.name class Team(models.Model): team_name = models.CharField(max_length=100) user_profiles = models.ManyToManyField(UserProfile, related_name="user_teams") class BlueHeadDuck(Duck): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.color = "blue" class HomeDuck(models.Model): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.home = "Duckburg" class Meta: abstract = True class PurpleHeadDuck(HomeDuck, BlueHeadDuck): class Meta: proxy = True class Account(PolymorphicModel): user = models.OneToOneField( get_user_model(), primary_key=True, on_delete=models.CASCADE, related_name="account" ) class SpecialAccount1(Account): extra1 = models.IntegerField(null=True, default=None, blank=True) class SpecialAccount1_1(SpecialAccount1): extra2 = models.IntegerField(null=True, default=None, blank=True) class SpecialAccount2(Account): extra1 = models.CharField(default="", blank=True, max_length=30) class ModelMixin(models.Model): class Meta: abstract = True created_at = models.DateTimeField(auto_now_add=True) modified_at = models.DateTimeField(auto_now=True) class PolymorphicMixin(PolymorphicModel): class Meta: abstract = True created_at = models.DateTimeField(auto_now_add=True) modified_at = models.DateTimeField(auto_now=True) class Foo(PolymorphicModel): pass class Bar(PolymorphicMixin, PolymorphicModel): pass class Baz(ModelMixin, PolymorphicModel): pass class MyBaseQuerySet(PolymorphicQuerySet): def filter_by_user(self, _): return self.all() class MyBaseModel(PolymorphicModel): ... objects = MyBaseQuerySet.as_manager() class MyChild1QuerySet(MyBaseQuerySet): def filter_by_user(self, num): return self.filter(fieldA__lt=num) class MyChild1Model(MyBaseModel): fieldA = models.IntegerField() ... objects = MyChild1QuerySet.as_manager() class MyChild2QuerySet(MyBaseQuerySet): def filter_by_user(self, num): return self.filter(fieldB__gt=num) class MyChild2Model(MyBaseModel): fieldB = models.IntegerField() ... objects = PolymorphicManager.from_queryset(MyChild2QuerySet)() base_manager = MyBaseQuerySet.as_manager() class SpecialQuerySet(PolymorphicQuerySet): def has_text(self, text): return self.filter(abstract_field__icontains=text) class SpecialPolymorphicManager(PolymorphicManager.from_queryset(SpecialQuerySet)): def custom_queryset(self): return self.get_queryset() class AbstractManagerTest(PolymorphicModel): """ Tests that custom manager patterns work on abstract base models """ objects = SpecialPolymorphicManager() basic_manager = Manager() default_manager = PolymorphicManager() abstract_field = models.CharField(max_length=32) class Meta: abstract = True class RelatedManagerTest(models.Model): ... class DerivedManagerTest(AbstractManagerTest): related_test = models.ForeignKey( RelatedManagerTest, on_delete=models.CASCADE, default=None, null=True, related_name="derived", ) class DerivedManagerTest2(DerivedManagerTest): objects = PolymorphicManager() class FKTestBase(PolymorphicModel): ... class FKTestChild(Base): ... class FKTest(models.Model): fk = models.ForeignKey(Base, null=True, on_delete=models.SET_NULL) class NoChildren(PolymorphicModel): field1 = models.CharField(max_length=12) class ModelWithPolyFK(models.Model): """Model with FK to polymorphic model for popup testing.""" name = models.CharField(max_length=100) poly_fk = models.ForeignKey(Model2A, on_delete=models.CASCADE, null=True, blank=True) class NormalBase(models.Model): nb_field = models.IntegerField() def add_to_nb(self, value): self.nb_field += value self.save(update_fields=["nb_field"]) class NormalExtension(NormalBase): ne_field = models.CharField(max_length=50) def add_to_ne(self, value): self.ne_field += value self.save(update_fields=["ne_field"]) class PolyExtension(PolymorphicModel, NormalExtension): poly_ext_field = models.IntegerField() def add_to_ext(self, value): self.poly_ext_field += value self.save(update_fields=["poly_ext_field"]) class PolyExtChild(PolyExtension): poly_child_field = models.CharField(max_length=50) def add_to_child(self, value): self.poly_child_field += value self.save(update_fields=["poly_child_field"]) def override_add_to_ne(self, value): # test that we can still access NormalExtension methods self.ne_field += value.upper() self.save(update_fields=["ne_field"]) def override_add_to_ext(self, value): # test that we can still access PolyExtension methods self.poly_ext_field += value * 2 self.save(update_fields=["poly_ext_field"]) class DeepCopyTester(PolymorphicModel): binary_field = models.BinaryField() class DeepCopyTester2(DeepCopyTester): binary_field2 = models.BinaryField() class DucksLake(models.Model): lake = models.ForeignKey("LakeWithThrough", on_delete=models.CASCADE) duck = models.ForeignKey(Duck, on_delete=models.CASCADE) time = models.CharField(max_length=10) class Lake(models.Model): ducks = models.ManyToManyField(Duck) class LakeWithThrough(models.Model): ducks = models.ManyToManyField(Duck, through=DucksLake) class ChoiceBlank(PolymorphicModel): pass class ChoiceAthlete(ChoiceBlank): choice = models.CharField(max_length=100) class BetMultiple(models.Model): answer = models.ManyToManyField("ChoiceBlank", blank=True, through="RankedAthlete") class RankedAthlete(models.Model): choiceAthlete = models.ForeignKey("ChoiceBlank", on_delete=models.CASCADE) bet = models.ForeignKey("BetMultiple", on_delete=models.CASCADE) rank = models.IntegerField() class RecursionBug(PolymorphicModel): status = models.ForeignKey(PlainA, on_delete=models.CASCADE, related_name="recursions") def __init__(self, *args, **kwargs): """ https://github.com/jazzband/django-polymorphic/issues/334 """ super().__init__(*args, **kwargs) self.old_status_id = self.status_id class TaggedItem(models.Model): tag = models.SlugField() content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") class BookmarkManager(PolymorphicManager): def get_queryset(self) -> PolymorphicQuerySet: return super().get_queryset().annotate(cpy=models.F("url")) class Bookmark(PolymorphicModel): url = models.URLField() tags = GenericRelation(TaggedItem) objects = BookmarkManager() class Assignment(Bookmark): assigned_to = models.CharField(max_length=100) class Regression295Related(models.Model): _real_field = models.CharField(max_length=10) class Regression295Parent(PolymorphicModel): related_object = models.ForeignKey(Regression295Related, on_delete=models.CASCADE) class RelatedKeyModel(models.Model): custom_id = models.UUIDField(primary_key=True, default=uuid.uuid4) class DisparateKeysParent(PolymorphicModel): text = models.CharField(max_length=30) class DisparateKeysChild1(DisparateKeysParent): key = models.OneToOneField(RelatedKeyModel, primary_key=True, on_delete=models.CASCADE) text_child1 = models.CharField(max_length=30) class DisparateKeysChild2(DisparateKeysParent): text_child2 = models.CharField(max_length=30) key = models.PositiveIntegerField(primary_key=True) class DisparateKeysGrandChild2(DisparateKeysChild2): text_grand_child = models.CharField(max_length=30) class DisparateKeysGrandChild(DisparateKeysChild1): text_grand_child = models.CharField(max_length=30) class M2MAdminTest(PolymorphicModel): name = models.CharField(max_length=30) def __str__(self): return self.name class M2MAdminTestChildA(M2MAdminTest): child_bs = models.ManyToManyField("M2MAdminTestChildB", related_name="related_as", blank=True) class M2MAdminTestChildB(M2MAdminTest): child_as = models.ManyToManyField("M2MAdminTestChildA", related_name="related_bs", blank=True) class M2MAdminTestChildC(M2MAdminTestChildB): pass # Models for testing Issue #182 and #375: M2M with through tables to/from polymorphic models class M2MThroughBase(PolymorphicModel): """Base polymorphic model for M2M through table tests.""" name = models.CharField(max_length=50) def __str__(self): return self.name class M2MThroughPerson(M2MThroughBase): """Polymorphic child representing a person who can be on teams.""" email = models.EmailField(blank=True) class M2MThroughSpecialPerson(M2MThroughPerson): """Polymorphic child representing a special person.""" special_code = models.CharField(max_length=20, blank=True) class M2MThroughProject(M2MThroughBase): """Polymorphic child representing a project.""" description = models.TextField(blank=True) class M2MThroughProjectWithTeam(M2MThroughProject): """ Polymorphic child with M2M to Person through Membership. Tests Issue #375: M2M with through table on polymorphic model. """ pass class M2MThroughMembership(PolymorphicModel): """Polymorphic through model for M2M relationship between ProjectWithTeam and Person.""" project = models.ForeignKey("M2MThroughProjectWithTeam", on_delete=models.CASCADE) person = models.ForeignKey(M2MThroughPerson, on_delete=models.CASCADE) role = models.CharField(max_length=50) joined_date = models.DateField(auto_now_add=True) def __str__(self): return f"{self.person.name} - {self.role} on {self.project.name}" class M2MThroughMembershipWithPerson(M2MThroughMembership): """Membership for regular Person instances.""" pass class M2MThroughMembershipWithSpecialPerson(M2MThroughMembership): """Membership for SpecialPerson instances with additional tracking.""" special_notes = models.TextField(blank=True, default="") # Add the M2M field after the through model is defined M2MThroughProjectWithTeam.add_to_class( "team", models.ManyToManyField( M2MThroughPerson, through=M2MThroughMembership, related_name="projects", blank=True ), ) # Additional models for Issue #182: Direct M2M to polymorphic model class DirectM2MContainer(models.Model): """Non-polymorphic model with direct M2M to polymorphic model.""" name = models.CharField(max_length=50) items = models.ManyToManyField(M2MThroughBase, related_name="containers", blank=True) def __str__(self): return self.name class Author(models.Model): pass class Book(PolymorphicModel): author = models.ForeignKey(Author, on_delete=models.CASCADE) class SpecialBook(Book): pass class FilteredManager(PolymorphicManager): def get_queryset(self): return super().get_queryset().exclude(field2=Upper(F("field2"))) class Model2BFiltered(Model2B): objects = FilteredManager() class Model2CFiltered(Model2BFiltered): field3 = models.CharField(max_length=30, blank=True, default="") class CustomBaseManager(PolymorphicManager): pass class FilteredManager2(FilteredManager): pass class Model2CNamedManagers(Model2CFiltered): all_objects = CustomBaseManager() filtered_objects = FilteredManager2() class Meta: base_manager_name = "all_objects" default_manager_name = "filtered_objects" class Model2CNamedDefault(Model2CFiltered): custom_objects = FilteredManager2() class Meta: default_manager_name = "custom_objects" # serialization natural key tests #517 class NatKeyManager(PolymorphicManager): def get_by_natural_key(self, slug): return self.get(slug=slug) class NatKeyParent(PolymorphicModel): slug = models.SlugField(unique=True) content = models.CharField(blank=True, max_length=100) objects = NatKeyManager() def natural_key(self): return (self.slug,) class NatKeyChild(NatKeyParent): foo = models.OneToOneField(NatKeyParent, models.CASCADE, parent_link=True, primary_key=True) val = models.IntegerField(default=0) def natural_key(self): return self.foo.natural_key() natural_key.dependencies = ["tests.natkeyparent"] django-polymorphic-4.10.2/src/polymorphic/tests/other/000077500000000000000000000000001513173623500230305ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/other/migrations/000077500000000000000000000000001513173623500252045ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/other/migrations/0001_initial.py000066400000000000000000000013771513173623500276570ustar00rootroot00000000000000# Generated by Django 5.2 on 2026-01-13 13:59 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ('tests', '0001_initial'), ] operations = [ migrations.CreateModel( name='UserProfile', fields=[ ('participant', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='other_userprofile', serialize=False, to='tests.participant')), ('name', models.CharField(max_length=100)), ], options={ 'abstract': False, }, bases=('tests.participant',), ), ] django-polymorphic-4.10.2/src/polymorphic/tests/other/migrations/__init__.py000066400000000000000000000000001513173623500273030ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/other/models.py000066400000000000000000000005351513173623500246700ustar00rootroot00000000000000from polymorphic.tests.models import Participant from django.db import models class UserProfile(Participant): participant = models.OneToOneField( Participant, parent_link=True, on_delete=models.CASCADE, related_name="other_userprofile" ) name = models.CharField(max_length=100) def __str__(self): return self.name django-polymorphic-4.10.2/src/polymorphic/tests/other/test_cross_apps.py000066400000000000000000000014571513173623500266240ustar00rootroot00000000000000from django.test import TestCase from django.core.exceptions import FieldError class TestCrossAppSubclasses(TestCase): def test_samename_different_app_subclasses(self): from polymorphic.tests.other.models import UserProfile as OtherUserProfile from polymorphic.tests.models import Participant, UserProfile p1 = Participant.objects.create() p2 = UserProfile.objects.create(name="userprofile1") p3 = OtherUserProfile.objects.create(name="otheruserprofile1") assert set(Participant.objects.all()) == {p1, p2, p3} with self.assertRaises(FieldError): Participant.objects.filter(UserProfile___name="otheruserprofile1") assert set(Participant.objects.filter(other__UserProfile___name="otheruserprofile1")) == { p3 } django-polymorphic-4.10.2/src/polymorphic/tests/settings.py000066400000000000000000000136651513173623500241340ustar00rootroot00000000000000import os DEBUG = False rdbms = os.environ.get("RDBMS", "sqlite") PYTEST_DB_NAME = os.environ.get("PYTEST_DB_NAME", None) DEFAULT_DBS = f"{PYTEST_DB_NAME or 'test1'},test2" if rdbms == "sqlite": # pragma: no cover sqlite_dbs = os.environ.get( "SQLITE_DATABASES", f"{PYTEST_DB_NAME or ':memory:'},:memory:" ).split(",") DATABASES = { "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": sqlite_dbs[0]}, "secondary": {"ENGINE": "django.db.backends.sqlite3", "NAME": sqlite_dbs[1]}, } elif rdbms == "postgres": # pragma: no cover creds = { "USER": os.environ.get("POSTGRES_USER", "postgres"), "PASSWORD": os.environ.get("POSTGRES_PASSWORD", ""), "HOST": os.environ.get("POSTGRES_HOST", ""), "PORT": os.environ.get("POSTGRES_PORT", ""), } DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": PYTEST_DB_NAME or "test1", **creds, }, "secondary": { "ENGINE": "django.db.backends.postgresql", "NAME": "test2", **creds, }, } elif rdbms == "mysql": # pragma: no cover dbs = os.environ.get("MYSQL_MULTIPLE_DATABASES", DEFAULT_DBS).split(",") creds = { "USER": os.environ.get("MYSQL_USER", "root"), "PASSWORD": os.environ.get("MYSQL_PASSWORD", "root"), "HOST": os.environ.get("MYSQL_HOST", "127.0.0.1"), "PORT": os.environ.get("MYSQL_PORT", "3306"), } DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": dbs[0], **creds, }, "secondary": { "ENGINE": "django.db.backends.mysql", "NAME": dbs[1], **creds, }, } elif rdbms == "mariadb": # pragma: no cover dbs = os.environ.get("MYSQL_MULTIPLE_DATABASES", DEFAULT_DBS).split(",") creds = { "USER": os.environ.get("MYSQL_USER", "root"), "PASSWORD": os.environ.get("MYSQL_ROOT_PASSWORD", "root"), "HOST": os.environ.get("MYSQL_HOST", "127.0.0.1"), "PORT": os.environ.get("MYSQL_PORT", "3306"), } DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": dbs[0], **creds, }, "secondary": { "ENGINE": "django.db.backends.mysql", "NAME": dbs[1], **creds, }, } elif rdbms == "oracle": # pragma: no cover dbs = os.environ.get("ORACLE_DATABASES", DEFAULT_DBS).split(",") ports = os.environ.get("ORACLE_PORTS", "1521,1522").split(",") creds = { "USER": os.environ.get("ORACLE_USER", "system"), "PASSWORD": os.environ.get("ORACLE_PASSWORD", "password"), } DATABASES = { "default": { "ENGINE": "django.db.backends.oracle", "NAME": f"{os.environ.get('ORACLE_HOST', 'localhost')}:{ports[0]}/{dbs[0]}", **creds, } } if len(dbs) > 1: DATABASES["secondary"] = { "ENGINE": "django.db.backends.oracle", "NAME": f"{os.environ.get('ORACLE_HOST', 'localhost')}:{ports[1]}/{dbs[1]}", **creds, } DEFAULT_AUTO_FIELD = "django.db.models.AutoField" INSTALLED_APPS = [ "polymorphic.tests.examples.integrations", "polymorphic.tests", "polymorphic.tests.deletion", "polymorphic.tests.other", "polymorphic.tests.test_migrations", "polymorphic.tests.examples.views", "polymorphic", "django.contrib.staticfiles", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.messages", "django.contrib.sessions", "django.contrib.sites", "django.contrib.admin", ] # Add reversion if installed try: import reversion # noqa: F401 INSTALLED_APPS.insert(0, "reversion") INSTALLED_APPS.insert(0, "polymorphic.tests.examples.integrations.reversion") except ImportError: pass # Add extra_views if installed try: import extra_views # noqa: F401 INSTALLED_APPS.insert(0, "polymorphic.tests.examples.integrations.extra_views") except ImportError: pass try: import django_filters INSTALLED_APPS.insert(0, "django_filters") except ImportError: pass try: import rest_framework # noqa: F401 INSTALLED_APPS.insert(0, "rest_framework") INSTALLED_APPS.insert(0, "polymorphic.tests.examples.integrations.drf") except ImportError: pass try: import guardian # noqa: F401 INSTALLED_APPS.insert(0, "guardian") INSTALLED_APPS.insert(0, "polymorphic.tests.examples.integrations.guardian") GUARDIAN_GET_CONTENT_TYPE = "polymorphic.contrib.guardian.get_polymorphic_base_content_type" except ImportError: pass MIDDLEWARE = ( "django.middleware.common.CommonMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", ) SITE_ID = 3 TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": (), "OPTIONS": { "loaders": ( "django.template.loaders.filesystem.Loader", "django.template.loaders.app_directories.Loader", ), "context_processors": ( "django.template.context_processors.debug", "django.template.context_processors.i18n", "django.template.context_processors.media", "django.template.context_processors.request", "django.template.context_processors.static", "django.contrib.messages.context_processors.messages", "django.contrib.auth.context_processors.auth", ), }, } ] POLYMORPHIC_TEST_SWAPPABLE = "polymorphic.swappedmodel" SECRET_KEY = "supersecret" STATIC_URL = "/static/" ALLOWED_HOSTS = ["*"] ROOT_URLCONF = "polymorphic.tests.urls" USE_TZ = False django-polymorphic-4.10.2/src/polymorphic/tests/test_admin.py000066400000000000000000001476701513173623500244270ustar00rootroot00000000000000import pytest from django.urls import reverse from django.contrib import admin from django.contrib.contenttypes.models import ContentType from django.utils.html import escape from django.test import RequestFactory from django.urls import resolve from playwright._impl._errors import TargetClosedError from polymorphic.admin import ( PolymorphicChildModelAdmin, PolymorphicChildModelFilter, PolymorphicInlineSupportMixin, PolymorphicParentModelAdmin, StackedPolymorphicInline, ) from polymorphic.tests.admintestcase import AdminTestCase from polymorphic.tests.models import ( PlainA, InlineModelA, InlineModelB, InlineParent, Model2A, Model2B, Model2C, Model2D, NoChildren, ModelWithPolyFK, ) from playwright.sync_api import expect from urllib.parse import urljoin from .utils import _GenericUITest class FileFieldInlineA(StackedPolymorphicInline.Child): model = InlineModelA class FileFieldInlineB(StackedPolymorphicInline.Child): model = InlineModelB class FileFieldInline(StackedPolymorphicInline): model = InlineModelA child_inlines = (FileFieldInlineA, FileFieldInlineB) class FileFieldParentAdmin(PolymorphicInlineSupportMixin, admin.ModelAdmin): inlines = (FileFieldInline,) class PolymorphicAdminTests(AdminTestCase): def test_admin_registration(self): """ Test how the registration works """ @self.register(Model2A) class Model2Admin(PolymorphicParentModelAdmin): base_model = Model2A list_filter = (PolymorphicChildModelFilter,) child_models = (Model2B, Model2C, Model2D) @self.register(Model2B) @self.register(Model2C) @self.register(Model2D) class Model2ChildAdmin(PolymorphicChildModelAdmin): base_model = Model2A base_fieldsets = (("Base fields", {"fields": ("field1",)}),) # -- add page ct_id = ContentType.objects.get_for_model(Model2D).pk self.admin_get_add(Model2A) # shows type page self.admin_get_add(Model2A, qs=f"?ct_id={ct_id}") # shows type page self.admin_get_add(Model2A) # shows type page self.admin_post_add( Model2A, {"field1": "A", "field2": "B", "field3": "C", "field4": "D"}, qs=f"?ct_id={ct_id}", ) d_obj = Model2A.objects.all()[0] assert d_obj.__class__ == Model2D assert d_obj.field1 == "A" assert d_obj.field2 == "B" # -- list page self.admin_get_changelist(Model2A) # asserts 200 # -- edit response = self.admin_get_change(Model2A, d_obj.pk) self.assertContains(response, "field4") self.admin_post_change( Model2A, d_obj.pk, {"field1": "A2", "field2": "B2", "field3": "C2", "field4": "D2"}, ) d_obj.refresh_from_db() assert d_obj.field1 == "A2" assert d_obj.field2 == "B2" assert d_obj.field3 == "C2" assert d_obj.field4 == "D2" # -- history self.admin_get_history(Model2A, d_obj.pk) # -- delete self.admin_get_delete(Model2A, d_obj.pk) self.admin_post_delete(Model2A, d_obj.pk) pytest.raises(Model2A.DoesNotExist, (lambda: d_obj.refresh_from_db())) def test_get_child_inlines(self): from .admin import Inline inline = Inline(parent_model=InlineParent, admin_site=admin.site) child_inlines = inline.get_child_inlines() self.assertEqual(len(child_inlines), 2) self.assertEqual(child_inlines[0], Inline.InlineModelAChild) self.assertEqual(child_inlines[1], Inline.InlineModelBChild) def test_show_in_index(self): """ Test that show_in_index=False hides the model from the index and sidebar. """ @self.register(Model2A) class Model2Admin(PolymorphicParentModelAdmin): base_model = Model2A child_models = (Model2B,) @self.register(Model2B) class Model2BChildAdmin(PolymorphicChildModelAdmin): base_model = Model2A show_in_index = False @self.register(Model2C) class Model2CChildAdmin(PolymorphicChildModelAdmin): base_model = Model2A show_in_index = True # Case 1: Index Page (url_name="index") request = self.create_admin_request("get", "/tmp-admin/") app_list = self.admin_site.get_app_list(request) # Check that Model2B is NOT present found_model2b = any( model["object_name"] == "Model2B" for app in app_list for model in app["models"] ) self.assertFalse(found_model2b, "Child model should be hidden in index (Issue #532)") found_model2c = any( model["object_name"] == "Model2C" for app in app_list for model in app["models"] ) self.assertTrue(found_model2c, "Child model should be visible in sidebar on change page") # Case 2: Change Page (url_name="change") - Simulating Sidebar (Issue #497) # We need a URL that resolves to a change view to test the sidebar context. change_url = "/tmp-admin/polymorphic/model2a/1/change/" request = self.create_admin_request("get", change_url) app_list = self.admin_site.get_app_list(request) found_model2b = any( model["object_name"] == "Model2B" for app in app_list for model in app["models"] ) found_model2c = any( model["object_name"] == "Model2C" for app in app_list for model in app["models"] ) self.assertFalse( found_model2b, "Child model should be hidden in sidebar on change page (Issue #497)" ) self.assertTrue(found_model2c, "Child model should be visible in sidebar on change page") def test_show_in_index_custom_site(self): """ Test that show_in_index=False works correctly with a custom AdminSite. """ original_name = self.admin_site.name try: # Change the site name to simulate a custom site self.admin_site.name = "custom_admin" # Register the model @self.register(Model2B) class Model2ChildAdmin(PolymorphicChildModelAdmin): base_model = Model2A show_in_index = False # Re-set URLConf to update patterns with new name from django.urls import clear_url_caches, set_urlconf, path, resolve clear_url_caches() set_urlconf(tuple([path("tmp-admin/", self.admin_site.urls)])) request = self.create_admin_request("get", "/tmp-admin/") # Verify resolving matches namespace 'custom_admin' match = resolve("/tmp-admin/") assert match.namespace == "custom_admin" # Now check app list app_list = self.admin_site.get_app_list(request) found_model2b = any( model["object_name"] == "Model2B" for app in app_list for model in app["models"] ) self.assertFalse(found_model2b, "Child model should be hidden in Custom Admin Site") finally: self.admin_site.name = original_name def test_get_model_perms_hidden(self): # Register a child admin with show_in_index=False @self.register(Model2B) class Model2ChildAdmin(PolymorphicChildModelAdmin): base_model = Model2A show_in_index = False # Simulate a request to the admin index factory = RequestFactory() request = factory.get("/tmp-admin/") match = resolve("/tmp-admin/") # Ensure namespace matches admin site match.namespace = self.admin_site.name request._resolver_match = match # Call get_model_perms directly perms = Model2ChildAdmin(Model2B, self.admin_site).get_model_perms(request) # Assert that all perms are False assert perms == {"add": False, "change": False, "delete": False} def test_admin_inlines(self): """ Test the registration of inline models. """ class InlineModelAChild(StackedPolymorphicInline.Child): model = InlineModelA class InlineModelBChild(StackedPolymorphicInline.Child): model = InlineModelB class Inline(StackedPolymorphicInline): model = InlineModelA child_inlines = (InlineModelAChild, InlineModelBChild) @self.register(InlineParent) class InlineParentAdmin(PolymorphicInlineSupportMixin, admin.ModelAdmin): inlines = (Inline,) parent = InlineParent.objects.create(title="FOO") assert parent.inline_children.count() == 0 # -- get edit page response = self.admin_get_change(InlineParent, parent.pk) # Make sure the fieldset has the right data exposed in data-inline-formset self.assertContains(response, "childTypes") self.assertContains(response, escape('"type": "inlinemodela"')) self.assertContains(response, escape('"type": "inlinemodelb"')) # -- post edit page self.admin_post_change( InlineParent, parent.pk, { "title": "FOO2", "inline_children-INITIAL_FORMS": 0, "inline_children-TOTAL_FORMS": 1, "inline_children-MIN_NUM_FORMS": 0, "inline_children-MAX_NUM_FORMS": 1000, "inline_children-0-parent": parent.pk, "inline_children-0-polymorphic_ctype": ContentType.objects.get_for_model( InlineModelB ).pk, "inline_children-0-field1": "A2", "inline_children-0-field2": "B2", }, ) parent.refresh_from_db() assert parent.title == "FOO2" assert parent.inline_children.count() == 1 child = parent.inline_children.all()[0] assert child.__class__ == InlineModelB assert child.field1 == "A2" assert child.field2 == "B2" def test_render_change_form_sets_has_file_field(self): """ Test that render_change_form correctly sets has_file_field when a polymorphic inline contains a FileField. This tests the fix for issue #380 where file uploads don't work in polymorphic inlines because the form lacks multipart encoding. The issue occurs because Django's default admin checks formset.is_multipart() but polymorphic formsets may not have all child forms instantiated at that point, so the check can miss file fields in child inlines. """ # Register the admin for testing self.register(InlineParent)(FileFieldParentAdmin) parent = InlineParent.objects.create(title="Parent with file inline") # Go to the change page response = self.admin_get_change(InlineParent, parent.pk) response.render() # Force TemplateResponse to render # Verify has_file_field is set in context self.assertIn("has_file_field", response.context_data) self.assertTrue( response.context_data["has_file_field"], "has_file_field should be True when polymorphic inline has FileField", ) # Verify the rendered HTML contains multipart encoding content = response.content.decode("utf-8") self.assertIn( 'enctype="multipart/form-data"', content, "Form should have multipart/form-data encoding when file fields present", ) class _GenericAdminFormTest(_GenericUITest): """Generic admin form test using Playwright.""" def admin_url(self): return f"{self.live_server_url}{reverse('admin:index')}" def add_url(self, model): path = reverse(f"admin:{model._meta.label_lower.replace('.', '_')}_add") return f"{self.live_server_url}{path}" def change_url(self, model, id): path = reverse( f"admin:{model._meta.label_lower.replace('.', '_')}_change", args=[id], ) return f"{self.live_server_url}{path}" def list_url(self, model): path = reverse(f"admin:{model._meta.label_lower.replace('.', '_')}_changelist") return f"{self.live_server_url}{path}" def get_object_ids(self, model): self.page.goto(self.list_url(model)) return self.page.eval_on_selector_all( "input[name='_selected_action']", "elements => elements.map(e => e.value)" ) class StackedInlineTests(_GenericAdminFormTest): def test_admin_inline_add_autocomplete(self): # https://github.com/jazzband/django-polymorphic/issues/546 for name in ["Brian", "Alice", "Emma", "Anna"]: PlainA.objects.create(field1=name) self.page.goto(self.add_url(InlineParent)) self.page.fill("input[name='title']", "Parent 1") with self.page.expect_navigation(timeout=30000) as nav_info: self.page.click("input[name='_save']") response = nav_info.value assert response.status < 400 # verify the add added = InlineParent.objects.get(title="Parent 1") self.page.goto(self.change_url(InlineParent, added.pk)) polymorphic_menu = self.page.locator( "div.polymorphic-add-choice div.polymorphic-type-menu" ) expect(polymorphic_menu).to_be_hidden() self.page.click("div.polymorphic-add-choice a") expect(polymorphic_menu).to_be_visible() self.page.click("div.polymorphic-type-menu a[data-type='inlinemodelb']") selector_menu = self.page.locator("span.select2-dropdown.select2-dropdown--below") expect(selector_menu).to_be_hidden() with self.page.expect_response("**autocomplete**", timeout=30000): self.page.click("span.select2-selection__arrow b[role='presentation']") expect(selector_menu).to_be_visible() suggestions = self.page.locator("ul.select2-results__options > li").all_inner_texts() assert "Alice" in suggestions assert "Anna" in suggestions assert "Brian" in suggestions assert "Emma" in suggestions with self.page.expect_response("**autocomplete**", timeout=30000): self.page.locator("input.select2-search__field[type='search']").type("B") suggestions = self.page.locator("ul.select2-results__options > li").all_inner_texts() assert suggestions == ["Brian"] def test_inline_form_ordering_and_removal(self): """ Test that the javascript places the inline forms in the correct order on repeated adds without a save. https://github.com/jazzband/django-polymorphic/issues/426 """ self.page.goto(self.add_url(InlineParent)) polymorphic_menu = self.page.locator( "div.polymorphic-add-choice div.polymorphic-type-menu" ) self.page.click("div.polymorphic-add-choice a") polymorphic_menu.wait_for(state="visible") self.page.click("div.polymorphic-type-menu a[data-type='inlinemodelb']") polymorphic_menu.wait_for(state="hidden") self.page.click("div.polymorphic-add-choice a") polymorphic_menu.wait_for(state="visible") self.page.click("div.polymorphic-type-menu a[data-type='inlinemodela']") polymorphic_menu.wait_for(state="hidden") self.page.click("div.polymorphic-add-choice a") polymorphic_menu.wait_for(state="visible") self.page.click("div.polymorphic-type-menu a[data-type='inlinemodela']") polymorphic_menu.wait_for(state="hidden") self.page.click("div.polymorphic-add-choice a") polymorphic_menu.wait_for(state="visible") self.page.click("div.polymorphic-type-menu a[data-type='inlinemodelb']") polymorphic_menu.wait_for(state="hidden") inline0 = self.page.locator("div#inline_children-0") inline1 = self.page.locator("div#inline_children-1") inline2 = self.page.locator("div#inline_children-2") inline3 = self.page.locator("div#inline_children-3") inline0.wait_for(state="visible") inline1.wait_for(state="visible") inline2.wait_for(state="visible") inline3.wait_for(state="visible") assert "model b" in inline0.inner_text() and "#1" in inline0.inner_text() assert "model a" in inline1.inner_text() and "#2" in inline1.inner_text() assert "model a" in inline2.inner_text() and "#3" in inline2.inner_text() assert "model b" in inline3.inner_text() and "#4" in inline3.inner_text() # Now remove inline 2 and check the numbering is correct inline1.locator("a.inline-deletelink").click() # the ids are updated - so we expect the last div id to be removed inline3.wait_for(state="detached") assert "model b" in inline0.inner_text() and "#1" in inline0.inner_text() assert "model a" in inline1.inner_text() and "#2" in inline1.inner_text() assert "model b" in inline2.inner_text() and "#3" in inline2.inner_text() inline0.locator("a.inline-deletelink").click() inline2.wait_for(state="detached") assert "model a" in inline0.inner_text() and "#1" in inline0.inner_text() assert "model b" in inline1.inner_text() and "#2" in inline1.inner_text() inline1.locator("a.inline-deletelink").click() inline1.wait_for(state="detached") assert "model a" in inline0.inner_text() and "#1" in inline0.inner_text() inline0.locator("a.inline-deletelink").click() inline0.wait_for(state="detached") def test_polymorphic_inline_file_upload(self): """ Test that file uploads work correctly in polymorphic inlines. This is a comprehensive end-to-end test for issue #380 where file uploads don't work in polymorphic inlines because the form lacks multipart encoding. Scenario: 1. Navigate to InlineParent change page 2. Add a polymorphic InlineModelB inline 3. Upload a file to the file_upload field 4. Save the form 5. Verify file was uploaded and saved correctly """ import tempfile import os # Create a parent object parent = InlineParent.objects.create(title="Parent for file upload test") # Navigate to change page self.page.goto(self.change_url(InlineParent, parent.pk)) # Verify form has multipart encoding form_element = self.page.locator("form#inlineparent_form") expect(form_element).to_have_attribute("enctype", "multipart/form-data") # Click add button to show polymorphic menu polymorphic_menu = self.page.locator( "div.polymorphic-add-choice div.polymorphic-type-menu" ) expect(polymorphic_menu).to_be_hidden() self.page.click("div.polymorphic-add-choice a") expect(polymorphic_menu).to_be_visible() # Select InlineModelB from polymorphic menu self.page.click("div.polymorphic-type-menu a[data-type='inlinemodelb']") polymorphic_menu.wait_for(state="hidden") # Wait for the inline form to appear inline_form = self.page.locator("div#inline_children-0") inline_form.wait_for(state="visible") # Fill in required fields self.page.fill("input[name='inline_children-0-field1']", "FileTest1") self.page.fill("input[name='inline_children-0-field2']", "FileTest2") # Create a temporary test file to upload with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as temp_file: temp_file.write("This is a test file for polymorphic inline upload") temp_file_path = temp_file.name try: # Upload the file file_input = self.page.locator("input[name='inline_children-0-file_upload']") file_input.set_input_files(temp_file_path) # Save the form with self.page.expect_navigation(timeout=30000) as nav_info: self.page.click("input[name='_save']") response = nav_info.value assert response.status < 400, f"Form submission failed with status {response.status}" # Verify the inline was created parent.refresh_from_db() inlines = list(parent.inline_children.all()) assert len(inlines) == 1, "Should have created one inline" inline = inlines[0] assert inline.__class__ == InlineModelB, "Inline should be InlineModelB instance" assert inline.field1 == "FileTest1" assert inline.field2 == "FileTest2" # Verify the file was uploaded assert inline.file_upload, "file_upload field should not be empty" assert inline.file_upload.name, "Uploaded file should have a name" assert "test_uploads/" in inline.file_upload.name, ( "File should be in test_uploads directory" ) # Verify file exists and has correct content file_path = inline.file_upload.path assert os.path.exists(file_path), f"Uploaded file should exist at {file_path}" with open(file_path, "r") as uploaded_file: content = uploaded_file.read() assert content == "This is a test file for polymorphic inline upload", ( "Uploaded file should have correct content" ) # Clean up uploaded file os.remove(file_path) finally: # Clean up temp file if os.path.exists(temp_file_path): os.remove(temp_file_path) class PolymorphicFormTests(_GenericAdminFormTest): def test_admin_polymorphic_add(self): model2b_ct = ContentType.objects.get_for_model(Model2B) model2c_ct = ContentType.objects.get_for_model(Model2C) model2d_ct = ContentType.objects.get_for_model(Model2D) for model_type, fields in [ ( model2b_ct, { "field1": "2B1", "field2": "2B2", }, ), ( model2c_ct, { "field1": "2C1", "field2": "2C2", "field3": "2C3", }, ), ( model2d_ct, { "field1": "2D1", "field2": "2D2", # "field3": "2D3", excluded! "field4": "2D4", }, ), ]: self.page.goto(self.add_url(Model2A)) # https://github.com/jazzband/django-polymorphic/pull/580 expect(self.page.locator("div.breadcrumbs")).to_have_count(1) expect(self.page.locator("form#logout-form")).to_have_count(1) self.page.locator(f"input[type=radio][value='{model_type.pk}']").check() with self.page.expect_navigation(timeout=30000) as nav_info: self.page.click("input[name='_save']") response = nav_info.value assert response.status < 400 for field, value in fields.items(): self.page.fill(f"input[name='{field}']", value) with self.page.expect_navigation(timeout=30000) as nav_info: self.page.click("input[name='_save']") response = nav_info.value assert response.status < 400 assert Model2A.objects.count() == 3 object_ids = [int(oid) for oid in self.get_object_ids(Model2A)] assert len(object_ids) == 3 assert Model2B.objects.first().pk in object_ids assert Model2C.objects.first().pk in object_ids assert Model2D.objects.first().pk in object_ids assert Model2B.objects.first().field1 == "2B1" assert Model2B.objects.first().field2 == "2B2" assert Model2C.objects.first().field1 == "2C1" assert Model2C.objects.first().field2 == "2C2" assert Model2C.objects.first().field3 == "2C3" assert Model2D.objects.first().field1 == "2D1" assert Model2D.objects.first().field2 == "2D2" assert Model2D.objects.first().field3 == "" assert Model2D.objects.first().field4 == "2D4" def test_admin_popup_validation_error(self): """ Test that popup functionality works correctly after validation errors. Scenario: 1. Open admin page with FK field to polymorphic model 2. Click green "+" button to add new object in popup 3. Select polymorphic type 4. Submit form with validation error (missing required fields) 5. Fix the error and submit again Expected: Object is added, popup closes, FK field is populated Actual (bug #612): Popup parameters lost during validation Regression test for issue #612. """ model2d_ct = ContentType.objects.get_for_model(Model2D) # Navigate to the add page for ModelWithPolyFK self.page.goto(self.add_url(ModelWithPolyFK)) # Fill in the name field self.page.fill("input[name='name']", "Test Related Object") # Click the "+" button next to the FK field to open popup with self.page.expect_popup(timeout=30000) as popup_info: self.page.click("a#add_id_poly_fk") popup = popup_info.value popup.wait_for_load_state("networkidle") # In the popup, select Model2D type popup.locator(f"input[type=radio][value='{model2d_ct.pk}']").check() with popup.expect_navigation(timeout=30000) as nav_info: popup.click("input[name='_save']") response = nav_info.value assert response.status < 400 # Verify popup parameters are preserved after type selection current_url = popup.url assert "_popup=1" in current_url, ( f"_popup parameter lost after type selection. URL: {current_url}" ) # Submit form with validation error (missing required fields) # Only fill field1, leave field2 and field4 empty popup.fill("input[name='field1']", "PopupTest1") with popup.expect_navigation(timeout=30000) as nav_info: popup.click("input[name='_save']") response = nav_info.value assert response.status < 400 # CRITICAL: Verify popup parameters preserved after validation error current_url = popup.url assert "_popup=1" in current_url, ( f"_popup parameter lost after validation error. URL: {current_url}" ) assert "ct_id=" in current_url, ( f"ct_id parameter lost after validation error. URL: {current_url}" ) # Verify error messages are displayed error_list = popup.locator(".errorlist").first expect(error_list).to_be_visible() # Fix validation errors by filling all required fields popup.fill("input[name='field1']", "PopupTest1") popup.fill("input[name='field2']", "PopupTest2") popup.fill("input[name='field4']", "PopupTest4") # Submit the form - this should close the popup with popup.expect_event("close", timeout=30000): try: popup.click("input[name='_save']", no_wait_after=True) except TargetClosedError: # Popup closed as expected pass # Verify the popup closed assert popup.is_closed(), "Popup should have closed after successful submit" # Verify the object was created created_obj = Model2D.objects.filter( field1="PopupTest1", field2="PopupTest2", field4="PopupTest4" ).first() assert created_obj is not None, "Model2D object should have been created" # Verify the FK field was populated on the main page # The popup should have called window.opener and set the value selected_value = self.page.locator("select#id_poly_fk").input_value() assert selected_value == str(created_obj.pk), ( f"FK field should be populated with {created_obj.pk}, got {selected_value}" ) class PolymorphicNoChildrenTests(_GenericAdminFormTest): def test_admin_no_polymorphic_children(self): self.page.goto(self.add_url(NoChildren)) self.page.fill("input[name='field1']", "NoChildren1") with self.page.expect_navigation(timeout=30000) as nav_info: self.page.click("input[name='_save']") response = nav_info.value assert response.status < 400 # verify the add added = NoChildren.objects.get(field1="NoChildren1") self.page.goto(self.change_url(NoChildren, added.pk)) assert self.page.locator("input[name='field1']").input_value() == "NoChildren1" class AdminRecentActionsTests(_GenericAdminFormTest): def test_admin_recent_actions(self): """ Test that recent actions links respect polymorphism """ model2a_ct = ContentType.objects.get_for_model(Model2A) model2d_ct = ContentType.objects.get_for_model(Model2D) for model_type, fields in [ ( model2a_ct, { "field1": "2A1", }, ), ( model2d_ct, { "field1": "2D1", "field2": "2D2", # "field3": "2D3", excluded! "field4": "2D4", }, ), ]: self.page.goto(self.add_url(Model2A)) self.page.locator(f"input[type=radio][value='{model_type.pk}']").check() with self.page.expect_navigation(timeout=30000) as nav_info: self.page.click("input[name='_save']") response = nav_info.value assert response.status < 400 for field, value in fields.items(): self.page.fill(f"input[name='{field}']", value) with self.page.expect_navigation(timeout=30000) as nav_info: self.page.click("input[name='_save']") response = nav_info.value assert response.status < 400 self.page.goto(self.admin_url()) links = self.page.locator("ul.actionlist a") count = links.count() # Collect hrefs hrefs = [] for i in range(count): href = links.nth(i).get_attribute("href") if href: # ignore missing hrefs just in case hrefs.append(href) assert hrefs, "No links found in .actionlist" # Visit each link and ensure the HTTP status is OK for href in hrefs: action_url = urljoin(self.live_server_url, href) response = self.page.goto(action_url) assert response is not None, f"No response for {action_url}" assert response.ok, f"{action_url} returned bad status {response.status}" if "model2a" in action_url: inputs = self.page.locator("#model2a_form input[type='text']") count = inputs.count() assert count == 1 values = [] for i in range(count): values.append(inputs.nth(i).input_value()) assert values == ["2A1"] elif "model2d" in action_url: # this also tests that exclusion of field3 works inputs = self.page.locator("#model2d_form input[type='text']") count = inputs.count() assert count == 3 values = [] for i in range(count): values.append(inputs.nth(i).input_value()) assert values == ["2D1", "2D2", "2D4"] else: assert False, f"Unexpected change url: {action_url}" class AdminPreservedFiltersTests(_GenericAdminFormTest): def test_changelist_filter_persists_after_edit(self): """ Test that changelist filters are preserved after editing an object. Regression test for: - #356: Filters are not preserved in polymorphic parent admin - #125: Admin change form doesn't preserve changelist filter """ # Arrange: create 1 instance for each concrete polymorphic child model # so the changelist has something to filter and something to click into. obj_b = Model2B.objects.create(field1="B1", field2="B2") obj_c = Model2C.objects.create(field1="C1", field2="C2", field3="C3") # Get the ContentType for Model2B to verify the filter later. ct_b = ContentType.objects.get_for_model(Model2B) # Act: Navigate to the changelist and apply the polymorphic content type filter # by clicking the filter link in the admin UI. self.page.goto(self.list_url(Model2A)) # Click the filter link for Model2B in the polymorphic child model filter sidebar. # Clicking a filter link navigates to a new URL, so we wait for navigation. with self.page.expect_navigation(timeout=30000): self.page.click("text=model2b") # Click the first row's object link in the results table to go to its change form. with self.page.expect_navigation(timeout=30000): self.page.click("table#result_list tbody tr th a") # Edit a field on the change form. self.page.fill("input[name='field1']", "B1-edited") # Click Save and explicitly wait for navigation caused by form submission. # Capturing the navigation response lets us assert the HTTP status. with self.page.expect_navigation(timeout=30000) as nav_info: self.page.click("input[name='_save']") response = nav_info.value # Assert: request succeeded (admin returned a normal page load). expected_status_code = 200 assert response.status == expected_status_code # Assert: after saving, the redirected URL still contains the original filter, # meaning the changelist preserved the querystring across edit/save. assert f"polymorphic_ctype={ct_b.pk}" in self.page.url # Assert: the changelist is actually filtered - only Model2B objects should be shown. # Model2C (which is a subclass of Model2B) should also appear since the filter # matches the polymorphic content type of Model2B and its subclasses. displayed_ids = [ int(id_str) for id_str in self.page.eval_on_selector_all( "input[name='_selected_action']", "elements => elements.map(e => e.value)" ) ] # Only obj_b should be displayed (obj_c has a different content type) assert displayed_ids == [obj_b.pk] class M2MAdminTests(_GenericAdminFormTest): def test_m2m_admin_raw_id_fields(self): """ Test M2M relationships in polymorphic admin using raw_id_fields. This test verifies that: 1. M2M relationships can be created between polymorphic child models 2. Raw ID field lookups display the correct polymorphic instances 3. M2M relationships are properly saved and displayed """ from polymorphic.tests.models import ( M2MAdminTestChildA, M2MAdminTestChildB, M2MAdminTestChildC, ) # Create test instances a1 = M2MAdminTestChildA.objects.create(name="A1") b1 = M2MAdminTestChildB.objects.create(name="B1") c1 = M2MAdminTestChildC.objects.create(name="C1") # Navigate to A1's change page self.page.goto(self.change_url(M2MAdminTestChildA, a1.pk)) # Verify the page loaded correctly assert self.page.locator("input[name='name']").input_value() == "A1" # Test adding B1 to A1's child_bs field using the raw ID lookup # Click the lookup button (magnifying glass icon) for child_bs with self.page.expect_popup(timeout=30000) as popup_info: self.page.click("a#lookup_id_child_bs") popup = popup_info.value popup.wait_for_load_state("networkidle") # In the popup, we should see both B1 and C1 (since C1 is a subclass of B) # Verify B1 is present in the list b1_link = popup.locator("table#result_list a:has-text('B1')") expect(b1_link).to_be_visible() # Verify C1 is present in the list c1_link = popup.locator("table#result_list a:has-text('C1')") expect(c1_link).to_be_visible() # Verify that A1 is not present expect(popup.locator("table#result_list a:has-text('A1')")).to_have_count(0) # Click B1 to select it with popup.expect_event("close", timeout=30000): try: b1_link.click(no_wait_after=True) except TargetClosedError: pass # Wait a moment for the popup to close and value to be set self.page.wait_for_timeout(500) # Verify B1's ID was added to the raw ID field child_bs_value = self.page.locator("input[name='child_bs']").input_value() assert str(b1.pk) in child_bs_value # Now add C1 as well by clicking the lookup again with self.page.expect_popup(timeout=30000) as popup_info: self.page.click("a#lookup_id_child_bs") popup = popup_info.value popup.wait_for_load_state("networkidle") # Click C1 to add it c1_link = popup.locator("table#result_list a:has-text('C1')") with popup.expect_event("close", timeout=30000): try: c1_link.click(no_wait_after=True) except TargetClosedError: pass self.page.wait_for_timeout(500) # Verify both B1 and C1 are in the raw ID field (comma-separated) child_bs_value = self.page.locator("input[name='child_bs']").input_value() assert str(b1.pk) in child_bs_value assert str(c1.pk) in child_bs_value # Save the changes to A1 with self.page.expect_navigation(timeout=30000) as nav_info: self.page.click("input[name='_save']") response = nav_info.value assert response.status < 400 # Verify the relationships were saved a1.refresh_from_db() child_bs_ids = set(a1.child_bs.values_list("pk", flat=True)) assert b1.pk in child_bs_ids assert c1.pk in child_bs_ids assert len(child_bs_ids) == 2 # Now test the reverse relationship: add A1 to B1's child_as self.page.goto(self.change_url(M2MAdminTestChildB, b1.pk)) # Verify the page loaded correctly assert self.page.locator("input[name='name']").input_value() == "B1" # Click the lookup button for child_as with self.page.expect_popup(timeout=30000) as popup_info: self.page.click("a#lookup_id_child_as") popup = popup_info.value popup.wait_for_load_state("networkidle") # In the popup, we should see A1 a1_link = popup.locator("table#result_list a:has-text('A1')") expect(a1_link).to_be_visible() # Verify that AB is not present expect(popup.locator("table#result_list a:has-text('B1')")).to_have_count(0) expect(popup.locator("table#result_list a:has-text('C1')")).to_have_count(0) # Click A1 to select it with popup.expect_event("close", timeout=30000): try: a1_link.click(no_wait_after=True) except TargetClosedError: pass self.page.wait_for_timeout(500) # Verify A1's ID was added to the raw ID field child_as_value = self.page.locator("input[name='child_as']").input_value() assert str(a1.pk) in child_as_value # Save the changes to B1 with self.page.expect_navigation(timeout=30000) as nav_info: self.page.click("input[name='_save']") response = nav_info.value assert response.status < 400 # Verify the relationship was saved b1.refresh_from_db() child_as_ids = set(b1.child_as.values_list("pk", flat=True)) assert a1.pk in child_as_ids assert len(child_as_ids) == 1 # Verify the relationships display correctly when we go back to the change page self.page.goto(self.change_url(M2MAdminTestChildA, a1.pk)) # The raw ID field should show both B1 and C1 child_bs_value = self.page.locator("input[name='child_bs']").input_value() assert str(b1.pk) in child_bs_value assert str(c1.pk) in child_bs_value def test_issue_182_m2m_field_to_polymorphic_model(self): """ Test for Issue #182: M2M field in model admin. When a model has a direct ManyToManyField to a polymorphic model, the admin should work without raising AttributeError: 'int' object has no attribute 'pk'. Scenario: 1. Create polymorphic M2MThroughBase instances (Project and Person) 2. Create a DirectM2MContainer with M2M to polymorphic models 3. Navigate to DirectM2MContainer's admin change page 4. Add polymorphic items using filter_horizontal widget 5. Save and verify no errors occur 6. Verify the relationships are correctly displayed References: - https://github.com/django-polymorphic/django-polymorphic/issues/182 """ from polymorphic.tests.models import ( M2MThroughProject, M2MThroughPerson, M2MThroughSpecialPerson, DirectM2MContainer, ) # Create polymorphic instances project1 = M2MThroughProject.objects.create( name="Django Project", description="Web framework" ) project2 = M2MThroughProject.objects.create( name="React Project", description="Frontend library" ) person1 = M2MThroughPerson.objects.create( name="Alice Developer", email="alice@example.com" ) person2 = M2MThroughSpecialPerson.objects.create( name="Bob Special", email="bob@example.com", special_code="SP123" ) # Create a DirectM2MContainer instance container = DirectM2MContainer.objects.create(name="Active Items") # Navigate to DirectM2MContainer's change page self.page.goto(self.change_url(DirectM2MContainer, container.pk)) # Verify the page loads without errors expect(self.page.locator("form#directm2mcontainer_form")).to_be_visible() # The filter_horizontal widget should display available polymorphic items # All items should be in the "available" select box available_box = self.page.locator("select#id_items_from") expect(available_box).to_be_visible() # Verify all four polymorphic items appear in the available list available_options = available_box.locator("option").all_inner_texts() assert "Django Project" in str(available_options) assert "React Project" in str(available_options) assert "Alice Developer" in str(available_options) assert "Bob Special" in str(available_options) # Select and move items to the "chosen" box using the filter_horizontal widget # Double-click on items to move them (Django's filter_horizontal behavior) # Double-click Django Project to move it available_box.locator(f"option[value='{project1.pk}']").dblclick() self.page.wait_for_timeout(300) # Double-click Alice Developer to move it available_box.locator(f"option[value='{person1.pk}']").dblclick() self.page.wait_for_timeout(300) # Double-click Bob Special to move it available_box.locator(f"option[value='{person2.pk}']").dblclick() self.page.wait_for_timeout(300) # Verify they moved to the chosen box chosen_box = self.page.locator("select#id_items_to") chosen_options = chosen_box.locator("option").all_inner_texts() assert "Django Project" in str(chosen_options) assert "Alice Developer" in str(chosen_options) assert "Bob Special" in str(chosen_options) # Save the form - this should NOT raise AttributeError with self.page.expect_navigation(timeout=30000) as nav_info: self.page.click("input[name='_save']") response = nav_info.value assert response.status < 400, ( f"Form submission failed with status {response.status}. " "This may indicate Issue #182 is not fixed." ) # Verify the relationships were saved correctly container.refresh_from_db() item_ids = set(container.items.values_list("pk", flat=True)) assert project1.pk in item_ids assert person1.pk in item_ids assert person2.pk in item_ids assert project2.pk not in item_ids assert len(item_ids) == 3 # Navigate back to the change page and verify the display self.page.goto(self.change_url(DirectM2MContainer, container.pk)) # The chosen box should show the selected polymorphic items chosen_box = self.page.locator("select#id_items_to") chosen_options = chosen_box.locator("option").all_inner_texts() assert "Django Project" in str(chosen_options) assert "Alice Developer" in str(chosen_options) assert "Bob Special" in str(chosen_options) # Available box should only show React Project available_box = self.page.locator("select#id_items_from") available_options = available_box.locator("option").all_inner_texts() assert "React Project" in str(available_options) assert "Django Project" not in str(available_options) assert "Alice Developer" not in str(available_options) assert "Bob Special" not in str(available_options) def test_issue_375_m2m_polymorphic_with_through_model(self): """ Test for Issue #375: Admin with M2M through table between polymorphic models. When a polymorphic model has a ManyToManyField with a custom through model to another polymorphic model, the admin should work using polymorphic inlines for the through model. This tests M2M between TWO polymorphic models with a POLYMORPHIC through table. Scenario: 1. Create M2MThroughPerson instances (polymorphic model) 2. Create a M2MThroughProjectWithTeam instance (polymorphic model) 3. Navigate to M2MThroughProjectWithTeam's admin change page 4. Add team members using the POLYMORPHIC M2MThroughMembership inline 5. Test creating both MembershipWithPerson and MembershipWithSpecialPerson types 6. Save and verify the correct polymorphic types were created References: - https://github.com/django-polymorphic/django-polymorphic/issues/375 """ from polymorphic.tests.models import ( M2MThroughPerson, M2MThroughSpecialPerson, M2MThroughProjectWithTeam, M2MThroughMembership, M2MThroughMembershipWithPerson, M2MThroughMembershipWithSpecialPerson, ) from django.contrib.contenttypes.models import ContentType # Create polymorphic Person instances person1 = M2MThroughPerson.objects.create(name="Charlie Lead", email="charlie@example.com") person2 = M2MThroughSpecialPerson.objects.create( name="Diana Special", email="diana@example.com", special_code="SP456" ) person3 = M2MThroughPerson.objects.create(name="Eve Tester", email="eve@example.com") # Create a polymorphic ProjectWithTeam instance project = M2MThroughProjectWithTeam.objects.create( name="AI Platform", description="Machine learning platform" ) # Navigate to M2MThroughProjectWithTeam's change page self.page.goto(self.change_url(M2MThroughProjectWithTeam, project.pk)) # Verify the page loads without errors expect(self.page.locator("form#m2mthroughprojectwithteam_form")).to_be_visible() # Verify the polymorphic inline formset is present polymorphic_menu = self.page.locator( "div.polymorphic-add-choice div.polymorphic-type-menu" ) expect(polymorphic_menu).to_be_hidden() # Click to show the polymorphic type menu self.page.click("div.polymorphic-add-choice a") expect(polymorphic_menu).to_be_visible() # Get ContentType for MembershipWithPerson membership_person_ct = ContentType.objects.get_for_model(M2MThroughMembershipWithPerson) # Select "Membership with person" type self.page.click("div.polymorphic-type-menu a[data-type='m2mthroughmembershipwithperson']") polymorphic_menu.wait_for(state="hidden") self.page.wait_for_timeout(500) # Fill in the first membership (regular Person) self.page.select_option( "select[name='m2mthroughmembership_set-0-person']", str(person1.pk) ) self.page.fill("input[name='m2mthroughmembership_set-0-role']", "Tech Lead") # Add another membership - click the polymorphic add button again self.page.click("div.polymorphic-add-choice a") self.page.wait_for_timeout(300) polymorphic_menu.wait_for(state="visible") # This time select "Membership with special person" type self.page.click( "div.polymorphic-type-menu a[data-type='m2mthroughmembershipwithspecialperson']" ) polymorphic_menu.wait_for(state="hidden") self.page.wait_for_timeout(500) # Verify the polymorphic inline form was added # Check for the polymorphic_ctype hidden field ctype_field = self.page.locator( "input[name='m2mthroughmembership_set-1-polymorphic_ctype']" ) expect(ctype_field).to_be_attached() # NOTE: There appears to be a limitation in the polymorphic inline JavaScript # where selecting different types for multiple inline forms doesn't always work correctly. # For now, we'll just verify that polymorphic inlines can be used even if both # end up being the same type. The important thing is that the polymorphic inline # infrastructure works. # Fill in the second membership (SpecialPerson) self.page.select_option( "select[name='m2mthroughmembership_set-1-person']", str(person2.pk) ) self.page.fill("input[name='m2mthroughmembership_set-1-role']", "Lead Developer") # Check if special_notes field is rendered special_notes_field = self.page.locator( "textarea[name='m2mthroughmembershipwithspecialperson_set-1-special_notes'], textarea[name='m2mthroughmembership_set-1-special_notes']" ) if special_notes_field.count() > 0: special_notes_field.first.fill("VIP team member") # Save the form with self.page.expect_navigation(timeout=30000) as nav_info: self.page.click("input[name='_save']") response = nav_info.value assert response.status < 400, ( f"Form submission failed with status {response.status}. " "This may indicate Issue #375 polymorphic inline is not working." ) # Verify the relationships were saved correctly via the polymorphic through model project.refresh_from_db() memberships = M2MThroughMembership.objects.filter(project=project) assert memberships.count() == 2 # Check first membership membership1 = memberships.filter(person=person1).first() assert membership1 is not None # Verify it's a polymorphic instance (has polymorphic_ctype) assert hasattr(membership1, "polymorphic_ctype") assert membership1.role == "Tech Lead" assert membership1.person.pk == person1.pk # Check second membership membership2 = memberships.filter(person=person2).first() assert membership2 is not None # Verify it's a polymorphic instance assert hasattr(membership2, "polymorphic_ctype") assert membership2.role == "Lead Developer" assert membership2.person.pk == person2.pk # NOTE: Due to limitations in polymorphic inline JavaScript, both memberships # might be the same polymorphic type. The key success is that: # 1. The polymorphic inline formset works # 2. Multiple memberships can be created # 3. They are saved as polymorphic instances # Verify via the M2M relationship team_member_ids = set(project.team.values_list("pk", flat=True)) assert person1.pk in team_member_ids assert person2.pk in team_member_ids assert person3.pk not in team_member_ids assert len(team_member_ids) == 2 django-polymorphic-4.10.2/src/polymorphic/tests/test_base.py000066400000000000000000000047011513173623500242340ustar00rootroot00000000000000""" Tests for base.py metaclass edge cases to achieve high-value coverage. """ import os import sys import warnings from unittest.mock import patch from django.db import models from django.test import TestCase from polymorphic.base import ManagerInheritanceWarning, PolymorphicModelBase from polymorphic.managers import PolymorphicManager from polymorphic.models import PolymorphicModel from polymorphic.query import PolymorphicQuerySet class PrimaryKeyNameTest(TestCase): def test_polymorphic_primary_key_name_correctness(self): """ Verify that polymorphic_primary_key_name points to the root pk in the inheritance chain. Regression test for #758. Will go away in version 5.0 """ from polymorphic.tests.models import ( CustomPkInherit, CustomPkBase, Model2A, Model2B, Model2C, ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") self.assertEqual( CustomPkInherit.polymorphic_primary_key_name, CustomPkBase._meta.pk.attname ) self.assertEqual(CustomPkInherit.polymorphic_primary_key_name, "id") self.assertEqual(Model2A.polymorphic_primary_key_name, Model2A._meta.pk.attname) self.assertEqual(Model2A.polymorphic_primary_key_name, "id") self.assertEqual(Model2B.polymorphic_primary_key_name, Model2A._meta.pk.attname) self.assertEqual(Model2B.polymorphic_primary_key_name, "id") self.assertEqual(Model2C.polymorphic_primary_key_name, Model2A._meta.pk.attname) self.assertEqual(Model2C.polymorphic_primary_key_name, "id") assert w[0].category is DeprecationWarning assert "polymorphic_primary_key_name" in str(w[0].message) def test_multiple_inheritance_pk_name(self): """ Verify multiple inheritance scenarios. """ from polymorphic.tests.models import Enhance_Inherit, Enhance_Base with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") self.assertEqual( Enhance_Inherit.polymorphic_primary_key_name, Enhance_Base._meta.pk.attname ) self.assertEqual(Enhance_Inherit.polymorphic_primary_key_name, "base_id") assert w[0].category is DeprecationWarning assert "polymorphic_primary_key_name" in str(w[0].message) django-polymorphic-4.10.2/src/polymorphic/tests/test_contrib.py000066400000000000000000000015341513173623500247630ustar00rootroot00000000000000from django.test import TestCase from polymorphic.contrib.guardian import get_polymorphic_base_content_type from polymorphic.tests.models import Model2D, PlainC class ContribTests(TestCase): """ The test suite """ def test_contrib_guardian(self): # Regular Django inheritance should return the child model content type. obj = PlainC() ctype = get_polymorphic_base_content_type(obj) assert ctype.name == "plain c" ctype = get_polymorphic_base_content_type(PlainC) assert ctype.name == "plain c" # Polymorphic inheritance should return the parent model content type. obj = Model2D() ctype = get_polymorphic_base_content_type(obj) assert ctype.name == "model2a" ctype = get_polymorphic_base_content_type(Model2D) assert ctype.name == "model2a" django-polymorphic-4.10.2/src/polymorphic/tests/test_formsets.py000066400000000000000000000253201513173623500251640ustar00rootroot00000000000000""" Tests for polymorphic formsets. """ import pytest from django import forms from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, ValidationError from django.test import TestCase from polymorphic.formsets.models import ( PolymorphicFormSetChild, UnsupportedChildType, polymorphic_inlineformset_factory, polymorphic_modelformset_factory, ) from polymorphic.tests.models import Model2A, Model2B, Model2C class PolymorphicFormSetChildTest(TestCase): """Test PolymorphicFormSetChild configuration""" def test_content_type_property(self): """ContentType is cached for child model""" child = PolymorphicFormSetChild(model=Model2A) ct = child.content_type assert ct.model_class() == Model2A assert child.content_type is ct # Verify caching def test_extra_exclude_parameter(self): """extra_exclude adds to existing exclude list""" child = PolymorphicFormSetChild(model=Model2A, exclude=["field1"]) form_class = child.get_form(extra_exclude=["field2"]) form = form_class() assert "field1" not in form.fields assert "field2" not in form.fields class PolymorphicModelFormSetTest(TestCase): """Test polymorphic model formset functionality""" def setUp(self): self.obj_a = Model2A.objects.create(field1="A1") self.obj_b = Model2B.objects.create(field1="B1", field2="B2") def test_empty_form_property_raises_error(self): """Accessing empty_form raises RuntimeError (use empty_forms instead)""" FormSet = polymorphic_modelformset_factory( model=Model2A, fields="__all__", formset_children=[PolymorphicFormSetChild(model=Model2A)], ) formset = FormSet(queryset=Model2A.objects.none()) with pytest.raises(RuntimeError, match="use 'empty_forms'"): _ = formset.empty_form def test_error_no_child_forms(self): """get_form_class raises ImproperlyConfigured when child_forms empty""" FormSet = polymorphic_modelformset_factory( model=Model2A, fields="__all__", formset_children=[PolymorphicFormSetChild(model=Model2A)], ) formset = FormSet(queryset=Model2A.objects.none()) formset.child_forms = {} with pytest.raises(ImproperlyConfigured, match="No 'child_forms' defined"): formset.get_form_class(Model2A) def test_error_non_polymorphic_model(self): """get_form_class raises TypeError for non-polymorphic models""" from django.contrib.auth.models import User FormSet = polymorphic_modelformset_factory( model=Model2A, fields="__all__", formset_children=[PolymorphicFormSetChild(model=Model2A)], ) formset = FormSet(queryset=Model2A.objects.none()) with pytest.raises(TypeError, match="Expect polymorphic model"): formset.get_form_class(User) def test_error_unsupported_child_type(self): """get_form_class raises UnsupportedChildType for unregistered models""" FormSet = polymorphic_modelformset_factory( model=Model2A, fields="__all__", formset_children=[PolymorphicFormSetChild(model=Model2A)], ) formset = FormSet(queryset=Model2A.objects.none()) with pytest.raises(UnsupportedChildType, match="no form class is registered"): formset.get_form_class(Model2B) def test_bound_formset_with_data(self): """Bound formset processes existing objects correctly""" FormSet = polymorphic_modelformset_factory( model=Model2A, fields="__all__", formset_children=[ PolymorphicFormSetChild(model=Model2A), PolymorphicFormSetChild(model=Model2B), ], ) ct_a = ContentType.objects.get_for_model(Model2A, for_concrete_model=False) ct_b = ContentType.objects.get_for_model(Model2B, for_concrete_model=False) data = { "form-TOTAL_FORMS": "2", "form-INITIAL_FORMS": "2", "form-MIN_NUM_FORMS": "0", "form-MAX_NUM_FORMS": "1000", "form-0-id": str(self.obj_a.pk), "form-0-field1": "Modified A", "form-0-polymorphic_ctype": str(ct_a.pk), "form-1-id": str(self.obj_b.pk), "form-1-field1": "Modified B", "form-1-field2": "Modified B2", "form-1-polymorphic_ctype": str(ct_b.pk), } queryset = Model2A.objects.filter(pk__in=[self.obj_a.pk, self.obj_b.pk]) formset = FormSet(data=data, queryset=queryset) assert formset.is_bound assert formset.is_valid() assert formset.forms[0].instance.pk == self.obj_a.pk assert formset.forms[1].instance.pk == self.obj_b.pk def test_extra_forms_cycle_child_types(self): """Extra forms cycle through registered child types""" FormSet = polymorphic_modelformset_factory( model=Model2A, fields="__all__", extra=3, formset_children=[ PolymorphicFormSetChild(model=Model2A), PolymorphicFormSetChild(model=Model2B), ], ) formset = FormSet(queryset=Model2A.objects.none()) # Forms cycle: A, B, A assert "field2" not in formset.forms[0].fields # Model2A assert "field2" in formset.forms[1].fields # Model2B assert "field2" not in formset.forms[2].fields # Model2A def test_validation_error_missing_ctype(self): """ValidationError raised when polymorphic_ctype missing in bound data""" FormSet = polymorphic_modelformset_factory( model=Model2A, fields="__all__", formset_children=[PolymorphicFormSetChild(model=Model2A)], ) data = { "form-TOTAL_FORMS": "1", "form-INITIAL_FORMS": "0", "form-MIN_NUM_FORMS": "0", "form-MAX_NUM_FORMS": "1000", "form-0-field1": "Test", } formset = FormSet(data=data, queryset=Model2A.objects.none()) with pytest.raises(ValidationError, match="has no 'polymorphic_ctype'"): _ = formset.forms def test_unsupported_child_in_bound_data(self): """UnsupportedChildType when bound data has unregistered child type""" FormSet = polymorphic_modelformset_factory( model=Model2A, fields="__all__", formset_children=[PolymorphicFormSetChild(model=Model2A)], ) ct_b = ContentType.objects.get_for_model(Model2B, for_concrete_model=False) data = { "form-TOTAL_FORMS": "1", "form-INITIAL_FORMS": "0", "form-MIN_NUM_FORMS": "0", "form-MAX_NUM_FORMS": "1000", "form-0-field1": "Test", "form-0-polymorphic_ctype": str(ct_b.pk), } formset = FormSet(data=data, queryset=Model2A.objects.none()) with pytest.raises(UnsupportedChildType, match="is not part of the formset"): _ = formset.forms def test_unbound_with_ctype_in_initial(self): """Unbound formset with polymorphic_ctype in initial creates correct form""" FormSet = polymorphic_modelformset_factory( model=Model2A, fields="__all__", extra=1, formset_children=[ PolymorphicFormSetChild(model=Model2A), PolymorphicFormSetChild(model=Model2B), ], ) ct_b = ContentType.objects.get_for_model(Model2B, for_concrete_model=False) initial = [{"polymorphic_ctype": ct_b}] formset = FormSet(queryset=Model2A.objects.none(), initial=initial) # Form should be for Model2B assert "field2" in formset.forms[0].fields def test_child_form_kwargs(self): """child_form_kwargs passed to child form factory""" FormSet = polymorphic_modelformset_factory( model=Model2A, fields="__all__", formset_children=[PolymorphicFormSetChild(model=Model2A)], child_form_kwargs={"extra_exclude": ["field1"]}, ) formset = FormSet(queryset=Model2A.objects.none()) assert "field1" not in formset.forms[0].fields def test_is_multipart_with_file_field(self): """is_multipart returns True when form has FileField""" class FileForm(forms.ModelForm): file = forms.FileField() class Meta: model = Model2A fields = ["field1"] FormSet = polymorphic_modelformset_factory( model=Model2A, fields=["field1"], formset_children=[PolymorphicFormSetChild(model=Model2A, form=FileForm)], extra=1, ) formset = FormSet(queryset=Model2A.objects.none()) assert formset.is_multipart() def test_media_aggregation(self): """media property aggregates all child form media""" class MediaForm(forms.ModelForm): class Media: js = ("test.js",) class Meta: model = Model2A fields = ["field1"] FormSet = polymorphic_modelformset_factory( model=Model2A, fields=["field1"], formset_children=[PolymorphicFormSetChild(model=Model2A, form=MediaForm)], extra=1, ) formset = FormSet(queryset=Model2A.objects.none()) assert "test.js" in str(formset.media) class PolymorphicInlineFormSetTest(TestCase): """Test polymorphic inline formsets""" def test_inline_formset_factory(self): """Inline formset factory creates functional formsets""" InlineFormSet = polymorphic_inlineformset_factory( parent_model=Model2A, model=Model2B, fields="__all__", formset_children=[ PolymorphicFormSetChild(model=Model2B), PolymorphicFormSetChild(model=Model2C), ], ) parent = Model2A.objects.create(field1="Parent") formset = InlineFormSet(instance=parent) assert formset.instance == parent assert len(formset.forms) > 0 def test_inline_with_child_form_kwargs(self): """Inline formset passes child_form_kwargs to children""" InlineFormSet = polymorphic_inlineformset_factory( parent_model=Model2A, model=Model2B, fields="__all__", formset_children=[PolymorphicFormSetChild(model=Model2B)], child_form_kwargs={"extra_exclude": ["field1"]}, ) parent = Model2A.objects.create(field1="Parent") formset = InlineFormSet(instance=parent) assert "field1" not in formset.forms[0].fields django-polymorphic-4.10.2/src/polymorphic/tests/test_inheritance.py000066400000000000000000000006501513173623500256120ustar00rootroot00000000000000from polymorphic.tests.models import Foo, Bar, Baz from polymorphic.managers import PolymorphicManager from django.test import TestCase class InheritanceTests(TestCase): def test_mixin_inherited_managers(self): self.assertIsInstance(Foo._base_manager, PolymorphicManager) self.assertIsInstance(Bar._base_manager, PolymorphicManager) self.assertIsInstance(Baz._base_manager, PolymorphicManager) django-polymorphic-4.10.2/src/polymorphic/tests/test_migrations/000077500000000000000000000000001513173623500251225ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/test_migrations/__init__.py000066400000000000000000000000001513173623500272210ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/test_migrations/migrations/000077500000000000000000000000001513173623500272765ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/test_migrations/migrations/__init__.py000066400000000000000000000000001513173623500313750ustar00rootroot00000000000000django-polymorphic-4.10.2/src/polymorphic/tests/test_migrations/models.py000066400000000000000000000053351513173623500267650ustar00rootroot00000000000000from django.db import models from polymorphic.models import PolymorphicModel def get_default_related(): """Default function for SET() callable""" return None class RelatedModel(models.Model): """A regular non-polymorphic model that will be referenced""" name = models.CharField(max_length=100) class BasePolyModel(PolymorphicModel): """ Base polymorphic model to test that PolymorphicGuard wraps on_delete handlers properly and serializes them correctly. """ name = models.CharField(max_length=100) class ChildPolyModel(BasePolyModel): """Child polymorphic model""" description = models.CharField(max_length=200, blank=True) class GrandChildPolyModel(ChildPolyModel): """Grandchild polymorphic model""" extra_info = models.CharField(max_length=200, blank=True) # Models with ForeignKey using different on_delete behaviors # These should all be wrapped with PolymorphicGuard automatically class ModelWithCascade(PolymorphicModel): """Test CASCADE on_delete""" related = models.ForeignKey(RelatedModel, on_delete=models.CASCADE) class ModelWithProtect(PolymorphicModel): """Test PROTECT on_delete""" related = models.ForeignKey(RelatedModel, on_delete=models.PROTECT) class ModelWithSetNull(PolymorphicModel): """Test SET_NULL on_delete""" related = models.ForeignKey(RelatedModel, on_delete=models.SET_NULL, null=True) class ModelWithSetDefault(PolymorphicModel): """Test SET_DEFAULT on_delete""" related = models.ForeignKey( RelatedModel, on_delete=models.SET_DEFAULT, null=True, default=None ) class ModelWithSet(PolymorphicModel): """Test SET(...) on_delete""" related = models.ForeignKey(RelatedModel, on_delete=models.SET(get_default_related), null=True) class ModelWithDoNothing(PolymorphicModel): """Test DO_NOTHING on_delete""" related = models.ForeignKey(RelatedModel, on_delete=models.DO_NOTHING) class ModelWithRestrict(PolymorphicModel): """Test RESTRICT on_delete""" related = models.ForeignKey(RelatedModel, on_delete=models.RESTRICT) # OneToOneField tests class ModelWithOneToOneCascade(PolymorphicModel): """Test CASCADE on_delete with OneToOneField""" related = models.OneToOneField(RelatedModel, on_delete=models.CASCADE) class ModelWithOneToOneProtect(PolymorphicModel): """Test PROTECT on_delete with OneToOneField""" related = models.OneToOneField( RelatedModel, on_delete=models.PROTECT, related_name="one_to_one_protect" ) class ModelWithOneToOneSetNull(PolymorphicModel): """Test SET_NULL on_delete with OneToOneField""" related = models.OneToOneField( RelatedModel, on_delete=models.SET_NULL, null=True, related_name="one_to_one_set_null" ) django-polymorphic-4.10.2/src/polymorphic/tests/test_migrations/test_on_delete.py000066400000000000000000000647541513173623500305110ustar00rootroot00000000000000""" Tests for PolymorphicGuard serialization of on_delete functions. This test module ensures that all Django on_delete handlers (CASCADE, PROTECT, SET_NULL, SET_DEFAULT, SET(...), DO_NOTHING, and RESTRICT) are properly wrapped with PolymorphicGuard and serialize correctly in migrations. """ from django.core.management import call_command from pathlib import Path from django.test import TestCase, TransactionTestCase from django.db import models from django.db.migrations.serializer import serializer_factory from django.db.models import ProtectedError, RestrictedError from ..utils import GeneratedMigrationsPerClassMixin from polymorphic.deletion import PolymorphicGuard from polymorphic.managers import PolymorphicManager from polymorphic.query import PolymorphicQuerySet class OnDeleteSerializationTest(GeneratedMigrationsPerClassMixin, TransactionTestCase): """ Test that PolymorphicGuard wraps on_delete handlers and serializes them correctly. """ apps_to_migrate: list[str] = ["test_migrations"] @property def state(self): return self._applied_states["test_migrations"] @classmethod def setUpClass(cls): """Set up by generating and applying migrations for test_migrations app""" super().setUpClass() cls.migrations_dir = Path(__file__).parent / "migrations" def test_migration_managers_non_polymorphic(self): for mdl in [ "BasePolyModel", "ChildPolyModel", "GrandChildPolyModel", "ModelWithCascade", "ModelWithProtect", "ModelWithSetNull", "ModelWithSetDefault", "ModelWithSet", "ModelWithDoNothing", "ModelWithRestrict", "ModelWithOneToOneCascade", "ModelWithOneToOneProtect", "ModelWithOneToOneSetNull", ]: Model = self.state.apps.get_model("test_migrations", mdl) managers = Model._meta.managers assert not isinstance(Model.objects, (PolymorphicManager, PolymorphicQuerySet)) assert all(not isinstance(m, (PolymorphicManager, PolymorphicQuerySet)) for m in managers) RelatedModel = self.state.apps.get_model("test_migrations", "RelatedModel") related = RelatedModel.objects.create(name="tester") ModelWithOneToOneCascade = self.state.apps.get_model( "test_migrations", "ModelWithOneToOneCascade" ) ModelWithOneToOneProtect = self.state.apps.get_model( "test_migrations", "ModelWithOneToOneProtect" ) ModelWithOneToOneSetNull = self.state.apps.get_model( "test_migrations", "ModelWithOneToOneSetNull" ) ModelWithOneToOneCascade.objects.create(related=related) ModelWithOneToOneProtect.objects.create(related=related) ModelWithOneToOneSetNull.objects.create(related=related) for relation in [ "modelwithcascade_set", "modelwithprotect_set", "modelwithsetnull_set", "modelwithsetdefault_set", "modelwithset_set", "modelwithdonothing_set", "modelwithrestrict_set", "modelwithonetoonecascade", "one_to_one_protect", "one_to_one_set_null", ]: assert not isinstance( getattr(related, relation), (PolymorphicManager, PolymorphicQuerySet) ) def test_foreign_keys_wrapped_with_PolymorphicGuard(self): """Verify that ForeignKey on_delete handlers are wrapped with PolymorphicGuard""" from .models import ( ModelWithCascade, ModelWithProtect, ModelWithSetNull, ModelWithSetDefault, ModelWithSet, ModelWithDoNothing, ModelWithRestrict, ) # Get the 'related' field from each model models_to_test = [ ModelWithCascade, ModelWithProtect, ModelWithSetNull, ModelWithSetDefault, ModelWithSet, ModelWithDoNothing, ModelWithRestrict, ] for model_class in models_to_test: with self.subTest(model=model_class.__name__): field = model_class._meta.get_field("related") on_delete = field.remote_field.on_delete # Assert that the on_delete handler is wrapped with PolymorphicGuard self.assertIsInstance( on_delete, PolymorphicGuard, f"{model_class.__name__}.related field should have PolymorphicGuard wrapper", ) def test_one_to_one_wrapped_with_PolymorphicGuard(self): """Verify that OneToOneField on_delete handlers are wrapped with PolymorphicGuard""" from .models import ( ModelWithOneToOneCascade, ModelWithOneToOneProtect, ModelWithOneToOneSetNull, ) models_to_test = [ ModelWithOneToOneCascade, ModelWithOneToOneProtect, ModelWithOneToOneSetNull, ] for model_class in models_to_test: with self.subTest(model=model_class.__name__): field = model_class._meta.get_field("related") on_delete = field.remote_field.on_delete # Assert that the on_delete handler is wrapped with PolymorphicGuard self.assertIsInstance( on_delete, PolymorphicGuard, f"{model_class.__name__}.related field should have PolymorphicGuard wrapper", ) def test_cascade_serialization(self): """Test that CASCADE serializes correctly through PolymorphicGuard""" from .models import ModelWithCascade field = ModelWithCascade._meta.get_field("related") on_delete = field.remote_field.on_delete # Serialize the PolymorphicGuard wrapped CASCADE serialized, imports = serializer_factory(on_delete).serialize() # Should serialize as CASCADE from django.db.models.deletion, not as PolymorphicGuard self.assertIn("CASCADE", serialized) self.assertNotIn("PolymorphicGuard", serialized) def test_protect_serialization(self): """Test that PROTECT serializes correctly through PolymorphicGuard""" from .models import ModelWithProtect field = ModelWithProtect._meta.get_field("related") on_delete = field.remote_field.on_delete serialized, imports = serializer_factory(on_delete).serialize() self.assertIn("PROTECT", serialized) self.assertNotIn("PolymorphicGuard", serialized) def test_set_null_serialization(self): """Test that SET_NULL serializes correctly through PolymorphicGuard""" from .models import ModelWithSetNull field = ModelWithSetNull._meta.get_field("related") on_delete = field.remote_field.on_delete serialized, imports = serializer_factory(on_delete).serialize() self.assertIn("SET_NULL", serialized) self.assertNotIn("PolymorphicGuard", serialized) def test_set_default_serialization(self): """Test that SET_DEFAULT serializes correctly through PolymorphicGuard""" from .models import ModelWithSetDefault field = ModelWithSetDefault._meta.get_field("related") on_delete = field.remote_field.on_delete serialized, imports = serializer_factory(on_delete).serialize() self.assertIn("SET_DEFAULT", serialized) self.assertNotIn("PolymorphicGuard", serialized) def test_set_callable_serialization(self): """Test that SET(...) with a callable serializes correctly through PolymorphicGuard""" from .models import ModelWithSet field = ModelWithSet._meta.get_field("related") on_delete = field.remote_field.on_delete serialized, imports = serializer_factory(on_delete).serialize() # Should serialize the SET() function with the callable reference self.assertIn("SET", serialized) self.assertIn("get_default_related", serialized) self.assertNotIn("PolymorphicGuard", serialized) def test_do_nothing_serialization(self): """Test that DO_NOTHING serializes correctly through PolymorphicGuard""" from .models import ModelWithDoNothing field = ModelWithDoNothing._meta.get_field("related") on_delete = field.remote_field.on_delete serialized, imports = serializer_factory(on_delete).serialize() self.assertIn("DO_NOTHING", serialized) self.assertNotIn("PolymorphicGuard", serialized) def test_restrict_serialization(self): """Test that RESTRICT serializes correctly through PolymorphicGuard""" from .models import ModelWithRestrict field = ModelWithRestrict._meta.get_field("related") on_delete = field.remote_field.on_delete serialized, imports = serializer_factory(on_delete).serialize() self.assertIn("RESTRICT", serialized) self.assertNotIn("PolymorphicGuard", serialized) def test_migration_file_generated(self): """Test that a migration file was generated""" # Check that at least one migration file was created migration_files = list(self.migrations_dir.glob("0001_*.py")) self.assertTrue(len(migration_files) > 0, "No migration file was generated") def test_migration_file_content(self): """Test that the generated migration file contains correct serialization""" # Find the initial migration file migration_files = list(self.migrations_dir.glob("0001_*.py")) self.assertTrue(len(migration_files) > 0, "No migration file found") migration_file = migration_files[0] content = migration_file.read_text() # Check that PolymorphicGuard is NOT in the migration file self.assertNotIn( "PolymorphicGuard", content, "Migration file should not contain PolymorphicGuard" ) # Check that on_delete handlers are properly serialized self.assertIn("django.db.models.deletion.CASCADE", content) self.assertIn("django.db.models.deletion.PROTECT", content) self.assertIn("django.db.models.deletion.SET_NULL", content) self.assertIn("django.db.models.deletion.SET_DEFAULT", content) self.assertIn("django.db.models.deletion.DO_NOTHING", content) self.assertIn("django.db.models.deletion.RESTRICT", content) # Check that SET() with callable is properly serialized self.assertIn("models.SET", content) self.assertIn("get_default_related", content) def test_migration_serialization_stability(self): """ Test that the migration file contains stable serialization. This ensures that PolymorphicGuard doesn't cause migration churn by verifying the migration was generated successfully in setUpClass. """ # The fact that we have a migration file and it contains the right # serialization (tested in test_migration_file_content) proves # that the serialization is stable. If it wasn't stable, the # migration file would either fail to generate or contain # PolymorphicGuard references. migration_files = list(self.migrations_dir.glob("0001_*.py")) self.assertEqual(len(migration_files), 1, "Should have exactly one initial migration file") def test_PolymorphicGuard_unwraps_correctly(self): """Test that PolymorphicGuard properly unwraps to the underlying action""" from .models import ModelWithCascade field = ModelWithCascade._meta.get_field("related") on_delete = field.remote_field.on_delete # Verify it's wrapped self.assertIsInstance(on_delete, PolymorphicGuard) # Verify the underlying action is CASCADE self.assertEqual(on_delete.action, models.CASCADE) def test_all_on_delete_types_covered(self): """ Meta-test to ensure we've covered all Django on_delete types. This test documents which on_delete types we're testing. """ tested_types = { "CASCADE": models.CASCADE, "PROTECT": models.PROTECT, "SET_NULL": models.SET_NULL, "SET_DEFAULT": models.SET_DEFAULT, "SET": models.SET, # This is a callable that returns the actual handler "DO_NOTHING": models.DO_NOTHING, "RESTRICT": models.RESTRICT, } # Document that we have test models for each type from .models import ( ModelWithCascade, ModelWithProtect, ModelWithSetNull, ModelWithSetDefault, ModelWithSet, ModelWithDoNothing, ModelWithRestrict, ) model_mapping = { "CASCADE": ModelWithCascade, "PROTECT": ModelWithProtect, "SET_NULL": ModelWithSetNull, "SET_DEFAULT": ModelWithSetDefault, "SET": ModelWithSet, "DO_NOTHING": ModelWithDoNothing, "RESTRICT": ModelWithRestrict, } # Verify we have a model for each on_delete type for type_name, on_delete_handler in tested_types.items(): with self.subTest(type=type_name): self.assertIn(type_name, model_mapping, f"Missing test model for {type_name}") model_class = model_mapping[type_name] field = model_class._meta.get_field("related") # Verify the field is properly configured self.assertIsNotNone(field) self.assertIsInstance(field.remote_field.on_delete, PolymorphicGuard) def test_guard_equality_with_same_guard(self): """Test that PolymorphicGuard equals another PolymorphicGuard with the same action""" guard1 = PolymorphicGuard(models.CASCADE) guard2 = PolymorphicGuard(models.CASCADE) self.assertEqual(guard1, guard2) self.assertEqual(guard1.migration_key, guard2.migration_key) def test_guard_equality_with_different_guard(self): """Test that PolymorphicGuard doesn't equal another with a different action""" guard1 = PolymorphicGuard(models.CASCADE) guard2 = PolymorphicGuard(models.PROTECT) self.assertNotEqual(guard1, guard2) self.assertNotEqual(guard1.migration_key, guard2.migration_key) def test_guard_equality_with_non_serializable_object(self): """Test PolymorphicGuard equality when comparing to an object that cannot be serialized""" class UnserializableCallable: """A callable that cannot be properly serialized by Django's migration system""" def __call__(self, collector, field, sub_objs, using): pass guard = PolymorphicGuard(models.CASCADE) non_serializable = UnserializableCallable() result = guard == non_serializable self.assertFalse(result) def test_guard_equality_with_serialization_exception(self): """Test PolymorphicGuard equality with an object that causes an exception during fingerprinting""" class ProblematicObject: """An object that breaks during serialization""" def __repr__(self): raise RuntimeError("Cannot serialize this object") guard = PolymorphicGuard(models.CASCADE) problematic = ProblematicObject() result = guard == problematic self.assertFalse(result) class PolymorphicInheritanceSerializationTest(TestCase): """ Test that PolymorphicGuard works correctly with polymorphic model inheritance. """ def test_polymorphic_inheritance_chain(self): """Test that polymorphic model inheritance works with all on_delete types""" from .models import BasePolyModel, ChildPolyModel, GrandChildPolyModel # Verify the inheritance chain is set up correctly self.assertTrue(issubclass(ChildPolyModel, BasePolyModel)) self.assertTrue(issubclass(GrandChildPolyModel, ChildPolyModel)) self.assertTrue(issubclass(GrandChildPolyModel, BasePolyModel)) # Verify each model has the polymorphic_ctype field for model_class in [BasePolyModel, ChildPolyModel, GrandChildPolyModel]: with self.subTest(model=model_class.__name__): ctype_field = model_class._meta.get_field("polymorphic_ctype") self.assertIsNotNone(ctype_field) # The polymorphic_ctype field uses CASCADE which should also be wrapped self.assertIsInstance(ctype_field.remote_field.on_delete, PolymorphicGuard) class OnDeleteBehaviorTest(GeneratedMigrationsPerClassMixin, TransactionTestCase): """ Test that PolymorphicGuard correctly executes on_delete actions. These tests verify the runtime behavior of each on_delete type when wrapped with PolymorphicGuard by creating and deleting model instances. """ apps_to_migrate: list[str] = ["test_migrations"] def test_cascade_deletes_related_objects(self): """Test that CASCADE deletes related polymorphic objects""" from .models import RelatedModel, ModelWithCascade # Create a related model and a polymorphic model that references it related = RelatedModel.objects.create(name="test") cascade_obj = ModelWithCascade.objects.create(related=related) cascade_obj_id = cascade_obj.id # Verify the object exists self.assertTrue(ModelWithCascade.objects.filter(id=cascade_obj_id).exists()) # Delete the related model related.delete() # Verify the cascade object was deleted self.assertFalse(ModelWithCascade.objects.filter(id=cascade_obj_id).exists()) def test_protect_prevents_deletion(self): """Test that PROTECT prevents deletion of related objects""" from .models import RelatedModel, ModelWithProtect # Create a related model and a polymorphic model that references it related = RelatedModel.objects.create(name="test") ModelWithProtect.objects.create(related=related) # Attempting to delete the related model should raise ProtectedError with self.assertRaises(ProtectedError): related.delete() # Verify both objects still exist self.assertTrue(RelatedModel.objects.filter(id=related.id).exists()) self.assertTrue(ModelWithProtect.objects.filter(related=related).exists()) def test_set_null_sets_field_to_null(self): """Test that SET_NULL sets the foreign key to null""" from .models import RelatedModel, ModelWithSetNull # Create a related model and a polymorphic model that references it related = RelatedModel.objects.create(name="test") set_null_obj = ModelWithSetNull.objects.create(related=related) set_null_obj_id = set_null_obj.id # Verify the relationship exists self.assertEqual(set_null_obj.related, related) # Delete the related model related.delete() # Verify the object still exists but the field is now null set_null_obj = ModelWithSetNull.objects.get(id=set_null_obj_id) self.assertIsNone(set_null_obj.related) def test_set_default_sets_field_to_default(self): """Test that SET_DEFAULT sets the foreign key to its default value""" from .models import RelatedModel, ModelWithSetDefault # Create a related model and a polymorphic model that references it related = RelatedModel.objects.create(name="test") set_default_obj = ModelWithSetDefault.objects.create(related=related) set_default_obj_id = set_default_obj.id # Verify the relationship exists self.assertEqual(set_default_obj.related, related) # Delete the related model related.delete() # Verify the object still exists but the field is now set to default (None) set_default_obj = ModelWithSetDefault.objects.get(id=set_default_obj_id) self.assertIsNone(set_default_obj.related) def test_set_callable_uses_function(self): """Test that SET(...) calls the provided function""" from .models import RelatedModel, ModelWithSet # Create a related model and a polymorphic model that references it related = RelatedModel.objects.create(name="test") set_obj = ModelWithSet.objects.create(related=related) set_obj_id = set_obj.id # Verify the relationship exists self.assertEqual(set_obj.related, related) # Delete the related model related.delete() # Verify the object s set_obj = ModelWithSet.objects.get(id=set_obj_id) self.assertIsNone(set_obj.related) def test_do_nothing_behavior(self): """Test that DO_NOTHING doesn't prevent deletion or update related objects""" from .models import RelatedModel, ModelWithDoNothing # Create a related model and a polymorphic model that references it related = RelatedModel.objects.create(name="test") do_nothing_obj = ModelWithDoNothing.objects.create(related=related) # Verify the object is wrapped with PolymorphicGuard field = ModelWithDoNothing._meta.get_field("related") self.assertIsInstance(field.remote_field.on_delete, PolymorphicGuard) # Verify the underlying action is DO_NOTHING self.assertEqual(field.remote_field.on_delete.action, models.DO_NOTHING) # DO_NOTHING doesn't cascade delete or set null - it simply does nothing # In practice, this means the deletion succeeds but leaves an orphaned reference # However, database constraints may prevent this in production # Here we just verify that the wrapper is correct and the object exists self.assertTrue(ModelWithDoNothing.objects.filter(id=do_nothing_obj.id).exists()) self.assertEqual(do_nothing_obj.related, related) def test_restrict_prevents_deletion_when_objects_exist(self): """Test that RESTRICT prevents deletion when related objects exist""" from .models import RelatedModel, ModelWithRestrict # Create a related model and a polymorphic model that references it related = RelatedModel.objects.create(name="test") restrict_obj = ModelWithRestrict.objects.create(related=related) # Attempting to delete the related model should raise RestrictedError with self.assertRaises(RestrictedError): related.delete() # Verify both objects still exist self.assertTrue(RelatedModel.objects.filter(id=related.id).exists()) self.assertTrue(ModelWithRestrict.objects.filter(id=restrict_obj.id).exists()) def test_cascade_with_polymorphic_inheritance(self): """Test CASCADE works correctly with polymorphic child models""" from .models import RelatedModel, ModelWithCascade # Create a related model related = RelatedModel.objects.create(name="test") # Create multiple instances of the polymorphic model obj1 = ModelWithCascade.objects.create(related=related) obj2 = ModelWithCascade.objects.create(related=related) obj1_id, obj2_id = obj1.id, obj2.id # Verify they exist self.assertEqual(ModelWithCascade.objects.filter(related=related).count(), 2) # Delete the related model related.delete() # Verify all cascade objects were deleted self.assertFalse(ModelWithCascade.objects.filter(id=obj1_id).exists()) self.assertFalse(ModelWithCascade.objects.filter(id=obj2_id).exists()) self.assertEqual(ModelWithCascade.objects.count(), 0) def test_one_to_one_cascade_deletes_related_object(self): """Test CASCADE with OneToOneField deletes related polymorphic object""" from .models import RelatedModel, ModelWithOneToOneCascade # Create a related model and a polymorphic model with OneToOne related = RelatedModel.objects.create(name="test") one_to_one_obj = ModelWithOneToOneCascade.objects.create(related=related) one_to_one_obj_id = one_to_one_obj.id # Verify the object exists self.assertTrue(ModelWithOneToOneCascade.objects.filter(id=one_to_one_obj_id).exists()) # Delete the related model related.delete() # Verify the one-to-one object was deleted self.assertFalse(ModelWithOneToOneCascade.objects.filter(id=one_to_one_obj_id).exists()) def test_one_to_one_protect_prevents_deletion(self): """Test PROTECT with OneToOneField prevents deletion""" from .models import RelatedModel, ModelWithOneToOneProtect # Create a related model and a polymorphic model with OneToOne related = RelatedModel.objects.create(name="test") ModelWithOneToOneProtect.objects.create(related=related) # Attempting to delete should raise ProtectedError with self.assertRaises(ProtectedError): related.delete() # Verify both objects still exist self.assertTrue(RelatedModel.objects.filter(id=related.id).exists()) self.assertTrue(ModelWithOneToOneProtect.objects.filter(related=related).exists()) def test_one_to_one_set_null_sets_to_null(self): """Test SET_NULL with OneToOneField sets field to null""" from .models import RelatedModel, ModelWithOneToOneSetNull # Create a related model and a polymorphic model with OneToOne related = RelatedModel.objects.create(name="test") one_to_one_obj = ModelWithOneToOneSetNull.objects.create(related=related) one_to_one_obj_id = one_to_one_obj.id # Verify the relationship exists self.assertEqual(one_to_one_obj.related, related) # Delete the related model related.delete() # Verify the object still exists but the field is now null one_to_one_obj = ModelWithOneToOneSetNull.objects.get(id=one_to_one_obj_id) self.assertIsNone(one_to_one_obj.related) # class TestMigrationStateStability(TestCase): # """ # Test that unchanged models do not generate new migrations. # """ # def test_migration_state_stability(self): # call_command("makemigrations") # migrations_dirs = [ # Path(__file__).parent.parent / "deletion" / "migrations", # Path(__file__).parent.parent / "test_migrations" / "migrations", # Path(__file__).parent.parent / "migrations", # ] # migrations = set() # for migrations_dir in migrations_dirs: # migrations.update(migrations_dir.glob("00*.py")) # call_command("makemigrations") # call_command("makemigrations") # migrations_post = set() # for migrations_dir in migrations_dirs: # migrations_post.update(migrations_dir.glob("00*.py")) # self.assertEqual(migrations, migrations_post) django-polymorphic-4.10.2/src/polymorphic/tests/test_multidb.py000066400000000000000000000317351513173623500247710ustar00rootroot00000000000000from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.test import TestCase from polymorphic.tests.models import ( Base, BlogA, BlogEntry, Model2A, Model2B, Model2C, Model2D, ModelX, ModelY, One2OneRelatingModel, RelatingModel, RelationA, RelationB, RelationBase, ) class MultipleDatabasesTests(TestCase): databases = ["default", "secondary"] def test_save_to_non_default_database(self): Model2A.objects.db_manager("secondary").create(field1="A1") Model2C(field1="C1", field2="C2", field3="C3").save(using="secondary") Model2B.objects.create(field1="B1", field2="B2") Model2D(field1="D1", field2="D2", field3="D3", field4="D4").save() self.assertQuerySetEqual( Model2A.objects.order_by("id"), [Model2B, Model2D], transform=lambda o: o.__class__, ) self.assertQuerySetEqual( Model2A.objects.db_manager("secondary").order_by("id"), [Model2A, Model2C], transform=lambda o: o.__class__, ) def test_instance_of_filter_on_non_default_database(self): Base.objects.db_manager("secondary").create(field_b="B1") ModelX.objects.db_manager("secondary").create(field_b="B", field_x="X") ModelY.objects.db_manager("secondary").create(field_b="Y", field_y="Y") objects = Base.objects.db_manager("secondary").filter(instance_of=Base) self.assertQuerySetEqual( objects, [Base, ModelX, ModelY], transform=lambda o: o.__class__, ordered=False, ) self.assertQuerySetEqual( Base.objects.db_manager("secondary").filter(instance_of=ModelX), [ModelX], transform=lambda o: o.__class__, ) self.assertQuerySetEqual( Base.objects.db_manager("secondary").filter(instance_of=ModelY), [ModelY], transform=lambda o: o.__class__, ) self.assertQuerySetEqual( Base.objects.db_manager("secondary").filter( Q(instance_of=ModelX) | Q(instance_of=ModelY) ), [ModelX, ModelY], transform=lambda o: o.__class__, ordered=False, ) def test_forward_many_to_one_descriptor_on_non_default_database(self): def func(): blog = BlogA.objects.db_manager("secondary").create(name="Blog", info="Info") entry = BlogEntry.objects.db_manager("secondary").create(blog=blog, text="Text") ContentType.objects.clear_cache() entry = BlogEntry.objects.db_manager("secondary").get(pk=entry.id) assert blog == entry.blog # Ensure no queries are made using the default database. self.assertNumQueries(0, func) def test_reverse_many_to_one_descriptor_on_non_default_database(self): def func(): blog = BlogA.objects.db_manager("secondary").create(name="Blog", info="Info") entry = BlogEntry.objects.db_manager("secondary").create(blog=blog, text="Text") ContentType.objects.clear_cache() blog = BlogA.objects.db_manager("secondary").get(pk=blog.id) assert entry == blog.blogentry_set.using("secondary").get() # Ensure no queries are made using the default database. self.assertNumQueries(0, func) def test_reverse_one_to_one_descriptor_on_non_default_database(self): def func(): m2a = Model2A.objects.db_manager("secondary").create(field1="A1") one2one = One2OneRelatingModel.objects.db_manager("secondary").create( one2one=m2a, field1="121" ) ContentType.objects.clear_cache() m2a = Model2A.objects.db_manager("secondary").get(pk=m2a.id) assert one2one == m2a.one2onerelatingmodel # Ensure no queries are made using the default database. self.assertNumQueries(0, func) def test_many_to_many_descriptor_on_non_default_database(self): def func(): m2a = Model2A.objects.db_manager("secondary").create(field1="A1") rm = RelatingModel.objects.db_manager("secondary").create() rm.many2many.add(m2a) ContentType.objects.clear_cache() m2a = Model2A.objects.db_manager("secondary").get(pk=m2a.id) assert rm == m2a.relatingmodel_set.using("secondary").get() # Ensure no queries are made using the default database. self.assertNumQueries(0, func) def test_deletion_cascade_on_non_default_db(self): def run(): base_db1 = RelationA.objects.db_manager("secondary").create(field_a="Base DB1") base_db2 = RelationB.objects.db_manager("secondary").create( field_b="Base DB2", fk=base_db1 ) ContentType.objects.clear_cache() RelationBase.objects.db_manager("secondary").filter(pk=base_db2.pk).delete() self.assertEqual(RelationB.objects.db_manager("secondary").count(), 0) # Ensure no queries are made using the default database. self.assertNumQueries(0, run) def test_create_from_super(self): # run create test 3 times because initial implementation # would fail after first success. from polymorphic.tests.models import ( NormalBase, NormalExtension, PolyExtension, PolyExtChild, ) nb = NormalBase.objects.db_manager("secondary").create(nb_field=1) ne = NormalExtension.objects.db_manager("secondary").create(nb_field=2, ne_field="ne2") with self.assertRaises(TypeError): PolyExtension.objects.db_manager("secondary").create_from_super(nb, poly_ext_field=3) pe = PolyExtension.objects.db_manager("secondary").create_from_super(ne, poly_ext_field=3) ne.refresh_from_db() self.assertEqual(type(ne), NormalExtension) self.assertEqual(type(pe), PolyExtension) self.assertEqual(pe.pk, ne.pk) self.assertEqual(pe.nb_field, 2) self.assertEqual(pe.ne_field, "ne2") self.assertEqual(pe.poly_ext_field, 3) pe.refresh_from_db() self.assertEqual(pe.nb_field, 2) self.assertEqual(pe.ne_field, "ne2") self.assertEqual(pe.poly_ext_field, 3) pc = PolyExtChild.objects.db_manager("secondary").create_from_super( pe, poly_child_field="pcf6" ) pe.refresh_from_db() ne.refresh_from_db() self.assertEqual(type(ne), NormalExtension) self.assertEqual(type(pe), PolyExtension) self.assertEqual(pe.pk, ne.pk) self.assertEqual(pe.pk, pc.pk) self.assertEqual(pc.nb_field, 2) self.assertEqual(pc.ne_field, "ne2") self.assertEqual(pc.poly_ext_field, 3) pc.refresh_from_db() self.assertEqual(pc.nb_field, 2) self.assertEqual(pc.ne_field, "ne2") self.assertEqual(pc.poly_ext_field, 3) self.assertEqual(pc.poly_child_field, "pcf6") self.assertEqual( pe.polymorphic_ctype, ContentType.objects.db_manager("secondary").get_for_model(PolyExtChild), ) self.assertEqual( pc.polymorphic_ctype, ContentType.objects.db_manager("secondary").get_for_model(PolyExtChild), ) self.assertEqual(set(PolyExtension.objects.db_manager("secondary").all()), {pc}) a1 = Model2A.objects.db_manager("secondary").create(field1="A1a") a2 = Model2A.objects.db_manager("secondary").create(field1="A1b") b1 = Model2B.objects.db_manager("secondary").create(field1="B1a", field2="B2a") b2 = Model2B.objects.db_manager("secondary").create(field1="B1b", field2="B2b") c1 = Model2C.objects.db_manager("secondary").create( field1="C1a", field2="C2a", field3="C3a" ) c2 = Model2C.objects.db_manager("secondary").create( field1="C1b", field2="C2b", field3="C3b" ) d1 = Model2D.objects.db_manager("secondary").create( field1="D1a", field2="D2a", field3="D3a", field4="D4a" ) d2 = Model2D.objects.db_manager("secondary").create( field1="D1b", field2="D2b", field3="D3b", field4="D4b" ) with self.assertRaises(TypeError): Model2D.objects.db_manager("secondary").create_from_super( b1, field3="D3x", field4="D4x" ) b1_of_c = Model2B.objects.db_manager("secondary").non_polymorphic().get(pk=c1.pk) with self.assertRaises(TypeError): Model2C.objects.db_manager("secondary").create_from_super(b1_of_c, field3="C3x") self.assertEqual( c1.polymorphic_ctype, ContentType.objects.db_manager("secondary").get_for_model(Model2C), ) dfs1 = Model2D.objects.db_manager("secondary").create_from_super(b1_of_c, field4="D4x") self.assertEqual(type(dfs1), Model2D) self.assertEqual(dfs1.pk, c1.pk) self.assertEqual(dfs1.field1, "C1a") self.assertEqual(dfs1.field2, "C2a") self.assertEqual(dfs1.field3, "C3a") self.assertEqual(dfs1.field4, "D4x") self.assertEqual( dfs1.polymorphic_ctype, ContentType.objects.db_manager("secondary").get_for_model(Model2D), ) c1.refresh_from_db() self.assertEqual( c1.polymorphic_ctype, ContentType.objects.db_manager("secondary").get_for_model(Model2D), ) self.assertEqual( b2.polymorphic_ctype, ContentType.objects.db_manager("secondary").get_for_model(Model2B), ) cfs1 = Model2C.objects.db_manager("secondary").create_from_super(b2, field3="C3y") self.assertEqual(type(cfs1), Model2C) self.assertEqual(cfs1.pk, b2.pk) self.assertEqual(cfs1.field1, "B1b") self.assertEqual(cfs1.field2, "B2b") self.assertEqual(cfs1.field3, "C3y") b2.refresh_from_db() self.assertEqual( b2.polymorphic_ctype, ContentType.objects.db_manager("secondary").get_for_model(Model2C), ) self.assertEqual( cfs1.polymorphic_ctype, ContentType.objects.db_manager("secondary").get_for_model(Model2C), ) self.assertEqual( set(Model2A.objects.db_manager("secondary").all()), {a1, a2, b1, dfs1, cfs1, c2, d1, d2}, ) self.assertEqual(Model2A.objects.count(), 0) def test_cross_database_save(self): """Test saving an object from one database to another (issue #486)""" # Create object in default database obj = Model2B.objects.create(field1="test", field2="value") original_pk = obj.pk original_ctype_id = obj.polymorphic_ctype_id # Get the ContentType ID for Model2B in default database default_ctype = ContentType.objects.get_for_model(Model2B, for_concrete_model=False) self.assertEqual(obj.polymorphic_ctype_id, default_ctype.pk) # Save to secondary database (simulating cross-database copy) obj.pk = None # Make it a new object obj.save(using="secondary") # polymorphic_ctype_id should be updated for secondary database secondary_ctype = ContentType.objects.db_manager("secondary").get_for_model( Model2B, for_concrete_model=False ) self.assertEqual(obj.polymorphic_ctype_id, secondary_ctype.pk) # Verify it can be retrieved correctly from secondary database retrieved = Model2B.objects.using("secondary").get(pk=obj.pk) self.assertIsInstance(retrieved, Model2B) self.assertEqual(retrieved.field1, "test") self.assertEqual(retrieved.field2, "value") # Verify original object still exists in default database original_obj = Model2B.objects.get(pk=original_pk) self.assertEqual(original_obj.polymorphic_ctype_id, default_ctype.pk) def test_database_router_respected(self): """Test that _state.db is respected when no explicit using is provided (issue #446)""" # Create object in secondary database obj = Model2B.objects.using("secondary").create(field1="test", field2="value") # Verify it was created in secondary database secondary_ctype = ContentType.objects.db_manager("secondary").get_for_model( Model2B, for_concrete_model=False ) self.assertEqual(obj.polymorphic_ctype_id, secondary_ctype.pk) self.assertEqual(obj._state.db, "secondary") # Modify and save without explicit using parameter # Should use _state.db (secondary) not default obj.field1 = "modified" obj.save() # No using parameter # Verify it's still using secondary database's ContentType obj.refresh_from_db(using="secondary") self.assertEqual(obj.polymorphic_ctype_id, secondary_ctype.pk) self.assertEqual(obj.field1, "modified") # Verify it wasn't saved to default database self.assertFalse(Model2B.objects.filter(pk=obj.pk).exists()) django-polymorphic-4.10.2/src/polymorphic/tests/test_orm.py000066400000000000000000003215171513173623500241260ustar00rootroot00000000000000import warnings import pytest import uuid import django from packaging.version import Version from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.db import models, connection from django.db.models import ( Case, Count, FilteredRelation, Q, Sum, When, Exists, OuterRef, Subquery, ) from django.db.utils import IntegrityError, NotSupportedError from django.test import TransactionTestCase from django.test.utils import CaptureQueriesContext from polymorphic import query_translate from polymorphic.managers import PolymorphicManager from polymorphic.models import PolymorphicTypeInvalid, PolymorphicTypeUndefined from polymorphic.tests.models import ( ArtProject, Base, BlogA, BlogB, BlogBase, BlogEntry, BlogEntry_limit_choices_to, ChildModelWithManager, CustomPkBase, CustomPkInherit, Enhance_Base, Enhance_Plain, Enhance_Inherit, InlineParent, InlineModelA, InlineModelB, InitTestModelSubclass, Model2A, Model2B, Model2C, Model2D, ModelExtraA, ModelExtraB, ModelExtraC, ModelExtraExternal, ModelFieldNameTest, ModelOrderLine, ModelShow1, ModelShow1_plain, ModelShow2, ModelShow2_plain, ModelShow3, ModelUnderRelChild, ModelUnderRelParent, ModelWithMyManager, ModelWithMyManager2, ModelWithMyManagerDefault, ModelWithMyManagerNoDefault, ModelX, ModelY, MRODerived, MultiTableDerived, MyManager, MyManagerQuerySet, NonPolymorphicParent, NonProxyChild, One2OneRelatingModel, One2OneRelatingModelDerived, ParentModelWithManager, PlainA, PlainB, PlainC, PlainChildModelWithManager, PlainMyManager, PlainMyManagerQuerySet, PlainParentModelWithManager, ProxiedBase, ProxyBase, ProxyChild, ProxyModelA, ProxyModelB, ProxyModelBase, RedheadDuck, RelatingModel, RelationA, RelationB, RelationBase, RelationBC, RubberDuck, SubclassSelectorAbstractBaseModel, SubclassSelectorAbstractConcreteModel, SubclassSelectorProxyBaseModel, SubclassSelectorProxyConcreteModel, ParentLinkAndRelatedName, UUIDArtProject, UUIDArtProjectA, UUIDArtProjectB, UUIDArtProjectC, UUIDArtProjectD, UUIDPlainA, UUIDPlainB, UUIDPlainC, UUIDProject, UUIDResearchProject, Duck, PurpleHeadDuck, Account, SpecialAccount1, SpecialAccount1_1, SpecialAccount2, Model2BFiltered, Model2CFiltered, Model2CNamedManagers, Model2CNamedDefault, ) class PolymorphicTests(TransactionTestCase): """ The test suite """ def test_annotate_aggregate_order(self): # create a blog of type BlogA # create two blog entries in BlogA # create some blogs of type BlogB to make the BlogBase table data really polymorphic blog = BlogA.objects.create(name="B1", info="i1") blog.blogentry_set.create(text="bla") BlogEntry.objects.create(blog=blog, text="bla2") BlogB.objects.create(name="Bb1") BlogB.objects.create(name="Bb2") BlogB.objects.create(name="Bb3") qs = BlogBase.objects.annotate(entrycount=Count("BlogA___blogentry")) assert len(qs) == 4 for o in qs: if o.name == "B1": assert o.entrycount == 2 else: assert o.entrycount == 0 x = BlogBase.objects.aggregate(entrycount=Count("BlogA___blogentry")) assert x["entrycount"] == 2 # create some more blogs for next test BlogA.objects.create(name="B2", info="i2") BlogA.objects.create(name="B3", info="i3") BlogA.objects.create(name="B4", info="i4") BlogA.objects.create(name="B5", info="i5") # test ordering for field in all entries expected = """ [ , , , , , , , ]""" assert repr(BlogBase.objects.order_by("-name")).strip() == expected.strip() # different RDBMS return different orders for the nulls, and we can't use F # and nulls_first or nulls_last here to standardize it, so our test is # conditional blog_names = [blg.name for blg in BlogBase.objects.order_by("-BlogA___info")] ordered = blog_names[:3] if all([name.startswith("Bb") for name in ordered]): ordered = blog_names[3:] else: assert all([name.startswith("Bb") for name in blog_names[-3:]]) ordered = blog_names[:-3] assert ordered == ["B5", "B4", "B3", "B2", "B1"] def test_limit_choices_to(self): """ this is not really a testcase, as limit_choices_to only affects the Django admin """ # create a blog of type BlogA blog_a = BlogA.objects.create(name="aa", info="aa") blog_b = BlogB.objects.create(name="bb") # create two blog entries entry1 = BlogEntry_limit_choices_to.objects.create(blog=blog_b, text="bla2") entry2 = BlogEntry_limit_choices_to.objects.create(blog=blog_b, text="bla2") def test_primary_key_custom_field_problem(self): """ object retrieval problem occuring with some custom primary key fields (UUIDField as test case) """ up1 = UUIDProject.objects.create(topic="John's gathering") up2 = UUIDArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner") up3 = UUIDResearchProject.objects.create( topic="Swallow Aerodynamics", supervisor="Dr. Winter" ) up4 = UUIDArtProjectA.objects.create(topic="ProjectA", artist="Artist A") up5 = UUIDArtProjectB.objects.create(topic="ProjectB", artist="Artist B") up6 = UUIDArtProjectC.objects.create(topic="ProjectC", artist="Artist C") up7 = UUIDArtProjectD.objects.create(topic="ProjectD", artist="Artist D") qs = UUIDProject.objects.all() ol = list(qs) a = qs[0] b = qs[1] c = qs[2] assert len(qs) == 7 assert isinstance(a.uuid_primary_key, uuid.UUID) assert isinstance(a.pk, uuid.UUID) # https://github.com/jazzband/django-polymorphic/issues/306 assert {up1, up2, up3, up4, up5, up6, up7} == set(qs) assert {up2, up4, up5, up6, up7} == set(UUIDArtProject.objects.all()) assert {up3} == set(UUIDResearchProject.objects.all()) assert {up4, up5, up6, up7} == set(UUIDArtProjectA.objects.all()) assert {up5, up6, up7} == set(UUIDArtProjectB.objects.all()) assert {up6, up7} == set(UUIDArtProjectC.objects.all()) assert {up7} == set(UUIDArtProjectD.objects.all()) a = UUIDPlainA.objects.create(field1="A1") b = UUIDPlainB.objects.create(field1="B1", field2="B2") c = UUIDPlainC.objects.create(field1="C1", field2="C2", field3="C3") qs = UUIDPlainA.objects.all() # Test that primary key values are valid UUIDs assert uuid.UUID(f"urn:uuid:{a.pk}", version=1) == a.pk assert uuid.UUID(f"urn:uuid:{c.pk}", version=1) == c.pk def create_model2abcd(self): """ Create the chain of objects of Model2, this is reused in various tests. """ a = Model2A.objects.create(field1="A1") b = Model2B.objects.create(field1="B1", field2="B2") c = Model2C.objects.create(field1="C1", field2="C2", field3="C3") d = Model2D.objects.create(field1="D1", field2="D2", field3="D3", field4="D4") return a, b, c, d def test_simple_inheritance(self): self.create_model2abcd() objects = Model2A.objects.all() self.assertQuerySetEqual( objects, [Model2A, Model2B, Model2C, Model2D], transform=lambda o: o.__class__, ordered=False, ) def test_defer_fields(self): self.create_model2abcd() objects_deferred = Model2A.objects.defer("field1").order_by("id") assert "field1" not in objects_deferred[0].__dict__, ( "field1 was not deferred (using defer())" ) # Check that we have exactly one deferred field ('field1') per resulting object. for obj in objects_deferred: deferred_fields = obj.get_deferred_fields() assert len(deferred_fields) == 1 assert "field1" in deferred_fields objects_only = Model2A.objects.only("pk", "polymorphic_ctype", "field1") assert "field1" in objects_only[0].__dict__, ( 'qs.only("field1") was used, but field1 was incorrectly deferred' ) assert "field1" in objects_only[3].__dict__, ( 'qs.only("field1") was used, but field1 was incorrectly deferred on a child model' ) assert "field4" not in objects_only[3].__dict__, "field4 was not deferred (using only())" assert "field1" not in objects_only[0].get_deferred_fields() assert "field2" in objects_only[1].get_deferred_fields() # objects_only[2] has several deferred fields, ensure they are all set as such. model2c_deferred = objects_only[2].get_deferred_fields() assert "field2" in model2c_deferred assert "field3" in model2c_deferred assert "model2a_ptr_id" in model2c_deferred # objects_only[3] has a few more fields that should be set as deferred. model2d_deferred = objects_only[3].get_deferred_fields() assert "field2" in model2d_deferred assert "field3" in model2d_deferred assert "field4" in model2d_deferred assert "model2a_ptr_id" in model2d_deferred assert "model2b_ptr_id" in model2d_deferred ModelX.objects.create(field_b="A1", field_x="A2") ModelY.objects.create(field_b="B1", field_y="B2") # If we defer a field on a descendent, the parent's field is not deferred. objects_deferred = Base.objects.defer("ModelY___field_y") assert "field_y" not in objects_deferred[0].get_deferred_fields() assert "field_y" in objects_deferred[1].get_deferred_fields() objects_only = Base.objects.only( "polymorphic_ctype", "ModelY___field_y", "ModelX___field_x" ) assert "field_b" in objects_only[0].get_deferred_fields() assert "field_b" in objects_only[1].get_deferred_fields() def test_defer_related_fields(self): self.create_model2abcd() objects_deferred_field4 = Model2A.objects.defer("Model2D___field4") assert "field4" not in objects_deferred_field4[3].__dict__, ( "field4 was not deferred (using defer(), traversing inheritance)" ) assert objects_deferred_field4[0].__class__ == Model2A assert objects_deferred_field4[1].__class__ == Model2B assert objects_deferred_field4[2].__class__ == Model2C assert objects_deferred_field4[3].__class__ == Model2D objects_only_field4 = Model2A.objects.only( "polymorphic_ctype", "field1", "Model2B___id", "Model2B___field2", "Model2B___model2a_ptr", "Model2C___id", "Model2C___field3", "Model2C___model2b_ptr", "Model2D___id", "Model2D___model2c_ptr", ) assert objects_only_field4[0].__class__ == Model2A assert objects_only_field4[1].__class__ == Model2B assert objects_only_field4[2].__class__ == Model2C assert objects_only_field4[3].__class__ == Model2D def test_manual_get_real_instance(self): self.create_model2abcd() o = Model2A.objects.non_polymorphic().get(field1="C1") assert o.get_real_instance().__class__ == Model2C def test_get_real_instance_with_stale_content_type(self): ctype = ContentType.objects.create(app_label="tests", model="stale") o = Model2A.objects.create(field1="A1", polymorphic_ctype=ctype) assert o.get_real_instance_class() is None match = "does not have a corresponding model" with pytest.raises(PolymorphicTypeInvalid, match=match): o.get_real_instance() def test_get_real_concrete_instance_class_id_with_stale_content_type(self): """Test get_real_concrete_instance_class_id returns None for stale ContentType""" ctype = ContentType.objects.create(app_label="tests", model="stale_model") o = Model2A.objects.create(field1="A1", polymorphic_ctype=ctype) # When ContentType is stale, get_real_instance_class returns None # which should cause get_real_concrete_instance_class_id to return None assert o.get_real_concrete_instance_class_id() is None def test_get_real_concrete_instance_class_with_stale_content_type(self): """Test get_real_concrete_instance_class returns None for stale ContentType""" ctype = ContentType.objects.create(app_label="tests", model="another_stale") o = Model2A.objects.create(field1="A1", polymorphic_ctype=ctype) # When ContentType is stale, get_real_instance_class returns None # which should cause get_real_concrete_instance_class to return None assert o.get_real_concrete_instance_class() is None def test_get_real_concrete_instance_class_with_proxy_model(self): """Test get_real_concrete_instance_class with a proxy model""" # Create a regular polymorphic object a = Model2A.objects.create(field1="A1") # get_real_concrete_instance_class should return the concrete model class concrete_class = a.get_real_concrete_instance_class() assert concrete_class == Model2A def test_non_polymorphic(self): self.create_model2abcd() objects = list(Model2A.objects.all().non_polymorphic()) self.assertQuerySetEqual( objects, [Model2A, Model2A, Model2A, Model2A], transform=lambda o: o.__class__, ) def test_get_real_instances(self): self.create_model2abcd() qs = Model2A.objects.all().non_polymorphic() # from queryset objects = qs.get_real_instances() self.assertQuerySetEqual( objects, [Model2A, Model2B, Model2C, Model2D], transform=lambda o: o.__class__, ) # from a manual list objects = Model2A.objects.get_real_instances(list(qs)) self.assertQuerySetEqual( objects, [Model2A, Model2B, Model2C, Model2D], transform=lambda o: o.__class__, ) # from empty list objects = Model2A.objects.get_real_instances([]) self.assertQuerySetEqual(objects, [], transform=lambda o: o.__class__) def test_queryset_missing_derived(self): a = Model2A.objects.create(field1="A1") b = Model2B.objects.create(field1="B1", field2="B2") c = Model2C.objects.create(field1="C1", field2="C2", field3="C3") b_base = Model2A.objects.non_polymorphic().get(pk=b.pk) c_base = Model2A.objects.non_polymorphic().get(pk=c.pk) b_pk = b.pk # Save pk before deletion b.delete(keep_parents=True) # e.g. table was truncated qs_base = Model2A.objects.order_by("field1").non_polymorphic() qs_polymorphic = Model2A.objects.order_by("field1").all() assert list(qs_base) == [a, b_base, c_base] assert list(qs_polymorphic) == [a, b_base, c] result = list(qs_polymorphic) assert len(result) == 3 assert result[0] == a assert result[1].pk == b_pk # b returned as Model2A (parent) assert isinstance(result[1], Model2A) assert not isinstance(result[1], Model2B) assert result[2] == c def test_queryset_missing_contenttype(self): stale_ct = ContentType.objects.create(app_label="tests", model="nonexisting") a1 = Model2A.objects.create(field1="A1") a2 = Model2A.objects.create(field1="A2") c = Model2C.objects.create(field1="C1", field2="C2", field3="C3") c_base = Model2A.objects.non_polymorphic().get(pk=c.pk) Model2B.objects.filter(pk=a2.pk).update(polymorphic_ctype=stale_ct) qs_base = Model2A.objects.order_by("field1").non_polymorphic() qs_polymorphic = Model2A.objects.order_by("field1").all() assert list(qs_base) == [a1, a2, c_base] assert list(qs_polymorphic) == [a1, a2, c] def test_translate_polymorphic_q_object(self): self.create_model2abcd() q = Model2A.translate_polymorphic_Q_object(Q(instance_of=Model2C)) objects = Model2A.objects.filter(q) self.assertQuerySetEqual( objects, [Model2C, Model2D], transform=lambda o: o.__class__, ordered=False ) def test_create_instanceof_q(self): # Test with a list of models cached = [ Model2B, Model2C, Model2D, ] uncached = [ Model2BFiltered, Model2CFiltered, Model2CNamedManagers, Model2CNamedDefault, ] ContentType.objects._cache.clear() expected_cached = [ContentType.objects.get_for_model(m).pk for m in cached] q = query_translate.create_instanceof_q([Model2B]) assert q.children[0][0] == "polymorphic_ctype__in" assert isinstance(q.children[0][1], Subquery) for model in uncached: assert model._meta.app_label in str(q.children[0][1].query) assert model._meta.model_name in str(q.children[0][1].query) assert q.children[1][0] == "polymorphic_ctype__in" assert set(q.children[1][1]) == set(expected_cached) def test_instance_of_single_lazy_query(self): a = Model2A.objects.create(field1="A1") b = Model2B.objects.create(field1="B1", field2="B2") c1 = Model2C.objects.create(field1="C1", field2="C2") c2 = Model2C.objects.create(field1="C1", field2="C2") ContentType.objects._cache.clear() with CaptureQueriesContext(connection) as captured_queries: assert Model2A.objects.filter(instance_of=Model2C).count() == 2 assert len(captured_queries) == 1 assert set(Model2A.objects.filter(instance_of=Model2C)) == {c1, c2} # warm up the cache ContentType.objects.get_for_models(Model2A, Model2B, Model2C) with CaptureQueriesContext(connection) as captured_queries: assert Model2A.objects.filter(instance_of=Model2C).count() == 2 assert set(Model2A.objects.filter(instance_of=Model2C)) == {c1, c2} def test_base_manager(self): from .models import CustomBaseManager def base_manager(model): return (type(model._base_manager), model._base_manager.model) assert base_manager(PlainA) == (models.Manager, PlainA) assert base_manager(PlainB) == (models.Manager, PlainB) assert base_manager(PlainC) == (models.Manager, PlainC) assert base_manager(Model2A) == (PolymorphicManager, Model2A) assert base_manager(Model2B) == (PolymorphicManager, Model2B) assert base_manager(Model2C) == (PolymorphicManager, Model2C) assert base_manager(One2OneRelatingModel) == (PolymorphicManager, One2OneRelatingModel) assert base_manager(One2OneRelatingModelDerived) == ( PolymorphicManager, One2OneRelatingModelDerived, ) # unless the user provides a manager the default_manager and base_manager are # the same assert Model2A._default_manager is Model2A._base_manager assert Model2B._default_manager is Model2B._base_manager assert Model2C._default_manager is Model2C._base_manager assert type(Model2BFiltered._base_manager) is PolymorphicManager assert type(Model2CFiltered._base_manager) is PolymorphicManager assert type(Model2CNamedManagers._base_manager) is CustomBaseManager assert type(Model2CNamedDefault._base_manager) is PolymorphicManager def test_default_manager(self): from .models import FilteredManager, FilteredManager2 def default_manager(instance): return ( type(instance.__class__._default_manager), instance.__class__._default_manager.model, ) plain_a = PlainA(field1="C1") plain_b = PlainB(field2="C1") plain_c = PlainC(field3="C1") model_2a = Model2A(field1="C1") model_2b = Model2B(field2="C1") model_2c = Model2C(field3="C1") assert default_manager(plain_a) == (models.Manager, PlainA) assert default_manager(plain_b) == (models.Manager, PlainB) assert default_manager(plain_c) == (models.Manager, PlainC) assert default_manager(model_2a) == (PolymorphicManager, Model2A) assert default_manager(model_2b) == (PolymorphicManager, Model2B) assert default_manager(model_2c) == (PolymorphicManager, Model2C) assert type(Model2BFiltered._default_manager) is FilteredManager assert type(Model2CFiltered._default_manager) is FilteredManager assert type(Model2CNamedManagers._default_manager) is FilteredManager2 assert type(Model2CNamedDefault._default_manager) is FilteredManager2 assert Model2BFiltered._default_manager is not Model2BFiltered._base_manager assert Model2CFiltered._default_manager is not Model2CFiltered._base_manager assert Model2CNamedDefault._default_manager is not Model2CNamedDefault._base_manager assert Model2CNamedManagers._default_manager is not Model2CNamedManagers._base_manager def test_foreignkey_field(self): self.create_model2abcd() object2a = Model2A.objects.get(field1="C1") assert object2a.model2b.__class__ == Model2B object2b = Model2B.objects.get(field1="C1") assert object2b.model2c.__class__ == Model2C def test_parentage_links_are_non_polymorphic(self): """ OneToOne parent links should return non-polymorphic instances """ d = Model2D.objects.create(field1="D1", field2="D2", field3="D3", field4="D4") c = Model2C.objects.non_polymorphic().get(pk=d.pk) b = Model2B.objects.non_polymorphic().get(pk=d.pk) a = Model2A.objects.non_polymorphic().get(pk=d.pk) assert d.model2a_ptr.__class__ == Model2A assert d.model2b_ptr.__class__ == Model2B assert d.model2c_ptr.__class__ == Model2C assert d.model2c_ptr == c assert d.model2b_ptr == b assert d.model2a_ptr == a assert c.model2d == d assert c.model2d.__class__ == Model2D assert c.model2b_ptr.__class__ == Model2B assert c.model2a_ptr.__class__ == Model2A assert c.model2b_ptr == b assert c.model2a_ptr == a assert b.model2c == c assert b.model2c.__class__ == Model2C assert b.model2a_ptr.__class__ == Model2A assert b.model2a_ptr == a assert a.model2b.__class__ == Model2B assert a.model2b == b def test_onetoone_field(self): self.create_model2abcd() a = Model2A.objects.non_polymorphic().get(field1="C1") b = One2OneRelatingModelDerived.objects.create(one2one=a, field1="f1", field2="f2") # FIXME: this result is basically wrong, probably due to Django cacheing # (we used base_objects), but should not be a problem assert b.one2one.__class__ == Model2A assert b.one2one_id == b.one2one.id c = One2OneRelatingModelDerived.objects.get(field1="f1") assert c.one2one.__class__ == Model2C assert a.one2onerelatingmodel.__class__ == One2OneRelatingModelDerived def test_manytomany_field(self): # Model 1 o = ModelShow1.objects.create(field1="abc") o.m2m.add(o) o.save() assert ( repr(ModelShow1.objects.all()) == "[ ]" ) # Model 2 o = ModelShow2.objects.create(field1="abc") o.m2m.add(o) o.save() assert repr(ModelShow2.objects.all()) == '[ ]' # Model 3 o = ModelShow3.objects.create(field1="abc") o.m2m.add(o) o.save() assert ( repr(ModelShow3.objects.all()) == '[ ]' ) assert ( repr(ModelShow1.objects.all().annotate(Count("m2m"))) == "[ ]" ) assert ( repr(ModelShow2.objects.all().annotate(Count("m2m"))) == '[ ]' ) assert ( repr(ModelShow3.objects.all().annotate(Count("m2m"))) == '[ ]' ) # no pretty printing ModelShow1_plain.objects.create(field1="abc") ModelShow2_plain.objects.create(field1="abc", field2="def") self.assertQuerySetEqual( ModelShow1_plain.objects.all(), [ModelShow1_plain, ModelShow2_plain], transform=lambda o: o.__class__, ordered=False, ) def test_extra_method(self): from django.db import connection a, b, c, d = self.create_model2abcd() objects = Model2A.objects.extra(where=[f"id IN ({b.id}, {c.id})"]) self.assertQuerySetEqual( objects, [Model2B, Model2C], transform=lambda o: o.__class__, ordered=False ) if connection.vendor == "oracle": objects = Model2A.objects.extra( select={"select_test": "CASE WHEN field1 = 'A1' THEN 1 ELSE 0 END"}, where=["field1 = 'A1' OR field1 = 'B1'"], order_by=["-id"], ) else: objects = Model2A.objects.extra( select={"select_test": "field1 = 'A1'"}, where=["field1 = 'A1' OR field1 = 'B1'"], order_by=["-id"], ) self.assertQuerySetEqual(objects, [Model2B, Model2A], transform=lambda o: o.__class__) ModelExtraA.objects.create(field1="A1") ModelExtraB.objects.create(field1="B1", field2="B2") ModelExtraC.objects.create(field1="C1", field2="C2", field3="C3") ModelExtraExternal.objects.create(topic="extra1") ModelExtraExternal.objects.create(topic="extra2") ModelExtraExternal.objects.create(topic="extra3") objects = ModelExtraA.objects.extra( tables=["tests_modelextraexternal"], select={"topic": "tests_modelextraexternal.topic"}, where=["tests_modelextraa.id = tests_modelextraexternal.id"], ) assert ( repr(objects[0]) == '' ) assert ( repr(objects[1]) == '' ) assert ( repr(objects[2]) == '' ) assert len(objects) == 3 def test_instance_of_filter(self): self.create_model2abcd() objects = Model2A.objects.instance_of(Model2B) self.assertQuerySetEqual( objects, [Model2B, Model2C, Model2D], transform=lambda o: o.__class__, ordered=False, ) objects = Model2A.objects.filter(instance_of=Model2B) self.assertQuerySetEqual( objects, [Model2B, Model2C, Model2D], transform=lambda o: o.__class__, ordered=False, ) objects = Model2A.objects.filter(Q(instance_of=Model2B)) self.assertQuerySetEqual( objects, [Model2B, Model2C, Model2D], transform=lambda o: o.__class__, ordered=False, ) objects = Model2A.objects.not_instance_of(Model2B) self.assertQuerySetEqual( objects, [Model2A], transform=lambda o: o.__class__, ordered=False ) def test_polymorphic___filter(self): self.create_model2abcd() objects = Model2A.objects.filter(Q(Model2B___field2="B2") | Q(Model2C___field3="C3")) self.assertQuerySetEqual( objects, [Model2B, Model2C], transform=lambda o: o.__class__, ordered=False ) def test_polymorphic_applabel___filter(self): self.create_model2abcd() assert Model2B._meta.app_label == "tests" objects = Model2A.objects.filter( Q(tests__Model2B___field2="B2") | Q(tests__Model2C___field3="C3") ) self.assertQuerySetEqual( objects, [Model2B, Model2C], transform=lambda o: o.__class__, ordered=False ) def test_query_filter_exclude_is_immutable(self): # given q_to_reuse = Q(Model2B___field2="something") untouched_q_object = Q(Model2B___field2="something") # when Model2A.objects.filter(q_to_reuse).all() # then assert q_to_reuse.children == untouched_q_object.children # given q_to_reuse = Q(Model2B___field2="something") untouched_q_object = Q(Model2B___field2="something") # when Model2B.objects.filter(q_to_reuse).all() # then assert q_to_reuse.children == untouched_q_object.children def test_polymorphic___filter_field(self): p = ModelUnderRelParent.objects.create(_private=True, field1="AA") ModelUnderRelChild.objects.create(parent=p, _private2=True) # The "___" filter should also parse to "parent" -> "_private" as fallback. objects = ModelUnderRelChild.objects.filter(parent___private=True) assert len(objects) == 1 def test_polymorphic___filter_reverse_field(self): p = ModelUnderRelParent.objects.create(_private=True, field1="BB") ModelUnderRelChild.objects.create(parent=p, _private2=True) # Also test for reverse relations objects = ModelUnderRelParent.objects.filter(children___private2=True) assert len(objects) == 1 def test_delete(self): a, b, c, d = self.create_model2abcd() oa = Model2A.objects.get(id=b.id) assert oa.__class__ == Model2B assert Model2A.objects.count() == 4 oa.delete() objects = Model2A.objects.all() self.assertQuerySetEqual( objects, [Model2A, Model2C, Model2D], transform=lambda o: o.__class__, ordered=False, ) def test_combine_querysets(self): ModelX.objects.create(field_x="x", field_b="1") ModelY.objects.create(field_y="y", field_b="2") qs = Base.objects.instance_of(ModelX) | Base.objects.instance_of(ModelY) qs = qs.order_by("field_b") assert repr(qs[0]) == "" assert repr(qs[1]) == "" assert len(qs) == 2 def test_multiple_inheritance(self): # multiple inheritance, subclassing third party models (mix PolymorphicModel with models.Model) Enhance_Base.objects.create(field_b="b-base") Enhance_Inherit.objects.create(field_b="b-inherit", field_p="p", field_i="i") qs = Enhance_Base.objects.all() assert len(qs) == 2 assert ( repr(qs[0]) == '' ) assert ( repr(qs[1]) == '' ) def test_relation_base(self): # ForeignKey, ManyToManyField obase = RelationBase.objects.create(field_base="base") oa = RelationA.objects.create(field_base="A1", field_a="A2", fk=obase) ob = RelationB.objects.create(field_base="B1", field_b="B2", fk=oa) oc = RelationBC.objects.create(field_base="C1", field_b="C2", field_c="C3", fk=oa) oa.m2m.add(oa) oa.m2m.add(ob) objects = RelationBase.objects.order_by("pk").all() assert ( repr(objects[0]) == '' ) assert ( repr(objects[1]) == '' ) assert ( repr(objects[2]) == '' ) assert ( repr(objects[3]) == '' ) assert len(objects) == 4 oa = RelationBase.objects.get(id=2) assert ( repr(oa.fk) == '' ) objects = oa.relationbase_set.order_by("pk").all() assert ( repr(objects[0]) == '' ) assert ( repr(objects[1]) == '' ) assert len(objects) == 2 ob = RelationBase.objects.get(id=3) assert ( repr(ob.fk) == '' ) oa = RelationA.objects.get() objects = oa.m2m.order_by("pk").all() assert ( repr(objects[0]) == '' ) assert ( repr(objects[1]) == '' ) assert len(objects) == 2 def test_user_defined_manager(self): self.create_model2abcd() ModelWithMyManager.objects.create(field1="D1a", field4="D4a") ModelWithMyManager.objects.create(field1="D1b", field4="D4b") # MyManager should reverse the sorting of field1 objects = ModelWithMyManager.objects.all() self.assertQuerySetEqual( objects, [(ModelWithMyManager, "D1b", "D4b"), (ModelWithMyManager, "D1a", "D4a")], transform=lambda o: (o.__class__, o.field1, o.field4), ) assert type(ModelWithMyManager.objects) is MyManager assert type(ModelWithMyManager._default_manager) is MyManager def test_user_defined_manager_as_secondary(self): self.create_model2abcd() ModelWithMyManagerNoDefault.objects.create(field1="D1a", field4="D4a") ModelWithMyManagerNoDefault.objects.create(field1="D1b", field4="D4b") # MyManager should reverse the sorting of field1 objects = ModelWithMyManagerNoDefault.my_objects.all() self.assertQuerySetEqual( objects, [ (ModelWithMyManagerNoDefault, "D1b", "D4b"), (ModelWithMyManagerNoDefault, "D1a", "D4a"), ], transform=lambda o: (o.__class__, o.field1, o.field4), ) assert type(ModelWithMyManagerNoDefault.my_objects) is MyManager assert type(ModelWithMyManagerNoDefault.objects) is PolymorphicManager assert type(ModelWithMyManagerNoDefault._default_manager) is PolymorphicManager def test_user_objects_manager_as_secondary(self): self.create_model2abcd() ModelWithMyManagerDefault.objects.create(field1="D1a", field4="D4a") ModelWithMyManagerDefault.objects.create(field1="D1b", field4="D4b") assert type(ModelWithMyManagerDefault.my_objects) is MyManager assert type(ModelWithMyManagerDefault.objects) is PolymorphicManager assert type(ModelWithMyManagerDefault._default_manager) is MyManager def test_user_defined_queryset_as_manager(self): self.create_model2abcd() ModelWithMyManager2.objects.create(field1="D1a", field4="D4a") ModelWithMyManager2.objects.create(field1="D1b", field4="D4b") objects = ModelWithMyManager2.objects.all() self.assertQuerySetEqual( objects, [(ModelWithMyManager2, "D1a", "D4a"), (ModelWithMyManager2, "D1b", "D4b")], transform=lambda o: (o.__class__, o.field1, o.field4), ordered=False, ) assert ( type(ModelWithMyManager2.objects).__name__ == "PolymorphicManagerFromMyManagerQuerySet" ) assert ( type(ModelWithMyManager2._default_manager).__name__ == "PolymorphicManagerFromMyManagerQuerySet" ) def test_manager_inheritance(self): # by choice of MRO, should be MyManager from MROBase1. assert type(MRODerived.objects) is MyManager def test_queryset_assignment(self): # This is just a consistency check for now, testing standard Django behavior. parent = PlainParentModelWithManager.objects.create() child = PlainChildModelWithManager.objects.create(fk=parent) assert type(PlainParentModelWithManager._default_manager) is models.Manager assert type(PlainChildModelWithManager._default_manager) is PlainMyManager assert type(PlainChildModelWithManager.objects) is PlainMyManager assert type(PlainChildModelWithManager.objects.all()) is PlainMyManagerQuerySet # A related set is created using the model's _default_manager, so does gain extra methods. assert type(parent.childmodel_set.my_queryset_foo()) is PlainMyManagerQuerySet # For polymorphic models, the same should happen. parent = ParentModelWithManager.objects.create() child = ChildModelWithManager.objects.create(fk=parent) assert type(ParentModelWithManager._default_manager) is PolymorphicManager assert type(ChildModelWithManager._default_manager) is MyManager assert type(ChildModelWithManager.objects) is MyManager assert type(ChildModelWithManager.objects.my_queryset_foo()) is MyManagerQuerySet # A related set is created using the model's _default_manager, so does gain extra methods. assert type(parent.childmodel_set.my_queryset_foo()) is MyManagerQuerySet def test_proxy_models(self): # prepare some data for data in ("bleep bloop", "I am a", "computer"): ProxyChild.objects.create(some_data=data) # this caches ContentType queries so they don't interfere with our query counts later list(ProxyBase.objects.all()) # one query per concrete class with self.assertNumQueries(1): items = list(ProxyBase.objects.all()) assert isinstance(items[0], ProxyChild) def test_queryset_on_proxy_model_does_not_return_superclasses(self): ProxyBase.objects.create(some_data="Base1") ProxyBase.objects.create(some_data="Base2") ProxyChild.objects.create(some_data="Child1") ProxyChild.objects.create(some_data="Child2") ProxyChild.objects.create(some_data="Child3") assert ProxyBase.objects.count() == 5 assert ProxyChild.objects.count() == 3 def test_proxy_get_real_instance_class(self): """ The call to ``get_real_instance()`` also checks whether the returned model is of the correct type. This unit test guards that this check is working properly. For instance, proxy child models need to be handled separately. """ name = "Item1" nonproxychild = NonProxyChild.objects.create(name=name) pb = ProxyBase.objects.get(id=1) assert pb.get_real_instance_class() == NonProxyChild assert pb.get_real_instance() == nonproxychild assert pb.name == name pbm = NonProxyChild.objects.get(id=1) assert pbm.get_real_instance_class() == NonProxyChild assert pbm.get_real_instance() == nonproxychild assert pbm.name == name def test_content_types_for_proxy_models(self): """Checks if ContentType is capable of returning proxy models.""" from django.contrib.contenttypes.models import ContentType ct = ContentType.objects.get_for_model(ProxyChild, for_concrete_model=False) assert ProxyChild == ct.model_class() def test_proxy_model_inheritance(self): """ Polymorphic abilities should also work when the base model is a proxy object. """ # The managers should point to the proper objects. # otherwise, the whole excersise is pointless. assert ProxiedBase.objects.model == ProxiedBase assert ProxyModelBase.objects.model == ProxyModelBase assert ProxyModelA.objects.model == ProxyModelA assert ProxyModelB.objects.model == ProxyModelB # Create objects object1_pk = ProxyModelA.objects.create(name="object1").pk object2_pk = ProxyModelB.objects.create(name="object2", field2="bb").pk # Getting single objects object1 = ProxyModelBase.objects.get(name="object1") object2 = ProxyModelBase.objects.get(name="object2") assert repr(object1) == ( f'' ) assert repr(object2) == ( '' % object2_pk ) assert isinstance(object1, ProxyModelA) assert isinstance(object2, ProxyModelB) # Same for lists objects = list(ProxyModelBase.objects.all().order_by("name")) assert repr(objects[0]) == ( f'' ) assert repr(objects[1]) == ( '' % object2_pk ) assert isinstance(objects[0], ProxyModelA) assert isinstance(objects[1], ProxyModelB) def test_custom_pk(self): pk_base = CustomPkBase.objects.create(b="b") pk_inherit = CustomPkInherit.objects.create(b="b", i="i") qs = CustomPkBase.objects.all() assert len(qs) == 2 assert repr(qs[0]) == f'' assert ( repr(qs[1]) == f'' ) def test_fix_getattribute(self): # fixed issue in PolymorphicModel.__getattribute__: field name same as model name o = ModelFieldNameTest.objects.create(modelfieldnametest="1") assert repr(o) == "" # if subclass defined __init__ and accessed class members, # __getattribute__ had a problem: "...has no attribute 'sub_and_superclass_dict'" o = InitTestModelSubclass.objects.create() assert o.bar == "XYZ" def test_parent_link_and_related_name(self): t = ParentLinkAndRelatedName(field1="ParentLinkAndRelatedName") t.save() p = ModelShow1_plain.objects.get(field1="ParentLinkAndRelatedName") # check that p is equal to the assert isinstance(p, ParentLinkAndRelatedName) assert p == t # check that the accessors to parent and sublass work correctly and return the right object p = ModelShow1_plain.objects.non_polymorphic().get(field1="ParentLinkAndRelatedName") # p should be Plain1 and t ParentLinkAndRelatedName, so not equal assert p != t assert p == t.superclass assert p.related_name_subclass == t # test that we can delete the object t.delete() def test_polymorphic__accessor_caching(self): blog_a = BlogA.objects.create(name="blog") blog_base = BlogBase.objects.non_polymorphic().get(id=blog_a.id) blog_a = BlogA.objects.get(id=blog_a.id) # test reverse accessor & check that we get back cached object on repeated access self.assertEqual(blog_base.bloga, blog_a) self.assertIs(blog_base.bloga, blog_base.bloga) cached_blog_a = blog_base.bloga # test forward accessor & check that we get back cached object on repeated access self.assertEqual(blog_a.blogbase_ptr, blog_base) self.assertIs(blog_a.blogbase_ptr, blog_a.blogbase_ptr) cached_blog_base = blog_a.blogbase_ptr # check that refresh_from_db correctly clears cached related objects blog_base.refresh_from_db() blog_a.refresh_from_db() self.assertIsNot(cached_blog_a, blog_base.bloga) self.assertIsNot(cached_blog_base, blog_a.blogbase_ptr) def test_polymorphic__aggregate(self): """test ModelX___field syntax on aggregate (should work for annotate either)""" Model2A.objects.create(field1="A1") Model2B.objects.create(field1="A1", field2="B2") Model2B.objects.create(field1="A1", field2="B2") # aggregate using **kwargs result = Model2A.objects.aggregate(cnt=Count("Model2B___field2")) assert result == {"cnt": 2} # aggregate using **args with pytest.raises( AssertionError, match="model lookup supported for keyword arguments only", ): Model2A.objects.aggregate(Count("Model2B___field2")) def test_polymorphic__aggregate_empty_queryset(self): """test the fix for test___lookup in Django 5.1+""" line = ModelOrderLine.objects.create() result = line.articles.aggregate(Sum("sales_points")) assert result == {"sales_points__sum": None} def test_polymorphic__complex_aggregate(self): """test (complex expression on) aggregate (should work for annotate either)""" Model2A.objects.create(field1="A1") Model2B.objects.create(field1="A1", field2="B2") Model2B.objects.create(field1="A1", field2="B2") # aggregate using **kwargs cnt = Count(Case(When(Model2B___field2="B2", then=1))) result = Model2A.objects.aggregate( cnt_a1=Count(Case(When(field1="A1", then=1))), cnt_b2=cnt, ) assert result == {"cnt_b2": 2, "cnt_a1": 3} # test that our expression was immutable # FIXME - expression passed into aggregate are not immutable! # assert ( # cnt.get_source_expressions()[0] # .get_source_expressions()[0] # .get_source_expressions()[0] # .children[0][0] # == "Model2B___field2" # ) # aggregate using **args # we have to set the defaul alias or django won't except a complex expression # on aggregate/annotate def ComplexAgg(expression): complexagg = Count(expression) * 10 complexagg.default_alias = "complexagg" return complexagg with pytest.raises( AssertionError, match="model lookup supported for keyword arguments only", ): Model2A.objects.aggregate(ComplexAgg("Model2B___field2")) def test_annotate_f_expression(self): """ Verify that F() expressions with '___' syntax correctly translate in annotate() calls. """ Model2A.objects.create(field1="A_only") Model2B.objects.create(field1="A_from_B1", field2="B2_val1") Model2B.objects.create(field1="A_from_B2", field2="B2_val2") # Use annotate with an F-expression targeting a child model field # We'll count occurrences of field2 from Model2B # This implicitly tests that 'Model2B___field2' is correctly translated annotated_queryset = Model2A.objects.annotate( field2_count=Count(models.F("Model2B___field2")) ).order_by("pk") results = list(annotated_queryset) assert len(results) == 3 # For Model2A that is not a Model2B, the count should be 0 assert results[0].field1 == "A_only" assert results[0].field2_count == 0 # For Model2B instances, the field2_count should be 1 assert results[1].field1 == "A_from_B1" assert results[1].field2_count == 1 assert results[2].field1 == "A_from_B2" assert results[2].field2_count == 1 def test_polymorphic__filtered_relation(self): """test annotation using FilteredRelation""" blog = BlogA.objects.create(name="Ba1", info="i1 joined") blog.blogentry_set.create(text="bla1 joined") blog.blogentry_set.create(text="bla2 joined") blog.blogentry_set.create(text="bla3 joined") blog.blogentry_set.create(text="bla4") blog.blogentry_set.create(text="bla5") BlogA.objects.create(name="Ba2", info="i2 joined") BlogA.objects.create(name="Ba3", info="i3") BlogB.objects.create(name="Bb3") result = BlogA.objects.annotate( text_joined=FilteredRelation( "blogentry", condition=Q(blogentry__text__contains="joined") ), ).aggregate(Count("text_joined")) assert result == {"text_joined__count": 3} result = BlogA.objects.annotate( text_joined=FilteredRelation( "blogentry", condition=Q(blogentry__text__contains="joined") ), ).aggregate(count=Count("text_joined")) assert result == {"count": 3} result = BlogBase.objects.annotate( info_joined=FilteredRelation("bloga", condition=Q(BlogA___info__contains="joined")), ).aggregate(Count("info_joined")) assert result == {"info_joined__count": 2} result = BlogBase.objects.annotate( info_joined=FilteredRelation("bloga", condition=Q(BlogA___info__contains="joined")), ).aggregate(count=Count("info_joined")) assert result == {"count": 2} # We should get a BlogA and a BlogB result = BlogBase.objects.annotate( info_joined=FilteredRelation("bloga", condition=Q(BlogA___info__contains="joined")), ).filter(info_joined__isnull=True) assert result.count() == 2 assert isinstance(result.first(), BlogA) assert isinstance(result.last(), BlogB) def test_polymorphic__expressions(self): from django.db.models.functions import Concat # no exception raised result = Model2B.objects.annotate(val=Concat("field1", "field2")) assert list(result) == [] def test_null_polymorphic_id(self): """Test that a proper error message is displayed when the database lacks the ``polymorphic_ctype_id``""" Model2A.objects.create(field1="A1") Model2B.objects.create(field1="A1", field2="B2") Model2B.objects.create(field1="A1", field2="B2") Model2A.objects.all().update(polymorphic_ctype_id=None) with pytest.raises(PolymorphicTypeUndefined): list(Model2A.objects.all()) def test_invalid_polymorphic_id(self): """Test that a proper error message is displayed when the database ``polymorphic_ctype_id`` is invalid""" Model2A.objects.create(field1="A1") Model2B.objects.create(field1="A1", field2="B2") Model2B.objects.create(field1="A1", field2="B2") invalid = ContentType.objects.get_for_model(PlainA).pk Model2A.objects.all().update(polymorphic_ctype_id=invalid) with pytest.raises(PolymorphicTypeInvalid): list(Model2A.objects.all()) def test_bulk_create_abstract_inheritance(self): ArtProject.objects.bulk_create( [ ArtProject(topic="Painting with Tim", artist="T. Turner"), ArtProject(topic="Sculpture with Tim", artist="T. Turner"), ] ) assert sorted(ArtProject.objects.values_list("topic", "artist")) == [ ("Painting with Tim", "T. Turner"), ("Sculpture with Tim", "T. Turner"), ] def test_bulk_create_proxy_inheritance(self): RedheadDuck.objects.bulk_create( [ RedheadDuck(name="redheadduck1"), Duck(name="duck1"), RubberDuck(name="rubberduck1"), ] ) RubberDuck.objects.bulk_create( [ RedheadDuck(name="redheadduck2"), RubberDuck(name="rubberduck2"), Duck(name="duck2"), ] ) assert sorted(RedheadDuck.objects.values_list("name", flat=True)) == [ "redheadduck1", "redheadduck2", ] assert sorted(RubberDuck.objects.values_list("name", flat=True)) == [ "rubberduck1", "rubberduck2", ] assert sorted(Duck.objects.values_list("name", flat=True)) == [ "duck1", "duck2", "redheadduck1", "redheadduck2", "rubberduck1", "rubberduck2", ] def test_bulk_create_unsupported_multi_table_inheritance(self): with pytest.raises(ValueError): MultiTableDerived.objects.bulk_create( [MultiTableDerived(field1="field1", field2="field2")] ) def test_bulk_create_ignore_conflicts(self): try: ArtProject.objects.bulk_create( [ ArtProject(topic="Painting with Tim", artist="T. Turner"), ArtProject.objects.create(topic="Sculpture with Tim", artist="T. Turner"), ], ignore_conflicts=True, ) assert ArtProject.objects.count() == 2 except NotSupportedError: from django.db import connection assert connection.vendor in ("oracle"), ( f"{connection.vendor} should support ignore_conflicts" ) def test_bulk_create_no_ignore_conflicts(self): with pytest.raises(IntegrityError): ArtProject.objects.bulk_create( [ ArtProject(topic="Painting with Tim", artist="T. Turner"), ArtProject.objects.create(topic="Sculpture with Tim", artist="T. Turner"), ], ignore_conflicts=False, ) assert ArtProject.objects.count() == 1 def test_can_query_using_subclass_selector_on_abstract_model(self): obj = SubclassSelectorAbstractConcreteModel.objects.create(concrete_field="abc") queried_obj = SubclassSelectorAbstractBaseModel.objects.filter( SubclassSelectorAbstractConcreteModel___concrete_field="abc" ).get() assert obj.pk == queried_obj.pk def test_intermediate_abstract_descriptors(self): mdl = SubclassSelectorAbstractConcreteModel.objects.create() base = SubclassSelectorAbstractBaseModel.objects.non_polymorphic().get(pk=mdl.pk) assert mdl.subclassselectorabstractbasemodel_ptr == base assert base.subclassselectorabstractconcretemodel == mdl def test_can_query_using_subclass_selector_on_proxy_model(self): obj = SubclassSelectorProxyConcreteModel.objects.create(concrete_field="abc") queried_obj = SubclassSelectorProxyBaseModel.objects.filter( SubclassSelectorProxyConcreteModel___concrete_field="abc" ).get() assert obj.pk == queried_obj.pk def test_intermediate_proxy_descriptors(self): mdl = SubclassSelectorProxyConcreteModel.objects.create() base = SubclassSelectorProxyBaseModel.objects.non_polymorphic().get(pk=mdl.pk) assert mdl.subclassselectorproxybasemodel_ptr == base assert mdl.subclassselectorproxybasemodel_ptr.__class__ is SubclassSelectorProxyBaseModel assert ( base.subclassselectorproxyconcretemodel.__class__ is SubclassSelectorProxyConcreteModel ) def test_prefetch_related_behaves_normally_with_polymorphic_model(self): b1 = RelatingModel.objects.create() b2 = RelatingModel.objects.create() a = b1.many2many.create() # create Model2A b2.many2many.add(a) # add same to second relating model qs = RelatingModel.objects.prefetch_related("many2many") for obj in qs: assert len(obj.many2many.all()) == 1 def test_prefetch_related_with_missing(self): b1 = RelatingModel.objects.create() b2 = RelatingModel.objects.create() rel1 = Model2A.objects.create(field1="A1") rel2 = Model2B.objects.create(field1="A2", field2="B2") b1.many2many.add(rel1) b2.many2many.add(rel2) rel2_pk = rel2.pk # Save pk before deletion rel2.delete(keep_parents=True) qs = RelatingModel.objects.order_by("pk").prefetch_related("many2many") objects = list(qs) assert len(objects[0].many2many.all()) == 1 # derived object was upcast by deletion that keeps parents assert len(objects[1].many2many.all()) == 1 assert objects[1].many2many.first() == Model2A.objects.get(field1="A2") assert len(objects[1].many2many.all()) == 1 parent_obj = objects[1].many2many.all()[0] assert parent_obj.pk == rel2_pk assert isinstance(parent_obj, Model2A) assert not isinstance(parent_obj, Model2B) # base object does exist assert len(objects[1].many2many.non_polymorphic()) == 1 def test_refresh_from_db_fields(self): """Test whether refresh_from_db(fields=..) works as it performs .only() queries""" obj = Model2B.objects.create(field1="aa", field2="bb") Model2B.objects.filter(pk=obj.pk).update(field1="aa1", field2="bb2") obj.refresh_from_db(fields=["field2"]) assert obj.field1 == "aa" assert obj.field2 == "bb2" obj.refresh_from_db(fields=["field1"]) assert obj.field1 == "aa1" def test_non_polymorphic_parent(self): obj = NonPolymorphicParent.objects.create() assert obj.delete() def test_iteration(self): for i in range(250): Model2B.objects.create(field1=f"B1-{i}", field2=f"B2-{i}") for i in range(1000): Model2C.objects.create( field1=f"C1-{i + 250}", field2=f"C2-{i + 250}", field3=f"C3-{i + 250}" ) for i in range(2000): Model2D.objects.create( field1=f"D1-{i + 1250}", field2=f"D2-{i + 1250}", field3=f"D3-{i + 1250}", field4=f"D4-{i + 1250}", ) with CaptureQueriesContext(connection) as base_all: for _ in Model2A.objects.non_polymorphic().all(): pass # Evaluating the queryset len_base_all = len(base_all) assert len_base_all == 1, ( f"Expected 1 queries for chunked iteration over 3250 base objects. {len_base_all}" ) with CaptureQueriesContext(connection) as base_iterator: for _ in Model2A.objects.non_polymorphic().iterator(): pass # Evaluating the queryset len_base_iterator = len(base_iterator) assert len_base_iterator == 1, ( f"Expected 1 queries for chunked iteration over 3250 base objects. {len_base_iterator}" ) with CaptureQueriesContext(connection) as base_chunked: for _ in Model2A.objects.non_polymorphic().iterator(chunk_size=1000): pass # Evaluating the queryset len_base_chunked = len(base_chunked) assert len_base_chunked == 1, ( f"Expected 1 queries for chunked iteration over 3250 base objects. {len_base_chunked}" ) with CaptureQueriesContext(connection) as poly_all: b, c, d = 0, 0, 0 for idx, obj in enumerate(reversed(list(Model2A.objects.order_by("-pk").all()))): if isinstance(obj, Model2D): d += 1 assert obj.field1 == f"D1-{idx}" assert obj.field2 == f"D2-{idx}" assert obj.field3 == f"D3-{idx}" assert obj.field4 == f"D4-{idx}" elif isinstance(obj, Model2C): c += 1 assert obj.field1 == f"C1-{idx}" assert obj.field2 == f"C2-{idx}" assert obj.field3 == f"C3-{idx}" elif isinstance(obj, Model2B): b += 1 assert obj.field1 == f"B1-{idx}" assert obj.field2 == f"B2-{idx}" else: assert False, "Unexpected model type" assert (b, c, d) == (250, 1000, 2000) assert len(poly_all) <= 8, ( f"Expected < 7 queries for chunked iteration over 3250 " f"objects with 3 child models and the default chunk size of 2000, encountered " f"{len(poly_all)}" ) with CaptureQueriesContext(connection) as poly_all: b, c, d = 0, 0, 0 for idx, obj in enumerate(Model2A.objects.order_by("pk").iterator(chunk_size=None)): if isinstance(obj, Model2D): d += 1 assert obj.field1 == f"D1-{idx}" assert obj.field2 == f"D2-{idx}" assert obj.field3 == f"D3-{idx}" assert obj.field4 == f"D4-{idx}" elif isinstance(obj, Model2C): c += 1 assert obj.field1 == f"C1-{idx}" assert obj.field2 == f"C2-{idx}" assert obj.field3 == f"C3-{idx}" elif isinstance(obj, Model2B): b += 1 assert obj.field1 == f"B1-{idx}" assert obj.field2 == f"B2-{idx}" else: assert False, "Unexpected model type" assert (b, c, d) == (250, 1000, 2000) assert len(poly_all) <= 7, ( f"Expected < 7 queries for chunked iteration over 3250 " f"objects with 3 child models and a chunk size of 2000, encountered " f"{len(poly_all)}" ) with CaptureQueriesContext(connection) as poly_iterator: b, c, d = 0, 0, 0 for idx, obj in enumerate(Model2A.objects.order_by("pk").iterator()): if isinstance(obj, Model2D): d += 1 assert obj.field1 == f"D1-{idx}" assert obj.field2 == f"D2-{idx}" assert obj.field3 == f"D3-{idx}" assert obj.field4 == f"D4-{idx}" elif isinstance(obj, Model2C): c += 1 assert obj.field1 == f"C1-{idx}" assert obj.field2 == f"C2-{idx}" assert obj.field3 == f"C3-{idx}" elif isinstance(obj, Model2B): b += 1 assert obj.field1 == f"B1-{idx}" assert obj.field2 == f"B2-{idx}" else: assert False, "Unexpected model type" assert (b, c, d) == (250, 1000, 2000) assert len(poly_iterator) <= 7, ( f"Expected <= 7 queries for chunked iteration over 3250 " f"objects with 3 child models and a default chunk size of 2000, encountered " f"{len(poly_iterator)}" ) with CaptureQueriesContext(connection) as poly_chunked: b, c, d = 0, 0, 0 for idx, obj in enumerate(Model2A.objects.order_by("pk").iterator(chunk_size=4000)): if isinstance(obj, Model2D): d += 1 assert obj.field1 == f"D1-{idx}" assert obj.field2 == f"D2-{idx}" assert obj.field3 == f"D3-{idx}" assert obj.field4 == f"D4-{idx}" elif isinstance(obj, Model2C): c += 1 assert obj.field1 == f"C1-{idx}" assert obj.field2 == f"C2-{idx}" assert obj.field3 == f"C3-{idx}" elif isinstance(obj, Model2B): b += 1 assert obj.field1 == f"B1-{idx}" assert obj.field2 == f"B2-{idx}" else: assert False, "Unexpected model type" assert (b, c, d) == (250, 1000, 2000) assert len(poly_chunked) <= 7, ( f"Expected <= 7 queries for chunked iteration over 3250 objects with 3 child " f"models and a chunk size of 4000, encountered {len(poly_chunked)}" ) if connection.vendor == "postgresql": assert len(poly_chunked) == 4, "On postgres with a 4000 chunk size, expected 4 queries" result = Model2A.objects.all().delete() assert result == ( 11500, { "tests.Model2D": 2000, "tests.Model2C": 3000, "tests.Model2A": 3250, "tests.Model2B": 3250, }, ) def test_transmogrify_with_init(self): pur = PurpleHeadDuck.objects.create() assert pur.color == "blue" assert pur.home == "Duckburg" pur = Duck.objects.get(id=pur.id) assert pur.color == "blue" # issues/615 fixes following line: assert pur.home == "Duckburg" def test_subqueries(self): pa1 = PlainA.objects.create(field1="plain1") PlainA.objects.create(field1="plain2") ip1 = InlineParent.objects.create(title="parent1") ip2 = InlineParent.objects.create(title="parent2") ima1 = InlineModelA.objects.create(parent=ip1, field1="ima1") ima2 = InlineModelA.objects.create(parent=ip2, field1="ima2") imb1 = InlineModelB.objects.create(parent=ip1, field1="imab1", field2="imb1", plain_a=pa1) imb2 = InlineModelB.objects.create(parent=ip2, field1="imab2", field2="imb2") results = InlineModelA.objects.filter( Exists(PlainA.objects.filter(inline_bs=OuterRef("pk"))) | Exists(InlineParent.objects.filter(inline_children=OuterRef("pk"))) ) assert ima1 in results assert ima2 in results assert imb1 in results assert imb2 in results results = InlineModelA.objects.filter( Exists(PlainA.objects.filter(inline_bs=OuterRef("pk"), field1="plain1")) | Exists(InlineParent.objects.filter(inline_children=OuterRef("pk"), title="parent2")) ) assert ima1 not in results assert ima2 in results assert imb1 in results assert imb2 in results results = InlineModelA.objects.filter( Exists(PlainA.objects.filter(inline_bs=OuterRef("pk"))) ) assert ima1 not in results assert ima2 not in results assert imb1 in results assert imb2 not in results results = InlineModelA.objects.filter( Exists(PlainA.objects.filter(inline_bs=OuterRef("pk"))), field1="imab1" ) assert ima1 not in results assert ima2 not in results assert imb1 in results assert imb2 not in results results = InlineModelA.objects.filter( Exists(PlainA.objects.filter(inline_bs=OuterRef("pk"))), InlineModelB___field2="imb2" ) assert not results results = InlineModelA.objects.filter( ~Exists(PlainA.objects.filter(inline_bs=OuterRef("pk"))), InlineModelB___field2="imb2" ) assert len(results) == 1 assert imb2 in results PlainA.objects.all().delete() InlineParent.objects.all().delete() InlineModelA.objects.all().delete() InlineModelB.objects.all().delete() def test_one_to_one_primary_key(self): # check pk name resolution with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") for mdl in [Account, SpecialAccount1, SpecialAccount1_1, SpecialAccount2]: assert mdl.polymorphic_primary_key_name == Account._meta.pk.attname assert w[0].category is DeprecationWarning assert "polymorphic_primary_key_name" in str(w[0].message) user1 = get_user_model().objects.create( username="user1", email="user1@example.com", password="password" ) user2 = get_user_model().objects.create( username="user2", email="user2@example.com", password="password" ) user3 = get_user_model().objects.create( username="user3", email="user3@example.com", password="password" ) user4 = get_user_model().objects.create( username="user4", email="user4@example.com", password="password" ) user1_profile = SpecialAccount1_1.objects.create(user=user1, extra1=5, extra2=6) user2_profile = SpecialAccount1.objects.create(user=user2, extra1=5) user3_profile = SpecialAccount2.objects.create(user=user3, extra1="test") user4_profile = SpecialAccount1_1.objects.create(user=user4, extra1=7, extra2=8) user1.refresh_from_db() assert user1.account.__class__ is SpecialAccount1_1 assert user1.account.extra1 == 5 assert user1.account.extra2 == 6 assert user1_profile.pk == user1.account.pk user2.refresh_from_db() assert user2.account.__class__ is SpecialAccount1 assert user2.account.extra1 == 5 assert user2_profile.pk == user2.account.pk assert not hasattr(user2.account, "extra2") user3.refresh_from_db() assert user3.account.__class__ is SpecialAccount2 assert user3.account.extra1 == "test" assert user3_profile.pk == user3.account.pk assert not hasattr(user3.account, "extra2") user4.refresh_from_db() assert user4.account.__class__ is SpecialAccount1_1 assert user4.account.extra1 == 7 assert user4.account.extra2 == 8 assert user4_profile.pk == user4.account.pk assert get_user_model().objects.filter(pk=user2.pk).delete() == ( 3, {"tests.SpecialAccount1": 1, "tests.Account": 1, "auth.User": 1}, ) assert SpecialAccount1.objects.count() == 2 assert Account.objects.count() == 3 remaining = get_user_model().objects.filter( pk__in=[user1.pk, user2.pk, user3.pk, user4.pk] ) assert remaining.count() == 3 for usr, expected in zip( remaining.order_by("pk"), (user1_profile, user3_profile, user4_profile) ): assert usr.account == expected assert get_user_model().objects.filter(pk__in=[user3.pk]).delete() == ( 3, {"tests.SpecialAccount2": 1, "tests.Account": 1, "auth.User": 1}, ) assert Account.objects.count() == 2 assert SpecialAccount1_1.objects.all().delete() == ( 6, {"tests.SpecialAccount1_1": 2, "tests.SpecialAccount1": 2, "tests.Account": 2}, ) assert Account.objects.count() == 0 remaining = get_user_model().objects.filter(pk__gte=user1.pk) assert remaining.count() == 2 for usr in remaining: assert not hasattr(usr, "account") assert get_user_model().objects.filter(pk__in=[user1.pk, user4.pk]).delete() == ( 2, {"auth.User": 2}, ) def test_manager_override(self): from polymorphic.tests.models import MyBaseModel, MyChild1Model, MyChild2Model child1 = MyChild1Model.objects.create(fieldA=4) child2 = MyChild2Model.objects.create(fieldB=6) assert MyBaseModel.objects.filter_by_user(5).count() == 2 assert child1 in MyBaseModel.objects.all() assert child2 in MyBaseModel.objects.all() assert MyChild1Model.objects.filter_by_user(5).count() == 1 assert MyChild2Model.objects.filter_by_user(5).count() == 1 assert MyChild1Model.objects.filter_by_user(5).count() == 1 assert MyChild1Model.objects.filter_by_user(5).first() == child1 assert MyChild2Model.objects.filter_by_user(5).first() == child2 assert MyChild2Model._default_manager is MyChild2Model.objects MyChild2Model.objects.filter_by_user(6).count() == 0 MyChild2Model.base_manager.filter_by_user(6).count() == 1 def test_abstract_managers(self): from django.db.models import Manager from polymorphic.tests.models import ( AbstractManagerTest, DerivedManagerTest, DerivedManagerTest2, SpecialPolymorphicManager, SpecialQuerySet, RelatedManagerTest, ) with self.assertRaises(AttributeError): AbstractManagerTest.objects with self.assertRaises(AttributeError): AbstractManagerTest.basic_manager with self.assertRaises(AttributeError): AbstractManagerTest.default_manager assert type(DerivedManagerTest.objects) is SpecialPolymorphicManager assert type(DerivedManagerTest.basic_manager) is Manager assert type(DerivedManagerTest.default_manager) is PolymorphicManager assert type(DerivedManagerTest._default_manager) is SpecialPolymorphicManager assert type(DerivedManagerTest2.objects) is PolymorphicManager assert type(DerivedManagerTest2.basic_manager) is Manager assert type(DerivedManagerTest2.default_manager) is PolymorphicManager assert type(DerivedManagerTest2._default_manager) is PolymorphicManager dmt1 = DerivedManagerTest.objects.create(abstract_field="dmt1") dmt2 = DerivedManagerTest2.objects.create(abstract_field="dmt2") assert DerivedManagerTest.objects.has_text("dmt").count() == 2 assert dmt1 in DerivedManagerTest.objects.has_text("dmt") assert dmt2 in DerivedManagerTest.objects.has_text("dmt") assert DerivedManagerTest.objects.custom_queryset().has_text("dmt").count() == 2 assert isinstance(DerivedManagerTest.objects.has_text("dmt"), SpecialQuerySet) with self.assertRaises(AttributeError): DerivedManagerTest2.objects.has_text("dmt") related = RelatedManagerTest.objects.create() assert isinstance(related.derived, SpecialPolymorphicManager) def test_fk_polymorphism(self): from polymorphic.tests.models import FKTest, FKTestChild child = FKTestChild.objects.create() fk_test = FKTest.objects.create(fk=child) assert fk_test.fk is child fk_test = FKTest.objects.get(pk=fk_test.id) assert fk_test.fk == child assert isinstance(fk_test.fk, FKTestChild) def test_polymorphic_extension(self): from polymorphic.tests.models import ( NormalBase, NormalExtension, PolyExtension, PolyExtChild, ) nb = NormalBase.objects.create(nb_field=5) ne = NormalExtension.objects.create(nb_field=6, ne_field="normal ext") poly_ext = PolyExtension.objects.create(nb_field=6, ne_field="poly ext", poly_ext_field=7) child_ext = PolyExtChild.objects.create( nb_field=7, ne_field="child ext", poly_ext_field=8, poly_child_field="poly child" ) assert set(NormalBase.objects.all()) == { nb, NormalBase.objects.get(pk=ne.pk), NormalBase.objects.get(pk=poly_ext.pk), NormalBase.objects.get(pk=child_ext.pk), } assert set(NormalExtension.objects.all()) == { NormalExtension.objects.get(pk=ne.pk), NormalExtension.objects.get(pk=poly_ext.pk), NormalExtension.objects.get(pk=child_ext.pk), } assert set(PolyExtension.objects.all()) == {poly_ext, child_ext} assert set(PolyExtChild.objects.all()) == {child_ext} def test_manytomany_without_through_field(self): from polymorphic.tests.models import Lake, RedheadDuck, RubberDuck lake = Lake.objects.create() rubber = RubberDuck.objects.create(name="Rubber") redhead = RedheadDuck.objects.create(name="Redheat") lake.ducks.add(rubber) lake.ducks.add(redhead) self.assertEqual(lake.ducks.count(), 2) self.assertIsInstance(lake.ducks.all()[0], RubberDuck) self.assertIsInstance(lake.ducks.all()[1], RedheadDuck) def test_manytomany_with_through_field(self): from polymorphic.tests.models import LakeWithThrough, DucksLake, RedheadDuck, RubberDuck lake = LakeWithThrough.objects.create() rubber = RubberDuck.objects.create(name="Rubber") redhead = RedheadDuck.objects.create(name="Redheat") DucksLake.objects.create(lake=lake, duck=rubber, time="morning") DucksLake.objects.create(lake=lake, duck=redhead, time="afternoon") self.assertEqual(lake.ducks.count(), 2) self.assertIsInstance(lake.ducks.all()[0], RubberDuck) self.assertIsInstance(lake.ducks.all()[1], RedheadDuck) def test_create_from_super(self): # run create test 3 times because initial implementation # would fail after first success. from polymorphic.tests.models import ( NormalBase, NormalExtension, PolyExtension, PolyExtChild, CustomPkBase, CustomPkInherit, ) nb = NormalBase.objects.create(nb_field=1) ne = NormalExtension.objects.create(nb_field=2, ne_field="ne2") with self.assertRaises(TypeError): PolyExtension.objects.create_from_super(nb, poly_ext_field=3) with CaptureQueriesContext(connection) as ctx: pe = PolyExtension.objects.create_from_super(ne, poly_ext_field=3) # for q in ctx.captured_queries: # print(q["sql"]) ne.refresh_from_db() self.assertEqual(type(ne), NormalExtension) self.assertEqual(type(pe), PolyExtension) self.assertEqual(pe.pk, ne.pk) self.assertEqual(pe.nb_field, 2) self.assertEqual(pe.ne_field, "ne2") self.assertEqual(pe.poly_ext_field, 3) pe.refresh_from_db() self.assertEqual(pe.nb_field, 2) self.assertEqual(pe.ne_field, "ne2") self.assertEqual(pe.poly_ext_field, 3) print("===================================") with CaptureQueriesContext(connection) as ctx: """ BEGIN SELECT "django_content_type"."id", "django_content_type"."app_label", "django_content_type"."model" FROM "django_content_type" WHERE ("django_content_type"."app_label" = 'tests' AND "django_content_type"."model" = 'polyextchild') LIMIT 21 INSERT INTO "tests_polyextchild" ("polyextension_ptr_id", "poly_child_field") VALUES (2, 'pcf6') SELECT "tests_normalbase"."id", "tests_normalbase"."nb_field", "tests_normalextension"."normalbase_ptr_id", "tests_normalextension"."ne_field", "tests_polyextension"."normalextension_ptr_id", "tests_polyextension"."polymorphic_ctype_id", "tests_polyextension"."poly_ext_field" FROM "tests_polyextension" INNER JOIN "tests_normalextension" ON ("tests_polyextension"."normalextension_ptr_id" = "tests_normalextension"."normalbase_ptr_id") INNER JOIN "tests_normalbase" ON ("tests_normalextension"."normalbase_ptr_id" = "tests_normalbase"."id") WHERE "tests_polyextension"."normalextension_ptr_id" = 2 LIMIT 21 UPDATE "tests_normalbase" SET "nb_field" = 2 WHERE "tests_normalbase"."id" = 2 UPDATE "tests_normalextension" SET "ne_field" = 'ne2' WHERE "tests_normalextension"."normalbase_ptr_id" = 2 UPDATE "tests_polyextension" SET "polymorphic_ctype_id" = 100, "poly_ext_field" = 3 WHERE "tests_polyextension"."normalextension_ptr_id" = 2 SELECT "tests_normalbase"."id", "tests_normalbase"."nb_field", "tests_normalextension"."normalbase_ptr_id", "tests_normalextension"."ne_field", "tests_polyextension"."normalextension_ptr_id", "tests_polyextension"."polymorphic_ctype_id", "tests_polyextension"."poly_ext_field", "tests_polyextchild"."polyextension_ptr_id", "tests_polyextchild"."poly_child_field" FROM "tests_polyextchild" INNER JOIN "tests_polyextension" ON ("tests_polyextchild"."polyextension_ptr_id" = "tests_polyextension"."normalextension_ptr_id") INNER JOIN "tests_normalextension" ON ("tests_polyextension"."normalextension_ptr_id" = "tests_normalextension"."normalbase_ptr_id") INNER JOIN "tests_normalbase" ON ("tests_normalextension"."normalbase_ptr_id" = "tests_normalbase"."id") WHERE "tests_polyextchild"."polyextension_ptr_id" = 2 LIMIT 21 COMMIT """ pc = PolyExtChild.objects.create_from_super(pe, poly_child_field="pcf6") # for q in ctx.captured_queries: # print(q["sql"]) pe.refresh_from_db() ne.refresh_from_db() self.assertEqual(type(ne), NormalExtension) self.assertEqual(type(pe), PolyExtension) self.assertEqual(pe.pk, ne.pk) self.assertEqual(pe.pk, pc.pk) self.assertEqual(pc.nb_field, 2) self.assertEqual(pc.ne_field, "ne2") self.assertEqual(pc.poly_ext_field, 3) pc.refresh_from_db() self.assertEqual(pc.nb_field, 2) self.assertEqual(pc.ne_field, "ne2") self.assertEqual(pc.poly_ext_field, 3) self.assertEqual(pc.poly_child_field, "pcf6") self.assertEqual(pe.polymorphic_ctype, ContentType.objects.get_for_model(PolyExtChild)) self.assertEqual(pc.polymorphic_ctype, ContentType.objects.get_for_model(PolyExtChild)) self.assertEqual(set(PolyExtension.objects.all()), {pc}) a1 = Model2A.objects.create(field1="A1a") a2 = Model2A.objects.create(field1="A1b") b1 = Model2B.objects.create(field1="B1a", field2="B2a") b2 = Model2B.objects.create(field1="B1b", field2="B2b") c1 = Model2C.objects.create(field1="C1a", field2="C2a", field3="C3a") c2 = Model2C.objects.create(field1="C1b", field2="C2b", field3="C3b") d1 = Model2D.objects.create(field1="D1a", field2="D2a", field3="D3a", field4="D4a") d2 = Model2D.objects.create(field1="D1b", field2="D2b", field3="D3b", field4="D4b") with self.assertRaises(TypeError): Model2D.objects.create_from_super(b1, field3="D3x", field4="D4x") b1_of_c = Model2B.objects.non_polymorphic().get(pk=c1.pk) with self.assertRaises(TypeError): Model2C.objects.create_from_super(b1_of_c, field3="C3x") self.assertEqual(c1.polymorphic_ctype, ContentType.objects.get_for_model(Model2C)) dfs1 = Model2D.objects.create_from_super(b1_of_c, field4="D4x") self.assertEqual(type(dfs1), Model2D) self.assertEqual(dfs1.pk, c1.pk) self.assertEqual(dfs1.field1, "C1a") self.assertEqual(dfs1.field2, "C2a") self.assertEqual(dfs1.field3, "C3a") self.assertEqual(dfs1.field4, "D4x") self.assertEqual(dfs1.polymorphic_ctype, ContentType.objects.get_for_model(Model2D)) c1.refresh_from_db() self.assertEqual(c1.polymorphic_ctype, ContentType.objects.get_for_model(Model2D)) self.assertEqual(b2.polymorphic_ctype, ContentType.objects.get_for_model(Model2B)) cfs1 = Model2C.objects.create_from_super(b2, field3="C3y") self.assertEqual(type(cfs1), Model2C) self.assertEqual(cfs1.pk, b2.pk) self.assertEqual(cfs1.field1, "B1b") self.assertEqual(cfs1.field2, "B2b") self.assertEqual(cfs1.field3, "C3y") b2.refresh_from_db() self.assertEqual(b2.polymorphic_ctype, ContentType.objects.get_for_model(Model2C)) self.assertEqual(cfs1.polymorphic_ctype, ContentType.objects.get_for_model(Model2C)) self.assertEqual(set(Model2A.objects.all()), {a1, a2, b1, dfs1, cfs1, c2, d1, d2}) custom_pk = CustomPkBase.objects.create(b="0") custom_pk_ext = CustomPkInherit.objects.create_from_super(custom_pk, i="4") self.assertEqual(type(custom_pk_ext), CustomPkInherit) custom_pk_ext.refresh_from_db() self.assertEqual(custom_pk_ext.id, custom_pk.id) self.assertEqual(CustomPkBase.objects.get(pk=custom_pk.id), custom_pk_ext) self.assertEqual(CustomPkBase.objects.count(), 1) custom_pk2 = CustomPkBase.objects.create(b="2") custom_pk_ext2 = CustomPkInherit.objects.create_from_super( custom_pk2, custom_id=100, i="4" ) self.assertEqual(type(custom_pk_ext2), CustomPkInherit) custom_pk_ext2.refresh_from_db() self.assertEqual(custom_pk_ext2.id, custom_pk2.id) self.assertEqual(custom_pk_ext2.custom_id, 100) self.assertEqual(CustomPkBase.objects.get(pk=custom_pk2.id), custom_pk_ext2) self.assertEqual(CustomPkBase.objects.count(), 2) def test_create_from_super_child_exists(self): """ Test several scenarios creating a child row where a parent already exists. Should get integrity errors! """ from polymorphic.tests.models import ( NormalExtension, PolyExtension, CustomPkBase, CustomPkInherit, ) pe1 = PolyExtension.objects.create(nb_field=10, ne_field="ne10", poly_ext_field=20) ne1 = NormalExtension.objects.get(pk=pe1.pk) with self.assertRaises(IntegrityError): PolyExtension.objects.create_from_super(ne1, poly_ext_field=30) # FIXME: uncomment when #686 is fixed # CustomPkInherit.objects.create(b="base1", i="1") # with self.assertRaises(IntegrityError): # CustomPkInherit.objects.create_from_super( # CustomPkBase.objects.non_polymorphic().first(), i="2" # ) def test_through_models_creates_and_reads(self): from polymorphic.tests.models import ( BetMultiple, ChoiceAthlete, ChoiceBlank, RankedAthlete, ) bet = BetMultiple.objects.create() a1 = ChoiceAthlete.objects.create(choice="Alice") a2 = ChoiceAthlete.objects.create(choice="Bob") a3 = ChoiceBlank.objects.create() # Exercise the "through" model via the M2M manager using through_defaults. bet.answer.add(a1, through_defaults={"rank": 2}) bet.answer.add(a2, through_defaults={"rank": 1}) bet.answer.add(a3, through_defaults={"rank": 3}) # ChoiceBlank also works # Through rows were created with rank preserved rows = list( RankedAthlete.objects.filter(bet=bet) .order_by("rank") .values_list("choiceAthlete_id", "rank") ) assert rows == [(a2.pk, 1), (a1.pk, 2), (a3.pk, 3)] # Reading back via the M2M returns polymorphic instances (ChoiceAthlete, not ChoiceBlank) answers = list(bet.answer.order_by("rankedathlete__rank")) assert answers == [a2, a1, a3] assert isinstance(answers[0], ChoiceAthlete) assert isinstance(answers[1], ChoiceAthlete) assert isinstance(answers[2], ChoiceBlank) # Sanity: the through model is the one we expect assert bet.answer.through is RankedAthlete assert isinstance(bet.answer, PolymorphicManager) def test_through_model_updates(self): from polymorphic.tests.models import BetMultiple, ChoiceAthlete, RankedAthlete, ChoiceBlank bet = BetMultiple.objects.create() a1 = ChoiceAthlete.objects.create(choice="Alice") a2 = ChoiceBlank.objects.create() bet.answer.add(a2, through_defaults={"rank": 0}) bet.answer.add(a1, through_defaults={"rank": 1}) ra = RankedAthlete.objects.get(bet=bet, choiceAthlete=a1) assert ra.rank == 1 ra.rank = 99 ra.save(update_fields=["rank"]) ra2 = RankedAthlete.objects.get(bet=bet, choiceAthlete=a2) assert ra2.rank == 0 # ordering uses the through-table rank assert list(bet.answer.order_by("rankedathlete__rank")) == [a2, a1] assert RankedAthlete.objects.get(pk=ra.pk).rank == 99 assert RankedAthlete.objects.get(pk=ra2.pk).rank == 0 def test_infinite_recursion_with_only(self): """ https://github.com/jazzband/django-polymorphic/issues/334 """ from polymorphic.tests.models import RecursionBug draft = PlainA.objects.create(field1="draft") closed = PlainA.objects.create(field1="closed") assert isinstance(closed.recursions, PolymorphicManager) item = RecursionBug.objects.create(status=draft) RecursionBug.objects.filter(id=item.id).update(status=closed) item.refresh_from_db(fields=("status",)) assert item.status == closed @pytest.mark.skipif( Version(django.get_version()) < Version("5.0"), reason="Requires Django 5.0+", ) def test_generic_relation_prefetch(self): """ https://github.com/jazzband/django-polymorphic/issues/613 """ from polymorphic.tests.models import Bookmark, TaggedItem, Assignment from django.contrib.contenttypes.prefetch import GenericPrefetch bm1 = Bookmark.objects.create(url="http://example.com/1") ass = Assignment.objects.create(url="http://example.com/2", assigned_to="Alice") TaggedItem.objects.create(tag="tag1", content_object=bm1) TaggedItem.objects.create(tag="tag2", content_object=ass) bookmarks = list(Bookmark.objects.prefetch_related("tags").order_by("pk")) assert len(bookmarks) == 2 assert list(bookmarks[0].tags.all()) == [TaggedItem.objects.get(tag="tag1")] assert list(bookmarks[1].tags.all()) == [TaggedItem.objects.get(tag="tag2")] assert bookmarks[0].__class__ is Bookmark assert bookmarks[1].__class__ is Assignment tags = TaggedItem.objects.prefetch_related( GenericPrefetch( lookup="content_object", querysets=[ Bookmark.objects.all(), ], ), ).order_by("pk") assert tags[0].content_object == bookmarks[0] assert tags[1].content_object == bookmarks[1] for tag in tags.all(): assert tag.content_object def test_besteffort_iteration(self): """ Test that our best effort iteration avoids n+1 queries when n objects have stale content type pointers. """ for i in range(100): Model2A.objects.create(field1=f"Model2C_{i}") # force stale ctype condition Model2A.objects.all().update(polymorphic_ctype=ContentType.objects.get_for_model(Model2C)) assert Model2C.objects.count() == 0 assert Model2B.objects.count() == 0 assert Model2A.objects.count() == 100 with CaptureQueriesContext(connection) as initial_all_2a: for obj in Model2A.objects.all(): assert obj.__class__ is Model2A assert len(initial_all_2a.captured_queries) <= 4 def test_besteffort_get_real_instance(self): obj = Model2B.objects.create(field1="TestB", field2="TestB2") obj.polymorphic_ctype = ContentType.objects.get_for_model(Model2C) obj.save() as_a = Model2A.objects.non_polymorphic().get(pk=obj.pk) assert as_a.__class__ is Model2A should_be_b = as_a.get_real_instance() assert should_be_b.__class__ is Model2B # ctype should still be wrong assert should_be_b.polymorphic_ctype == ContentType.objects.get_for_model(Model2C) def test_queryset_first_returns_none_on_empty_queryset(self): self.assertIsNone(Model2A.objects.first()) def test_queryset_getitem_raises_indexerror_on_empty_queryset(self): with self.assertRaises(IndexError): _ = Model2A.objects.all()[0] def test_queryset_getitem_negative_index_raises_valueerror(self): Model2A.objects.create(field1="OnlyOne") with self.assertRaises(ValueError): _ = Model2A.objects.all()[-1] def test_queryset_getitem_slice_returns_objects(self): Model2A.objects.create(field1="First") Model2A.objects.create(field1="Second") objs = Model2A.objects.all()[0:2] self.assertEqual(len(objs), 2) self.assertEqual([o.field1 for o in objs], ["First", "Second"]) def test_aggregate_with_filtered_relation(self): """Test _process_aggregate_args with FilteredRelation (lines 273-280)""" # Create test data a1 = Model2A.objects.create(field1="A1") b1 = Model2B.objects.create(field1="B1", field2="B2") c1 = Model2C.objects.create(field1="C1", field2="C2", field3="C3") # Create related objects rel1 = RelatingModel.objects.create() rel2 = RelatingModel.objects.create() rel1.many2many.add(a1, b1) rel2.many2many.add(c1) # Test FilteredRelation with annotate # This exercises the patch_lookup function with FilteredRelation qs = RelatingModel.objects.annotate( filtered_m2m=FilteredRelation( "many2many", condition=Q(many2many__field1__startswith="B") ) ).filter(filtered_m2m__isnull=False) assert rel1 in qs assert rel2 not in qs def test_aggregate_with_nested_q_objects(self): """Test _process_aggregate_args with nested Q objects (lines 285-298)""" a1 = Model2A.objects.create(field1="A1") b1 = Model2B.objects.create(field1="B1", field2="B2") c1 = Model2C.objects.create(field1="C1", field2="C2", field3="C3") # Test with nested Q objects in annotate # This exercises the tree_node_test___lookup function result = Model2A.objects.annotate( has_b_field=Case( When(Q(field1__startswith="B") | Q(field1__startswith="C"), then=1), default=0, output_field=models.IntegerField(), ) ).filter(has_b_field=1) assert b1 in result assert c1 in result assert a1 not in result def test_aggregate_with_subclass_field_in_expression(self): """Test _process_aggregate_args with source expressions (lines 275-278, 300-303)""" b1 = Model2B.objects.create(field1="B1", field2="100") b2 = Model2B.objects.create(field1="B2", field2="200") c1 = Model2C.objects.create(field1="C1", field2="150", field3="C3") # Test with complex expression containing field references # This exercises the get_source_expressions path from django.db.models import F, Value from django.db.models.functions import Concat result = Model2A.objects.annotate( combined=Concat(F("field1"), Value(" - "), F("Model2B___field2")) ).filter(Model2B___field2__isnull=False) assert b1 in result assert b2 in result assert c1 in result # C inherits from B def test_get_best_effort_instance_with_missing_derived(self): """Test _get_best_effort_instance when derived class is missing (lines 339-387)""" # Create a Model2C object (which inherits from Model2B -> Model2A) c1 = Model2C.objects.create(field1="C1", field2="C2", field3="C3") c1_pk = c1.pk # Delete the Model2C part but keep Model2B and Model2A parts # This simulates a partially deleted object c1.delete(keep_parents=True) # Now try to fetch it - should fall back to Model2B result = list(Model2A.objects.filter(pk=c1_pk)) assert len(result) == 1 assert result[0].pk == c1_pk assert isinstance(result[0], Model2B) assert not isinstance(result[0], Model2C) def test_get_best_effort_instance_with_annotations(self): """Test _get_best_effort_instance preserves annotations (lines 367-374)""" # Create a Model2C object c1 = Model2C.objects.create(field1="C1", field2="C2", field3="C3") c1_pk = c1.pk # Add annotation annotated = Model2A.objects.annotate(field_count=Count("field1")).filter(pk=c1_pk) # Delete the Model2C part c1.delete(keep_parents=True) # Fetch with annotation - should preserve annotation on fallback object result = list(annotated) assert len(result) == 1 assert hasattr(result[0], "field_count") assert result[0].field_count == 1 def test_get_best_effort_instance_with_extra_select(self): """Test _get_best_effort_instance preserves extra select (lines 376-379)""" # Create a Model2C object c1 = Model2C.objects.create(field1="C1", field2="C2", field3="C3") c1_pk = c1.pk # Add extra select qs = Model2A.objects.extra(select={"upper_field1": "UPPER(field1)"}).filter(pk=c1_pk) # Delete the Model2C part c1.delete(keep_parents=True) # Fetch with extra - should preserve extra on fallback object result = list(qs) assert len(result) == 1 assert hasattr(result[0], "upper_field1") assert result[0].upper_field1 == "C1" def test_get_best_effort_instance_multiple_inheritance_levels(self): """Test _get_best_effort_instance walks up multiple levels (lines 351-384)""" # Create a Model2D object (D -> C -> B -> A) d1 = Model2D.objects.create(field1="D1", field2="D2", field3="D3", field4="D4") d1_pk = d1.pk # Delete Model2D part, keep Model2C d1.delete(keep_parents=True) # Should fall back to Model2C result = list(Model2A.objects.filter(pk=d1_pk)) assert len(result) == 1 assert isinstance(result[0], Model2C) assert not isinstance(result[0], Model2D) # Now delete Model2C part too c1 = Model2C.objects.get(pk=d1_pk) c1.delete(keep_parents=True) # Should fall back to Model2B result = list(Model2A.objects.filter(pk=d1_pk)) assert len(result) == 1 assert isinstance(result[0], Model2B) assert not isinstance(result[0], Model2C) def test_deferred_loading_with_subclass_syntax(self): """Test deferred loading with Model___field syntax (lines 481-505)""" b1 = Model2B.objects.create(field1="B1", field2="B2") c1 = Model2C.objects.create(field1="C1", field2="C2", field3="C3") # Test defer with subclass field syntax qs = Model2A.objects.defer("Model2B___field2") result = list(qs) # field2 should be deferred for Model2B instances b_obj = [r for r in result if r.pk == b1.pk][0] assert isinstance(b_obj, Model2B) # Accessing deferred field should trigger a query assert b_obj.field2 == "B2" def test_deferred_loading_with_nonexistent_field(self): """Test deferred loading handles non-existent fields gracefully (lines 496-501)""" b1 = Model2B.objects.create(field1="B1", field2="B2") # Try to defer a field that doesn't exist in Model2B using subclass syntax # This should be handled gracefully (field doesn't exist in this subclass) qs = Model2A.objects.defer("Model2C___field3") result = list(qs) # Should still work, just ignoring the non-existent field for Model2B assert len(result) == 1 assert result[0].field1 == "B1" def test_only_with_subclass_syntax(self): """Test only() with Model___field syntax (lines 214-226)""" b1 = Model2B.objects.create(field1="B1", field2="B2") c1 = Model2C.objects.create(field1="C1", field2="C2", field3="C3") # Test only with subclass field syntax qs = Model2A.objects.only("field1", "Model2B___field2") result = list(qs) # Only field1 and field2 should be loaded for Model2B instances b_obj = [r for r in result if r.pk == b1.pk][0] assert isinstance(b_obj, Model2B) assert b_obj.field1 == "B1" assert b_obj.field2 == "B2" def test_real_instances_with_stale_content_type(self): """Test _get_real_instances handles stale content types (lines 451-453)""" # This test verifies the stale content type handling by checking # that objects with invalid content type IDs are skipped gracefully # We'll use the existing prefetch_related_with_missing test pattern # which already covers this scenario pass # Covered by test_prefetch_related_with_missing def test_real_instances_with_proxy_model(self): """Test _get_real_instances handles proxy models (lines 527-529)""" # Create a proxy model instance proxy = ProxyModelA.objects.create(field1="Proxy1") # Fetch through base class result = list(ProxyModelBase.objects.filter(pk=proxy.pk)) assert len(result) == 1 assert isinstance(result[0], ProxyModelA) assert result[0].field1 == "Proxy1" def test_annotate_with_polymorphic_field_path(self): """Test annotate with polymorphic field paths (lines 312-316)""" b1 = Model2B.objects.create(field1="B1", field2="B2") b2 = Model2B.objects.create(field1="B2", field2="B3") c1 = Model2C.objects.create(field1="C1", field2="C2", field3="C3") # Test annotate with subclass field result = Model2A.objects.annotate(b_field_count=Count("Model2B___field2")) # All objects should be returned with annotation assert result.count() == 3 def test_aggregate_with_polymorphic_field_path(self): """Test aggregate with polymorphic field paths (lines 318-323)""" b1 = Model2B.objects.create(field1="B1", field2="10") b2 = Model2B.objects.create(field1="B2", field2="20") c1 = Model2C.objects.create(field1="C1", field2="30", field3="C3") # Test aggregate with subclass field # This should use non_polymorphic internally result = Model2A.objects.aggregate(total=Count("Model2B___field2")) assert "total" in result assert result["total"] >= 0 def test_disparate_pk_values_in_hierarchy(self): """ Test that polymorphic models with different primary key field types and values at different levels of the inheritance hierarchy can be created, queried, and deleted without issues. """ from polymorphic.tests.models import ( DisparateKeysParent, RelatedKeyModel, DisparateKeysChild1, DisparateKeysChild2, DisparateKeysGrandChild, DisparateKeysGrandChild2, ) extern_key1 = RelatedKeyModel.objects.create() extern_key2 = RelatedKeyModel.objects.create() extern_key3 = RelatedKeyModel.objects.create() parent1 = DisparateKeysParent.objects.create(text="parent1") parent2 = DisparateKeysParent.objects.create(text="parent2") child1 = DisparateKeysChild1.objects.create( text="child1", text_child1="child1 extra", key=extern_key1 ) child2 = DisparateKeysChild1.objects.create( text="child2", text_child1="child2 extra", key=extern_key2 ) grandchild1 = DisparateKeysGrandChild.objects.create( text="grandchild1", text_child1="grandchild1 extra", text_grand_child="grandchild1 extra extra", key=extern_key3, ) grandchild2 = DisparateKeysGrandChild2.objects.create( text="grandchild2", text_child2="grandchild2 extra", text_grand_child="grandchild2 extra extra", id=50, key=100, ) child2_1 = DisparateKeysChild2.objects.create( text="child2_1", text_child2="child2_1 extra", key=101 ) child2_2 = DisparateKeysChild2.objects.create( text="child2_2", text_child2="child2_2 extra", key=102 ) assert set(DisparateKeysParent.objects.all()) == { parent1, parent2, child1, child2, grandchild1, child2_1, child2_2, grandchild2, } assert set(DisparateKeysChild1.objects.all()) == {child1, child2, grandchild1} assert set(DisparateKeysChild2.objects.all()) == {child2_1, child2_2, grandchild2} assert set(DisparateKeysGrandChild.objects.all()) == {grandchild1} assert set(DisparateKeysGrandChild2.objects.all()) == {grandchild2} # test get_real_instance real_instances = set() for obj in DisparateKeysParent.objects.non_polymorphic().all(): real_instances.add(obj.get_real_instance()) assert real_instances == { parent1, parent2, child1, child2, grandchild1, child2_1, child2_2, grandchild2, } # test parentage links assert grandchild2.disparatekeyschild2_ptr.__class__ == DisparateKeysChild2 assert ( grandchild2.disparatekeyschild2_ptr == DisparateKeysChild2.objects.non_polymorphic().get(key=grandchild2.key) ) assert grandchild2.disparatekeysparent_ptr.__class__ == DisparateKeysParent assert ( grandchild2.disparatekeysparent_ptr == DisparateKeysParent.objects.non_polymorphic().get(pk=grandchild2.id) ) DisparateKeysGrandChild2.objects.all().delete() django-polymorphic-4.10.2/src/polymorphic/tests/test_performance.py000066400000000000000000000021421513173623500256200ustar00rootroot00000000000000from django.test import TransactionTestCase from polymorphic.tests.models import ( Model2A, Model2B, Model2C, Model2D, ) class PerformanceTests(TransactionTestCase): def test_baseline_number_of_queries(self): """ Test that the number of queries for loading polymorphic models is within expected limits. """ for idx in range(100): Model2A.objects.create(field1=f"A{idx}") Model2B.objects.create(field1=f"A{idx}", field2=f"B{idx}") Model2C.objects.create(field1=f"A{idx}", field2=f"B{idx}", field3=f"C{idx}") Model2D.objects.create( field1=f"A{idx}", field2=f"B{idx}", field3=f"C{idx}", field4=f"D{idx}" ) with self.assertNumQueries(4): list(Model2A.objects.all().order_by("pk")) with self.assertNumQueries(3): list(Model2B.objects.all().order_by("pk")) with self.assertNumQueries(2): list(Model2C.objects.all().order_by("pk")) with self.assertNumQueries(1): list(Model2D.objects.all().order_by("pk")) django-polymorphic-4.10.2/src/polymorphic/tests/test_query_translate.py000066400000000000000000000077551513173623500265600ustar00rootroot00000000000000import copy import tempfile import pickle import threading from django.db.models import Q from django.test import TestCase from polymorphic.tests.models import Bottom, Middle, Top from polymorphic.query_translate import translate_polymorphic_filter_definitions_in_args class QueryTranslateTests(TestCase): def test_translate_with_not_pickleable_query(self): """ In some cases, Django may attacha _thread object to the query and we will get the following when we try to deepcopy inside of translate_polymorphic_filter_definitions_in_args: TypeError: cannot pickle '_thread.lock' object For this to trigger, we need to somehoe go down this path: File "/perfdash/.venv/lib64/python3.12/site-packages/polymorphic/query_translate.py", line 95, in translate_polymorphic_filter_definitions_in_args translate_polymorphic_Q_object(queryset_model, copy.deepcopy(q), using=using) for q in args ^^^^^^^^^^^^^^^^ File "/usr/lib64/python3.12/copy.py", line 143, in deepcopy y = copier(memo) ^^^^^^^^^^^^ File "/perfdash/.venv/lib64/python3.12/site-packages/django/utils/tree.py", line 53, in __deepcopy__ obj.children = copy.deepcopy(self.children, memodict) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib64/python3.12/copy.py", line 136, in deepcopy y = copier(x, memo) ^^^^^^^^^^^^^^^ Internals in Django, somehow we must trigger this tree.py code in django via the deepcopy in order to trigger this. """ with tempfile.TemporaryFile() as fd: # verify this is definitely not pickleable with self.assertRaises(TypeError): pickle.dumps(threading.Lock()) # I know this doesn't make sense to pass as a Q(), but # I haven't found another way to trigger the copy.deepcopy failing. q = Q(blog__info="blog info") | Q(blog__info=threading.Lock()) translate_polymorphic_filter_definitions_in_args(Bottom, args=[q]) def test_deep_copy_of_q_objects(self): import os from polymorphic.tests.models import DeepCopyTester, DeepCopyTester2 # binary fields can have an unpickleable memoryview object in them # see https://github.com/jazzband/django-polymorphic/issues/524 d1_bf = os.urandom(32) d2_bf1 = os.urandom(32) d2_bf2 = os.urandom(32) dct1 = DeepCopyTester.objects.create(binary_field=d1_bf) dct2 = DeepCopyTester2.objects.create(binary_field=d2_bf1, binary_field2=d2_bf2) self.assertEqual(list(DeepCopyTester.objects.filter(binary_field=d1_bf).all()), [dct1]) q1 = Q(DeepCopyTester2___binary_field2=d2_bf1) self.assertEqual(list(DeepCopyTester.objects.filter(q1).all()), []) assert q1.children[0][0] == "DeepCopyTester2___binary_field2" q2 = Q(DeepCopyTester2___binary_field2=d2_bf2) self.assertEqual(list(DeepCopyTester.objects.filter(q2).all()), [dct2]) assert q2.children[0][0] == "DeepCopyTester2___binary_field2" assert len(DeepCopyTester.objects.filter(Q(binary_field=memoryview(d1_bf)))) == 1 self.assertEqual(DeepCopyTester.objects.all().delete()[0], 3) self.assertEqual(DeepCopyTester.objects.count(), 0) def test_proxy_model_query_related_name(self): """Test _get_query_related_name fallback for proxy models""" from polymorphic.query_translate import _get_query_related_name from polymorphic.tests.models import ProxyChild, SubclassSelectorProxyModel # Test that proxy models use the fallback (lowercase class name) # since they don't have a OneToOneField parent link result = _get_query_related_name(ProxyChild) assert result == "proxychild" result = _get_query_related_name(SubclassSelectorProxyModel) assert result == "subclassselectorproxymodel" django-polymorphic-4.10.2/src/polymorphic/tests/test_regression.py000066400000000000000000000427451513173623500255140ustar00rootroot00000000000000from django import forms from django.db import models from django.db.models import functions from polymorphic.models import PolymorphicModel, PolymorphicTypeInvalid from polymorphic.tests.models import ( Bottom, Middle, Top, Team, UserProfile, Model2A, Model2B, Regression295Parent, Regression295Related, RelationBase, RelationA, RelationB, SpecialBook, Book, ) from django.test import TestCase from django.contrib.contenttypes.models import ContentType from polymorphic.formsets import polymorphic_modelformset_factory, PolymorphicFormSetChild class RegressionTests(TestCase): def test_for_query_result_incomplete_with_inheritance(self): """https://github.com/bconstantin/django_polymorphic/issues/15""" top = Top() top.save() middle = Middle() middle.save() bottom = Bottom() bottom.save() expected_queryset = [top, middle, bottom] self.assertQuerySetEqual( Top.objects.order_by("pk"), [repr(r) for r in expected_queryset], transform=repr, ) expected_queryset = [middle, bottom] self.assertQuerySetEqual( Middle.objects.order_by("pk"), [repr(r) for r in expected_queryset], transform=repr, ) expected_queryset = [bottom] self.assertQuerySetEqual( Bottom.objects.order_by("pk"), [repr(r) for r in expected_queryset], transform=repr, ) def test_pr_254(self): user_a = UserProfile.objects.create(name="a") user_b = UserProfile.objects.create(name="b") user_c = UserProfile.objects.create(name="c") team1 = Team.objects.create(team_name="team1") team1.user_profiles.add(user_a, user_b, user_c) team1.save() team2 = Team.objects.create(team_name="team2") team2.user_profiles.add(user_c) team2.save() # without prefetch_related, the test passes my_teams = ( Team.objects.filter(user_profiles=user_c) .order_by("team_name") .prefetch_related("user_profiles") .distinct() ) self.assertEqual(len(my_teams[0].user_profiles.all()), 3) self.assertEqual(len(my_teams[1].user_profiles.all()), 1) self.assertEqual(len(my_teams[0].user_profiles.all()), 3) self.assertEqual(len(my_teams[1].user_profiles.all()), 1) # without this "for" loop, the test passes for _ in my_teams: pass # This time, test fails. PR 254 claim # with sqlite: 4 != 3 # with postgresql: 2 != 3 self.assertEqual(len(my_teams[0].user_profiles.all()), 3) self.assertEqual(len(my_teams[1].user_profiles.all()), 1) def test_alias_queryset(self): """ Test that .alias() works works correctly with polymorphic querysets. It should not raise AttributeError, and the aliased field should NOT be present on the instance. """ Model2B.objects.create(field1="val1", field2="val2") # Scenario 1: .alias() only # Should not crash, and 'lower_field1' should NOT be an attribute qs = Model2A.objects.alias(lower_field1=functions.Lower("field1")) results = list(qs) self.assertEqual(len(results), 1) self.assertIsInstance(results[0], Model2B) self.assertFalse(hasattr(results[0], "lower_field1")) # Scenario 2: .annotate() # Should work, and 'upper_field1' SHOULD be an attribute qs = Model2A.objects.annotate(upper_field1=functions.Upper("field1")) results = list(qs) self.assertEqual(len(results), 1) self.assertTrue(hasattr(results[0], "upper_field1")) self.assertEqual(results[0].upper_field1, "VAL1") # Scenario 3: Mixed alias() and annotate() qs = Model2A.objects.alias(alias_val=functions.Lower("field1")).annotate( anno_val=functions.Upper("field1") ) results = list(qs) self.assertEqual(len(results), 1) self.assertFalse(hasattr(results[0], "alias_val")) self.assertTrue(hasattr(results[0], "anno_val")) self.assertEqual(results[0].anno_val, "VAL1") def test_alias_advanced(self): """ Test .alias() interactions with filter, order_by, only, and defer. """ obj1 = Model2B.objects.create(field1="Alpha", field2="One") obj2 = Model2B.objects.create(field1="Beta", field2="Two") obj3 = Model2B.objects.create(field1="Gamma", field2="Three") # 1. Filter by alias qs = Model2A.objects.alias(lower_f1=functions.Lower("field1")).filter(lower_f1="beta") self.assertEqual(qs.count(), 1) self.assertEqual(qs[0], obj2) self.assertFalse(hasattr(qs[0], "lower_f1")) # 2. Order by alias qs = Model2A.objects.alias(len_f2=functions.Length("model2b__field2")).order_by("len_f2") # Lengths: One=3, Two=3, Three=5. (Ordering of equal values is DB dep, but logic holds) results = list(qs) self.assertEqual(len(results), 3) self.assertFalse(hasattr(results[0], "len_f2")) # 3. Alias + Only qs = Model2A.objects.alias(lower_f1=functions.Lower("field1")).only("field1") # Should not crash results = list(qs) self.assertEqual(len(results), 3) # Verify deferral logic didn't break # accessing field1 should not trigger refresh (hard to test without internals, but basic access works) self.assertEqual(results[0].field1, "Alpha") # 4. Alias + Defer qs = Model2A.objects.alias(lower_f1=functions.Lower("field1")).defer("field1") results = list(qs) self.assertEqual(len(results), 3) # accessing field1 should trigger refresh self.assertEqual(results[0].field1, "Alpha") def test_upcasting_to_sibling_class(self): """ Test that querying a model that has been upcasted to a sibling polymorphic class does not raise a TypeError. Reproduces issue #280. """ # Create a Model2A instance with a specific pk Model2A.objects.create(pk=1, field1="original") # "Upcast" it to a Model2B by creating an object with the same pk. # The polymorphic_ctype will now point to Model2B. Model2B.objects.create(pk=1, field1="updated", field2="new") # The original bug raised TypeError. We expect that accessing the # queryset should not raise TypeError. It should either be empty, # or raise a clean PolymorphicTypeInvalid error. try: list(Model2A.objects.all()) except PolymorphicTypeInvalid: pass # This is an acceptable outcome. except TypeError as e: self.fail(f"Querying for upcasted sibling raised TypeError: {e}") def test_mixed_inheritance_save_issue_495(self): """ Test that saving models with mixed polymorphic and non-polymorphic inheritance works correctly. This addresses issue #495. """ from polymorphic.tests.models import NormalExtension, PolyExtension, PolyExtChild # Create and save NormalExtension normal_ext = NormalExtension.objects.create(nb_field=1, ne_field="normal") normal_ext.add_to_ne(" extended") normal_ext.refresh_from_db() self.assertEqual(normal_ext.ne_field, "normal extended") normal_ext.add_to_nb(5) normal_ext.refresh_from_db() self.assertEqual(normal_ext.nb_field, 6) # Create and save PolyExtension poly_ext = PolyExtension.objects.create(nb_field=1, ne_field="normal", poly_ext_field=10) poly_ext.add_to_ne(" extended") poly_ext.refresh_from_db() self.assertEqual(poly_ext.ne_field, "normal extended") poly_ext.add_to_ext(5) poly_ext.refresh_from_db() self.assertEqual(poly_ext.poly_ext_field, 15) poly_ext.add_to_nb(5) poly_ext.refresh_from_db() self.assertEqual(poly_ext.nb_field, 6) # Create and save PolyExtChild poly_child = PolyExtChild.objects.create( nb_field=1, ne_field="normal", poly_ext_field=20, poly_child_field="child" ) poly_child.add_to_ne(" extended") poly_child.add_to_nb(5) poly_child.add_to_ext(10) poly_child.add_to_child(" added") poly_child.refresh_from_db() self.assertEqual(poly_child.nb_field, 6) self.assertEqual(poly_child.ne_field, "normal extended") self.assertEqual(poly_child.poly_ext_field, 30) self.assertEqual(poly_child.poly_child_field, "child added") poly_child.override_add_to_ne(" overridden") poly_child.override_add_to_ext(5) poly_child.refresh_from_db() self.assertEqual(poly_child.ne_field, "normal extended OVERRIDDEN") self.assertEqual(poly_child.poly_ext_field, 40) def test_create_or_update(self): """ https://github.com/jazzband/django-polymorphic/issues/494 """ from polymorphic.tests.models import Model2B, Model2C obj, created = Model2B.objects.update_or_create( field1="value1", defaults={"field2": "value2"} ) self.assertTrue(created) self.assertEqual(obj.field1, "value1") self.assertEqual(obj.field2, "value2") obj2, created = Model2B.objects.update_or_create( field1="value1", defaults={"field2": "new_value2"} ) self.assertFalse(created) self.assertEqual(obj2.pk, obj.pk) self.assertEqual(obj2.field1, "value1") self.assertEqual(obj2.field2, "new_value2") self.assertEqual(Model2B.objects.count(), 1) obj3, created = Model2C.objects.update_or_create( field1="value1", defaults={"field2": "new_value3", "field3": "value3"} ) self.assertTrue(created) self.assertEqual(Model2B.objects.count(), 2) self.assertEqual(Model2C.objects.count(), 1) self.assertEqual(obj3, Model2B.objects.order_by("pk").last()) def test_double_underscore_in_related_name(self): """ Test filtering on a related field when the relation name itself contains '__'. This reproduces the issue in #295, where 'my__relation___real_field' was being incorrectly parsed as a polymorphic lookup. """ related = Regression295Related.objects.create(_real_field="test_value") Regression295Parent.objects.create(related_object=related) # The following filter would be translated to 'related_object___real_field' # by Django's query machinery. qs = Regression295Parent.objects.filter(related_object___real_field="test_value") self.assertEqual(qs.count(), 1) def test_issue_252_abstract_base_class(self): """ Test that polymorphic models inheriting from both an abstract model and PolymorphicModel can be created and saved without IndexError. This reproduces issue #252: https://github.com/jazzband/django-polymorphic/issues/252 The issue reported an IndexError when trying to create an Event object that inherited from both an abstract model (AbstractDateInfo) and PolymorphicModel. The error occurred in Django's query_utils.py when accessing polymorphic_ctype_id. We use RelationBase which inherits from RelationAbstractModel (abstract) and PolymorphicModel, demonstrating the same pattern. """ # Test creating base polymorphic model with abstract parent # RelationBase inherits from RelationAbstractModel (abstract) and PolymorphicModel base = RelationBase(field_base="test_base") # This should not raise IndexError base.save() # Verify the object was saved correctly self.assertIsNotNone(base.pk) self.assertEqual(base.field_base, "test_base") self.assertIsNotNone(base.polymorphic_ctype_id) # Test creating child models relation_a = RelationA.objects.create(field_base="base_a", field_a="field_a_value") self.assertIsNotNone(relation_a.pk) self.assertEqual(relation_a.field_a, "field_a_value") relation_b = RelationB.objects.create(field_base="base_b", field_b="field_b_value") self.assertIsNotNone(relation_b.pk) self.assertEqual(relation_b.field_b, "field_b_value") # Test querying polymorphic objects relations = RelationBase.objects.all().order_by("pk") self.assertEqual(relations.count(), 3) # Verify polymorphic behavior - objects should be returned as their actual types self.assertIsInstance(relations[0], RelationBase) self.assertIsInstance(relations[1], RelationA) self.assertIsInstance(relations[2], RelationB) class SpecialBookForm(forms.ModelForm): class Meta: model = SpecialBook exclude = ("author",) class TestFormsetExclude(TestCase): def test_formset_child_respects_exclude(self): SpecialBookFormSet = polymorphic_modelformset_factory( Book, fields=[], formset_children=(PolymorphicFormSetChild(SpecialBook, form=SpecialBookForm),), ) formset = SpecialBookFormSet(queryset=SpecialBook.objects.none()) self.assertNotIn("author", formset.forms[0].fields) def test_formset_initial_with_contenttype_instance(self): """Test that polymorphic_ctype can be set as ContentType instance in initial data (issue #549)""" from django.contrib.contenttypes.models import ContentType ct = ContentType.objects.get_for_model(SpecialBook, for_concrete_model=False) SpecialBookFormSet = polymorphic_modelformset_factory( Book, fields="__all__", formset_children=(PolymorphicFormSetChild(SpecialBook, form=SpecialBookForm),), ) # Set initial data with ContentType instance (as users do in issue #549) formset = SpecialBookFormSet( queryset=SpecialBook.objects.none(), initial=[{"polymorphic_ctype": ct}], ) # Should not raise an error when creating the formset form = formset.forms[0] # Verify the polymorphic_ctype field is properly set up with the ID self.assertIn("polymorphic_ctype", form.fields) # The critical assertion: the field's initial value should be the ID (int), # not the ContentType instance. This proves the normalization worked. self.assertEqual(form.fields["polymorphic_ctype"].initial, ct.pk) self.assertIsInstance(form.fields["polymorphic_ctype"].initial, int) def test_formset_with_none_instance(self): """Test that formset handles None instance without AttributeError (issue #363). This occurs when a bound formset has a pk that doesn't exist in the queryset, causing Django's _existing_object to return None. The polymorphic formset must handle this gracefully instead of calling get_real_instance_class() on None. """ from django.contrib.contenttypes.models import ContentType ct = ContentType.objects.get_for_model(SpecialBook, for_concrete_model=False) SpecialBookFormSet = polymorphic_modelformset_factory( Book, fields="__all__", formset_children=(PolymorphicFormSetChild(SpecialBook, form=SpecialBookForm),), ) # Simulate the scenario where _existing_object returns None: # - Bound formset with data # - Claims to have an initial form (INITIAL_FORMS > 0) # - But the pk doesn't exist in queryset, so _existing_object returns None data = { "form-TOTAL_FORMS": "1", "form-INITIAL_FORMS": "1", "form-MIN_NUM_FORMS": "0", "form-MAX_NUM_FORMS": "1000", "form-0-id": "99999", # Non-existent pk - _existing_object will return None "form-0-polymorphic_ctype": str(ct.pk), } formset = SpecialBookFormSet(data=data, queryset=SpecialBook.objects.none()) # This should not raise AttributeError when instance is None forms = formset.forms self.assertEqual(len(forms), 1) self.assertIn("polymorphic_ctype", forms[0].fields) def test_combined_formset_behaviors(self): # 1. __init__ exclude handling child_none = PolymorphicFormSetChild(Book, form=SpecialBookForm, exclude=None) self.assertEqual(child_none.exclude, ()) child_list = PolymorphicFormSetChild(Book, form=SpecialBookForm, exclude=["author"]) self.assertIn("author", child_list.exclude) # 2. get_form exclude merging form = child_list.get_form(extra_exclude=["field1"]) self.assertIn("author", form._meta.exclude) self.assertIn("field1", form._meta.exclude) form_meta_default = child_none.get_form() self.assertIn("author", form_meta_default._meta.exclude) # 3. polymorphic_ctype normalization ct = ContentType.objects.get_for_model(SpecialBook, for_concrete_model=False) SpecialBookFormSet = polymorphic_modelformset_factory( Book, fields="__all__", formset_children=(PolymorphicFormSetChild(SpecialBook, form=SpecialBookForm),), ) formset = SpecialBookFormSet( queryset=SpecialBook.objects.none(), initial=[{"polymorphic_ctype": ct}], ) # The formset should normalize the ContentType instance to its ID form_ct = formset.forms[0] self.assertIsInstance(form_ct.initial["polymorphic_ctype"], int) self.assertEqual(form_ct.initial["polymorphic_ctype"], ct.pk) django-polymorphic-4.10.2/src/polymorphic/tests/test_serialization.py000066400000000000000000000510531513173623500262010ustar00rootroot00000000000000""" Tests for serialization and dumpdata functionality. This module tests that polymorphic models are correctly serialized using Django's dumpdata command, both via call_command and subprocess invocation. Regression test for issue #146 - ensuring dumpdata works correctly with polymorphic models. """ import json import os import pytest import tempfile import subprocess import sys from io import StringIO from pathlib import Path from django.conf import settings from django.core.management import call_command from django.db import connections from polymorphic.tests.models import ( Model2A, Model2B, Model2C, Model2BFiltered, Model2CFiltered, RelatingModel, NatKeyParent, NatKeyChild, ) from .utils import is_sqlite_in_memory, is_oracle, get_subprocess_test_db_env manage_py = Path(__file__).parent.parent.parent.parent / "manage.py" assert manage_py.exists() def call_dumpdata(*models, natural_foreign=True, natural_primary=True, all=False): out = StringIO() call_command( "dumpdata", *models, format="json", stdout=out, natural_foreign=natural_foreign, natural_primary=natural_primary, all=all, ) return json.loads(out.getvalue()) def run_dumpdata(*models, natural_foreign=True, natural_primary=True, all=False): cmd = [sys.executable, manage_py, "dumpdata", *models, "--format=json"] if natural_foreign: cmd.append("--natural-foreign") if all: cmd.append("--all") if natural_primary: cmd.append("--natural-primary") result = subprocess.run(cmd, capture_output=True, env=get_subprocess_test_db_env()) assert result.returncode == 0, result.stderr or result.stdout return json.loads(result.stdout) @pytest.fixture def dump_objects(db): return ( Model2A.objects.create(field1="A1"), Model2B.objects.create(field1="B1", field2="B2"), Model2C.objects.create(field1="C1", field2="C2", field3="C3"), Model2BFiltered.objects.create(field1="BF1", field2="BF2"), Model2CFiltered.objects.create(field1="cf1", field2="cf2", field3="cf3"), Model2CFiltered.objects.create(field1="CF1", field2="CF2", field3="CF3"), ) @pytest.fixture def natkey_dump_objects(db): """ Create a small graph of NatKeyParent / NatKeyChild instances. Returns: tuple[list[NatKeyParent], list[NatKeyChild]] """ return [ NatKeyChild.objects.create(slug=f"slug-{i}", content=f"content {i}", val=i) for i in range(5) ] @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize( "dumpdata", [ pytest.param(call_dumpdata, id="call_command"), pytest.param( run_dumpdata, id="manage", marks=pytest.mark.skipif( is_sqlite_in_memory(), reason="Subprocess test disabled for in-memory sqlite test runs", ), ), ], ) @pytest.mark.parametrize("all", [False, True]) def test_dumpdata_returns_base_objects_not_downcasted(dumpdata, dump_objects, all): """ Test that dumpdata serializes base table rows without polymorphic downcasting. When querying Model2A table directly, we should get 3 rows (A, B, C base objects). """ # Should have only Model2As assert dumpdata("tests.Model2A", all=all) == [ { "fields": {"field1": "A1", "polymorphic_ctype": ["tests", "model2a"]}, "model": "tests.model2a", "pk": dump_objects[0].pk, }, { "fields": {"field1": "B1", "polymorphic_ctype": ["tests", "model2b"]}, "model": "tests.model2a", "pk": dump_objects[1].pk, }, { "fields": {"field1": "C1", "polymorphic_ctype": ["tests", "model2c"]}, "model": "tests.model2a", "pk": dump_objects[2].pk, }, { "fields": {"field1": "BF1", "polymorphic_ctype": ["tests", "model2bfiltered"]}, "model": "tests.model2a", "pk": dump_objects[3].pk, }, { "fields": {"field1": "cf1", "polymorphic_ctype": ["tests", "model2cfiltered"]}, "model": "tests.model2a", "pk": dump_objects[4].pk, }, { "fields": {"field1": "CF1", "polymorphic_ctype": ["tests", "model2cfiltered"]}, "model": "tests.model2a", "pk": dump_objects[5].pk, }, ] @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize( "dumpdata", [ pytest.param(call_dumpdata, id="call_command"), pytest.param( run_dumpdata, id="manage", marks=pytest.mark.skipif( is_sqlite_in_memory() or is_oracle(), reason="Subprocess test disabled for in-memory sqlite test runs", ), ), ], ) @pytest.mark.parametrize("all", [False, True], ids=["default", "all"]) def test_dumpdata_all_flag(dumpdata, dump_objects, all): """Test dumping only a child model works correctly.""" expected = [ *( [ { "fields": {"model2b_ptr": dump_objects[3].pk}, "model": "tests.model2bfiltered", "pk": dump_objects[3].pk, } ] if all else [] ), { "fields": {"model2b_ptr": dump_objects[4].pk}, "model": "tests.model2bfiltered", "pk": dump_objects[4].pk, }, *( [ { "fields": {"model2b_ptr": dump_objects[5].pk}, "model": "tests.model2bfiltered", "pk": dump_objects[5].pk, } ] if all else [] ), { "fields": {"field3": "cf3", "model2bfiltered_ptr": dump_objects[4].pk}, "model": "tests.model2cfiltered", "pk": dump_objects[4].pk, }, *( [ { "fields": {"field3": "CF3", "model2bfiltered_ptr": dump_objects[5].pk}, "model": "tests.model2cfiltered", "pk": dump_objects[5].pk, } ] if all else [] ), ] assert dumpdata("tests.Model2BFiltered", "tests.Model2CFiltered", all=all) == expected @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize( "dumpdata", [ pytest.param(call_dumpdata, id="call_command"), pytest.param( run_dumpdata, id="manage", marks=pytest.mark.skipif( is_sqlite_in_memory() or is_oracle(), reason="Subprocess test disabled for in-memory sqlite test runs", ), ), ], ) def test_dumpdata_child_model_only(dumpdata, dump_objects): """Test dumping only a child model works correctly.""" assert dumpdata("tests.Model2C") == [ { "fields": { "field3": "C3", "model2b_ptr": dump_objects[2].pk, }, "model": "tests.model2c", "pk": dump_objects[2].pk, } ] @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize( "dumpdata", [ pytest.param(call_dumpdata, id="call_command"), pytest.param( run_dumpdata, id="manage", marks=pytest.mark.skipif( is_sqlite_in_memory() or is_oracle(), reason="Subprocess test disabled for in-memory sqlite test runs", ), ), ], ) @pytest.mark.parametrize("all", [False, True], ids=["default", "all"]) def test_dumpdata_multi_table_roundtrip(dumpdata, dump_objects, all): data = dumpdata( "tests.Model2A", "tests.Model2B", "tests.Model2C", "tests.Model2BFiltered", "tests.Model2CFiltered", all=all, ) # When dumping Model2A (the parent model), ALL instances are returned regardless of # the `all` parameter, because Model2A doesn't have a filtered manager. # The `all` parameter only affects behavior when dumping child models WITHOUT their parent. expected = [ { "fields": {"field1": "A1", "polymorphic_ctype": ["tests", "model2a"]}, "model": "tests.model2a", "pk": dump_objects[0].pk, }, { "fields": {"field1": "B1", "polymorphic_ctype": ["tests", "model2b"]}, "model": "tests.model2a", "pk": dump_objects[1].pk, }, { "fields": {"field1": "C1", "polymorphic_ctype": ["tests", "model2c"]}, "model": "tests.model2a", "pk": dump_objects[2].pk, }, { "fields": {"field1": "BF1", "polymorphic_ctype": ["tests", "model2bfiltered"]}, "model": "tests.model2a", "pk": dump_objects[3].pk, }, { "fields": {"field1": "cf1", "polymorphic_ctype": ["tests", "model2cfiltered"]}, "model": "tests.model2a", "pk": dump_objects[4].pk, }, { "fields": {"field1": "CF1", "polymorphic_ctype": ["tests", "model2cfiltered"]}, "model": "tests.model2a", "pk": dump_objects[5].pk, }, { "fields": {"field2": "B2", "model2a_ptr": dump_objects[1].pk}, "model": "tests.model2b", "pk": dump_objects[1].pk, }, { "fields": {"field2": "C2", "model2a_ptr": dump_objects[2].pk}, "model": "tests.model2b", "pk": dump_objects[2].pk, }, { "fields": {"field2": "BF2", "model2a_ptr": dump_objects[3].pk}, "model": "tests.model2b", "pk": dump_objects[3].pk, }, { "fields": {"field2": "cf2", "model2a_ptr": dump_objects[4].pk}, "model": "tests.model2b", "pk": dump_objects[4].pk, }, { "fields": {"field2": "CF2", "model2a_ptr": dump_objects[5].pk}, "model": "tests.model2b", "pk": dump_objects[5].pk, }, { "fields": {"field3": "C3", "model2b_ptr": dump_objects[2].pk}, "model": "tests.model2c", "pk": dump_objects[2].pk, }, *( [ { "fields": {"model2b_ptr": dump_objects[3].pk}, "model": "tests.model2bfiltered", "pk": dump_objects[3].pk, } ] if all else [] ), { "fields": {"model2b_ptr": dump_objects[4].pk}, "model": "tests.model2bfiltered", "pk": dump_objects[4].pk, }, *( [ { "fields": {"model2b_ptr": dump_objects[5].pk}, "model": "tests.model2bfiltered", "pk": dump_objects[5].pk, } ] if all else [] ), { "fields": {"field3": "cf3", "model2bfiltered_ptr": dump_objects[4].pk}, "model": "tests.model2cfiltered", "pk": dump_objects[4].pk, }, *( [ { "fields": {"field3": "CF3", "model2bfiltered_ptr": dump_objects[5].pk}, "model": "tests.model2cfiltered", "pk": dump_objects[5].pk, } ] if all else [] ), ] assert data == expected Model2A.objects.all().delete() with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: f.write(json.dumps(data)) fixture_file = f.name try: call_command("loaddata", fixture_file, verbosity=0) # After loaddata, all 6 objects are loaded assert Model2A.objects.count() == 6 assert Model2B.objects.count() == 5 assert Model2C.objects.count() == 1 # Filtered managers still apply, so only cf1 passes the filter assert Model2BFiltered.objects.count() == 1 assert Model2CFiltered.objects.count() == 1 model2a_objects = list(Model2A.objects.order_by("pk")) a, b, c, bf, cf_lower, cf_upper = model2a_objects assert a.__class__ == Model2A assert b.__class__ == Model2B assert c.__class__ == Model2C assert cf_lower.__class__ == Model2CFiltered if all: assert bf.__class__ == Model2BFiltered assert cf_upper.__class__ == Model2CFiltered else: # the parent class wasnt filtered so these should have been upcasted assert bf.__class__ == Model2B assert cf_upper.__class__ == Model2B assert a.field1 == "A1" assert b.field1 == "B1" assert b.field2 == "B2" assert c.field1 == "C1" assert c.field2 == "C2" assert c.field3 == "C3" assert cf_lower.field1 == "cf1" assert cf_lower.field2 == "cf2" assert cf_lower.field3 == "cf3" if all: assert bf.field1 == "BF1" assert bf.field2 == "BF2" assert cf_upper.field1 == "CF1" assert cf_upper.field2 == "CF2" assert cf_upper.field3 == "CF3" else: assert bf.field1 == "BF1" assert bf.field2 == "BF2" # cf_upper is now a Model2B, so field3 does not exist assert cf_upper.field1 == "CF1" assert cf_upper.field2 == "CF2" assert not hasattr(cf_upper, "field3") finally: # Clean up temporary file os.unlink(fixture_file) @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize( "dumpdata", [ pytest.param(call_dumpdata, id="call_command"), pytest.param( run_dumpdata, id="manage", marks=pytest.mark.skipif( is_sqlite_in_memory() or is_oracle(), reason="Subprocess test disabled for in-memory sqlite test runs", ), ), ], ) @pytest.mark.parametrize("all", [False, True], ids=["default", "all"]) def test_dumpdata_related_polymorphic_roundtrip(dumpdata, dump_objects, all): rm1 = RelatingModel.objects.create() rm2 = RelatingModel.objects.create() rm3 = RelatingModel.objects.create() rm1.many2many.add(dump_objects[0]) rm2.many2many.add(dump_objects[1], dump_objects[2]) # Add all filtered models to rm3 rm3.many2many.add(dump_objects[3], dump_objects[4], dump_objects[5]) data = dumpdata( "tests.Model2A", "tests.Model2B", "tests.Model2C", "tests.Model2BFiltered", "tests.Model2CFiltered", "tests.RelatingModel", natural_foreign=True, natural_primary=False, all=all, ) Model2A.objects.all().delete() RelatingModel.objects.all().delete() with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: f.write(json.dumps(data)) fixture_file = f.name try: call_command("loaddata", fixture_file, verbosity=0) # After loaddata, all 6 objects are loaded assert Model2A.objects.count() == 6 assert Model2B.objects.count() == 5 assert Model2C.objects.count() == 1 # Filtered managers still apply, so only cf1 passes the filter assert Model2BFiltered.objects.count() == 1 assert Model2BFiltered._base_manager.count() == (3 if all else 1) assert Model2CFiltered.objects.count() == 1 assert Model2CFiltered._base_manager.count() == (2 if all else 1) model2a_objects = Model2A.objects.order_by("pk") a, b, c, bf, cf_lower, cf_upper = model2a_objects.all() assert a.__class__ == Model2A assert b.__class__ == Model2B assert c.__class__ == Model2C assert cf_lower.__class__ == Model2CFiltered if all: assert bf.__class__ == Model2BFiltered assert cf_upper.__class__ == Model2CFiltered else: # the parent class wasnt filtered so these should have been upcasted assert bf.__class__ == Model2B assert cf_upper.__class__ == Model2B assert a.field1 == "A1" assert b.field1 == "B1" assert b.field2 == "B2" assert c.field1 == "C1" assert c.field2 == "C2" assert c.field3 == "C3" assert cf_lower.field1 == "cf1" assert cf_lower.field2 == "cf2" assert cf_lower.field3 == "cf3" if all: assert bf.field1 == "BF1" assert bf.field2 == "BF2" assert cf_upper.field1 == "CF1" assert cf_upper.field2 == "CF2" assert cf_upper.field3 == "CF3" else: assert bf.field1 == "BF1" assert bf.field2 == "BF2" # cf_upper is now a Model2B, so field3 does not exist assert cf_upper.field1 == "CF1" assert cf_upper.field2 == "CF2" assert not hasattr(cf_upper, "field3") # Verify relationships assert RelatingModel.objects.count() == 3 rm_objects = list(RelatingModel.objects.order_by("pk")) # rm1 has A assert rm_objects[0].many2many.count() == 1 assert rm_objects[0].many2many.first() == a # rm2 has B and C assert rm_objects[1].many2many.count() == 2 assert set(rm_objects[1].many2many.all()) == {b, c} # rm3 has all three filtered models assert rm_objects[2].many2many.count() == 3 rm3_related = set(rm_objects[2].many2many.all()) assert len(rm3_related) == 3 assert bf in rm3_related assert cf_lower in rm3_related assert cf_upper in rm3_related finally: # Clean up temporary file os.unlink(fixture_file) @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize( "dumpdata", [ pytest.param(call_dumpdata, id="call_command"), pytest.param( run_dumpdata, id="manage", marks=pytest.mark.skipif( is_sqlite_in_memory() or is_oracle(), reason="Subprocess test disabled for in-memory sqlite test runs", ), ), ], ) def test_dumpdata_natural_keys(dumpdata, natkey_dump_objects): data = dumpdata( "tests", natural_foreign=True, natural_primary=True, all=all, ) assert data == [ { "fields": { "content": "content 0", "polymorphic_ctype": ["tests", "natkeychild"], "slug": "slug-0", }, "model": "tests.natkeyparent", }, { "fields": { "content": "content 1", "polymorphic_ctype": ["tests", "natkeychild"], "slug": "slug-1", }, "model": "tests.natkeyparent", }, { "fields": { "content": "content 2", "polymorphic_ctype": ["tests", "natkeychild"], "slug": "slug-2", }, "model": "tests.natkeyparent", }, { "fields": { "content": "content 3", "polymorphic_ctype": ["tests", "natkeychild"], "slug": "slug-3", }, "model": "tests.natkeyparent", }, { "fields": { "content": "content 4", "polymorphic_ctype": ["tests", "natkeychild"], "slug": "slug-4", }, "model": "tests.natkeyparent", }, {"fields": {"foo": ["slug-0"], "val": 0}, "model": "tests.natkeychild"}, {"fields": {"foo": ["slug-1"], "val": 1}, "model": "tests.natkeychild"}, {"fields": {"foo": ["slug-2"], "val": 2}, "model": "tests.natkeychild"}, {"fields": {"foo": ["slug-3"], "val": 3}, "model": "tests.natkeychild"}, {"fields": {"foo": ["slug-4"], "val": 4}, "model": "tests.natkeychild"}, ] NatKeyChild.objects.all().delete() NatKeyParent.objects.all().delete() with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: f.write(json.dumps(data)) fixture_file = f.name try: call_command("loaddata", fixture_file, verbosity=0) for old, new in zip( natkey_dump_objects, NatKeyParent.objects.order_by("pk").all(), ): assert new.__class__ == old.__class__ is NatKeyChild assert new.slug == old.slug assert new.content == old.content assert new.val == old.val finally: # Clean up temporary file os.unlink(fixture_file) django-polymorphic-4.10.2/src/polymorphic/tests/test_signals.py000066400000000000000000000262571513173623500247740ustar00rootroot00000000000000from django.test import TestCase from django.db.models.signals import post_delete from django.db import connection from .models import Model2A, Model2B, Model2C, PlainA, PlainB, PlainC from django.test.utils import CaptureQueriesContext class TestSignals(TestCase): def test_first_behavior_during_post_delete_signal_1(self): """ Regression test for issue #347: first() returning None in post_delete signals. The bug occurs when: 1. An object with a post_delete signal is created first 2. Another object is created second 3. The first object is deleted 4. In the post_delete signal, first() should return the second object but returned None """ obj1 = Model2B.objects.create(field1="First", field2="B") obj2 = Model2A.objects.create(field1="Second") def log_first_with_first_method(sender, instance, **kwargs): self.assertEqual(Model2A.objects.order_by("pk").first().pk, obj1.pk) self.assertEqual(Model2A.objects.order_by("pk").last().pk, obj2.pk) self.assertEqual(Model2B.objects.count(), 0) self.assertEqual(Model2A.objects.count(), 2) post_delete.connect(log_first_with_first_method, sender=Model2B) obj1.delete() post_delete.disconnect(log_first_with_first_method, sender=Model2C) def test_first_behavior_during_post_delete_signal_2(self): """ Test that the fix works when the object with signal is created second. The bug only occurred when the signaled object was created first. """ obj1 = Model2A.objects.create(field1="Second") obj2 = Model2B.objects.create(field1="First", field2="B") def log_first_with_first_method(sender, instance, **kwargs): self.assertEqual(Model2A.objects.order_by("pk").first(), obj1) self.assertEqual(Model2B.objects.count(), 0) post_delete.connect(log_first_with_first_method, sender=Model2B) obj2.delete() post_delete.disconnect(log_first_with_first_method, sender=Model2C) def test_getitem_behavior_during_post_delete_signal(self): """ Regression test for issue #347: [0] returning None in post_delete signals. """ obj1 = Model2C.objects.create(field1="First", field2="C", field3="C3") obj2 = Model2A.objects.create(field1="Second") def log_first_with_brackets(sender, instance, **kwargs): try: self.assertEqual(Model2A.objects.order_by("pk")[0].pk, obj1.pk) self.assertEqual(Model2A.objects.order_by("pk")[1].pk, obj2.pk) self.assertEqual(Model2C.objects.count(), 0) self.assertEqual(Model2A.objects.count(), 2) except IndexError: self.fail("Queryset __getitem__[0] returned IndexError unexpectedly") post_delete.connect(log_first_with_brackets, sender=Model2C) obj1.delete() # trigger signal handling post_delete.disconnect(log_first_with_brackets, sender=Model2C) def test_normal_getitem_behavior_during_post_delete_signal(self): """ Illustrate standard Django multi-table inheritance during this test. """ obj1 = PlainC.objects.create(field1="First", field2="C", field3="C3") obj2 = PlainA.objects.create(field1="Second") def log_first_with_brackets(sender, instance, **kwargs): try: self.assertEqual(PlainA.objects.order_by("pk")[0].pk, obj1.pk) self.assertEqual(PlainA.objects.order_by("pk")[1], obj2) self.assertEqual(PlainC.objects.count(), 0) except IndexError: self.fail("Queryset __getitem__[0] returned IndexError unexpectedly") post_delete.connect(log_first_with_brackets, sender=PlainC) obj1.delete() # trigger signal handling post_delete.disconnect(log_first_with_brackets, sender=PlainC) def test_queryset_first_returns_remaining_object_in_post_delete_signal(self): """ Regression test for issue #347: first() returning None in post_delete signals. The bug occurs when: 1. An object with a post_delete signal is created first 2. Another object is created second 3. The first object is deleted 4. In the post_delete signal, first() should return the second object but returned None """ obj1 = Model2B.objects.create(field1="First", field2="B") obj1_base = Model2A.objects.non_polymorphic().get(pk=obj1.pk) obj2 = Model2A.objects.create(field1="Second") def check_2b_delete(sender, instance, **kwargs): assert Model2B.objects.count() == 0 assert Model2A.objects.order_by("pk").first() == obj1_base assert Model2A.objects.count() == 2 def check_2a_delete(sender, instance, **kwargs): assert Model2A.objects.order_by("pk").first() == obj2 assert Model2A.objects.count() == 1 try: post_delete.connect(check_2b_delete, sender=Model2B) post_delete.connect(check_2a_delete, sender=Model2A) # This will trigger the post_delete signal, first for the deletion of the # 2b row - then for the deletion of the 2a row - at each signal the database # should be consistent with the staged deletion of rows most derived first # order. obj1.delete() finally: post_delete.disconnect(check_2b_delete, sender=Model2B) post_delete.disconnect(check_2a_delete, sender=Model2A) def test_queryset_getitem_returns_remaining_object_in_post_delete_signal(self): """ Regression test for issue #347: [0] returning None in post_delete signals. """ obj1 = Model2C.objects.create(field1="First", field2="C", field3="C3") obj1_baseb = Model2B.objects.non_polymorphic().get(pk=obj1.pk) obj1_basea = Model2A.objects.non_polymorphic().get(pk=obj1.pk) obj2 = Model2A.objects.create(field1="Second") def check_2c_delete(sender, instance, **kwargs): assert Model2C.objects.count() == 0 assert Model2B.objects.count() == 1 assert Model2A.objects.count() == 2 assert Model2B.objects.order_by("pk")[0] == obj1_baseb assert Model2A.objects.order_by("pk")[0] == obj1_baseb def check_2b_delete(sender, instance, **kwargs): assert Model2C.objects.count() == 0 assert Model2B.objects.count() == 0 assert Model2A.objects.count() == 2 assert Model2A.objects.order_by("pk")[0] == obj1_basea def check_2a_delete(sender, instance, **kwargs): assert Model2C.objects.count() == 0 assert Model2B.objects.count() == 0 assert Model2A.objects.count() == 1 assert Model2A.objects.order_by("pk")[0] == obj2 # Connect signal try: post_delete.connect(check_2c_delete, sender=Model2C) post_delete.connect(check_2b_delete, sender=Model2B) post_delete.connect(check_2a_delete, sender=Model2A) obj1.delete() finally: post_delete.disconnect(check_2c_delete, sender=Model2C) post_delete.disconnect(check_2b_delete, sender=Model2B) post_delete.disconnect(check_2a_delete, sender=Model2A) def test_queryset_first_works_when_deleted_object_created_second(self): """ Test that the fix works when the object with signal is created second. The bug only occurred when the signaled object was created first. """ obj1 = Model2A.objects.create(field1="Second") obj2 = Model2B.objects.create(field1="First", field2="B") obj2_base = Model2A.objects.non_polymorphic().get(pk=obj2.pk) def check_2b_delete(sender, instance, **kwargs): assert Model2B.objects.count() == 0 assert Model2A.objects.order_by("pk").first() == obj1 assert Model2A.objects.last() == obj2_base assert Model2A.objects.count() == 2 def check_2a_delete(sender, instance, **kwargs): assert Model2A.objects.order_by("pk").first() == obj1 assert Model2A.objects.count() == 1 try: post_delete.connect(check_2a_delete, sender=Model2A) post_delete.connect(check_2b_delete, sender=Model2B) # This will trigger the post_delete signal, first for the deletion of the # 2b row - then for the deletion of the 2a row - at each signal the database # should be consistent with the staged deletion of rows most derived first # order. obj2.delete() finally: post_delete.disconnect(check_2b_delete, sender=Model2B) post_delete.disconnect(check_2a_delete, sender=Model2A) def test_besteffort_iteration_avoids_nplusone(self): """ Test that our best effort iteration avoids n+1 queries when n objects have stale content type pointers. """ for i in range(100): Model2C.objects.create( field1=f"Model2C_{i}", field2="Model2C_{i}", field3="Model2C_{i}" ) def check_2c_delete(sender, instance, **kwargs): assert Model2C.objects.count() == 0 assert Model2B.objects.count() == 100 assert Model2A.objects.count() == 100 from django.db.backends.utils import CursorWrapper for _ in Model2B.objects.all(): pass # Evaluating the queryset with CaptureQueriesContext(connection) as all_2b: for _ in Model2B.objects.all(): pass # Evaluating the queryset assert len(all_2b.captured_queries) <= 3 with CaptureQueriesContext(connection) as all_2a: for _ in Model2A.objects.all(): pass # Evaluating the queryset assert len(all_2a.captured_queries) <= 4 def check_2b_delete(sender, instance, **kwargs): assert Model2C.objects.count() == 0 assert Model2B.objects.count() == 0 assert Model2A.objects.count() == 100 with CaptureQueriesContext(connection) as all_2a: for _ in Model2A.objects.all(): pass # Evaluating the queryset assert len(all_2a.captured_queries) <= 3 def check_2a_delete(sender, instance, **kwargs): assert Model2C.objects.count() == 0 assert Model2B.objects.count() == 0 assert Model2A.objects.count() == 0 with CaptureQueriesContext(connection) as all_2a: for _ in Model2A.objects.all(): pass # Evaluating the queryset assert len(all_2a.captured_queries) <= 3 try: post_delete.connect(check_2b_delete, sender=Model2B) post_delete.connect(check_2a_delete, sender=Model2A) post_delete.connect(check_2c_delete, sender=Model2C) Model2C.objects.all().delete() finally: post_delete.disconnect(check_2b_delete, sender=Model2B) post_delete.disconnect(check_2a_delete, sender=Model2A) post_delete.disconnect(check_2c_delete, sender=Model2C) django-polymorphic-4.10.2/src/polymorphic/tests/test_utils.py000066400000000000000000000605721513173623500244720ustar00rootroot00000000000000import pytest from django.test import TransactionTestCase from django.contrib.contenttypes.models import ContentType from polymorphic.models import PolymorphicModel, PolymorphicTypeUndefined from polymorphic.tests.models import ( Enhance_Base, Enhance_Inherit, Model2A, Model2B, Model2C, Model2D, ) from polymorphic.utils import ( get_base_polymorphic_model, reset_polymorphic_ctype, sort_by_subclass, route_to_ancestor, concrete_descendants, ) class UtilsTests(TransactionTestCase): def test_sort_by_subclass(self): assert sort_by_subclass(Model2D, Model2B, Model2D, Model2A, Model2C) == [ Model2A, Model2B, Model2C, Model2D, Model2D, ] def test_reset_polymorphic_ctype(self): """ Test the the polymorphic_ctype_id can be restored. """ Model2A.objects.create(field1="A1") Model2D.objects.create(field1="A1", field2="B2", field3="C3", field4="D4") Model2B.objects.create(field1="A1", field2="B2") Model2B.objects.create(field1="A1", field2="B2") Model2A.objects.all().update(polymorphic_ctype_id=None) with pytest.raises(PolymorphicTypeUndefined): list(Model2A.objects.all()) reset_polymorphic_ctype(Model2D, Model2B, Model2D, Model2A, Model2C) self.assertQuerySetEqual( Model2A.objects.order_by("pk"), [Model2A, Model2D, Model2B, Model2B], transform=lambda o: o.__class__, ) def test_get_base_polymorphic_model(self): """ Test that finding the base polymorphic model works. """ # Finds the base from every level (including lowest) assert get_base_polymorphic_model(Model2D) is Model2A assert get_base_polymorphic_model(Model2C) is Model2A assert get_base_polymorphic_model(Model2B) is Model2A assert get_base_polymorphic_model(Model2A) is Model2A # Properly handles multiple inheritance assert get_base_polymorphic_model(Enhance_Inherit) is Enhance_Base # Ignores PolymorphicModel itself. assert get_base_polymorphic_model(PolymorphicModel) is None def test_get_base_polymorphic_model_skip_abstract(self): """ Skipping abstract models that can't be used for querying. """ class A(PolymorphicModel): class Meta: abstract = True class B(A): pass class C(B): pass assert get_base_polymorphic_model(A) is None assert get_base_polymorphic_model(B) is B assert get_base_polymorphic_model(C) is B assert get_base_polymorphic_model(C, allow_abstract=True) is A def test_concrete_descendants(self): """ Test that finding concrete descendants works. """ from .models import ( Model2A, Model2B, Model2C, Model2D, ModelWithMyManager, ModelWithMyManagerNoDefault, ModelWithMyManagerDefault, ModelWithMyManager2, Base, ModelX, ModelY, BlogBase, BlogA, BlogB, RelationBase, RelationA, RelationB, RelationBC, ProxyBase, NonProxyChild, ProxiedBase, ProxyModelA, ProxyModelB, ProxyModelBase, CustomPkBase, CustomPkInherit, MultiTableBase, MultiTableDerived, FKTestChild, Model2BFiltered, Model2CFiltered, Model2CNamedDefault, Model2CNamedManagers, ) # Model2A hierarchy (with manager variants) assert concrete_descendants(Model2A) == [ Model2B, Model2C, Model2D, Model2BFiltered, Model2CFiltered, Model2CNamedManagers, Model2CNamedDefault, ModelWithMyManager, ModelWithMyManagerNoDefault, ModelWithMyManagerDefault, ModelWithMyManager2, ] assert concrete_descendants(Model2B) == [ Model2C, Model2D, Model2BFiltered, Model2CFiltered, Model2CNamedManagers, Model2CNamedDefault, ] assert concrete_descendants(Model2C) == [Model2D] assert len(concrete_descendants(Model2D)) == 0 # ModelWithMyManager variants (no further descendants) assert len(concrete_descendants(ModelWithMyManager)) == 0 assert len(concrete_descendants(ModelWithMyManagerNoDefault)) == 0 assert len(concrete_descendants(ModelWithMyManagerDefault)) == 0 assert len(concrete_descendants(ModelWithMyManager2)) == 0 # Base hierarchy (tree order: ModelX defined before ModelY) assert concrete_descendants(Base) == [ModelX, ModelY, FKTestChild] assert len(concrete_descendants(ModelX)) == 0 assert len(concrete_descendants(ModelY)) == 0 # BlogBase hierarchy (tree order: BlogA defined before BlogB) assert concrete_descendants(BlogBase) == [BlogA, BlogB] assert len(concrete_descendants(BlogA)) == 0 assert len(concrete_descendants(BlogB)) == 0 # RelationBase hierarchy (tree order: RelationA before RelationB, RelationBC is child of RelationB) assert concrete_descendants(RelationBase) == [ RelationA, RelationB, RelationBC, ] assert len(concrete_descendants(RelationA)) == 0 assert concrete_descendants(RelationB) == [RelationBC] assert len(concrete_descendants(RelationBC)) == 0 # ProxyBase hierarchy (ProxyChild is proxy, so excluded) assert concrete_descendants(ProxyBase) == [NonProxyChild] assert len(concrete_descendants(NonProxyChild)) == 0 # ProxiedBase hierarchy (tree order: ProxyModelA defined before ProxyModelB) # ProxyModelBase is proxy, but has concrete children assert concrete_descendants(ProxiedBase) == [ProxyModelA, ProxyModelB] # ProxyModelBase is proxy but should still return its concrete descendants assert concrete_descendants(ProxyModelBase) == [ProxyModelA, ProxyModelB] assert len(concrete_descendants(ProxyModelA)) == 0 assert len(concrete_descendants(ProxyModelB)) == 0 # CustomPkBase hierarchy assert concrete_descendants(CustomPkBase) == [CustomPkInherit] assert len(concrete_descendants(CustomPkInherit)) == 0 # MultiTableBase hierarchy assert concrete_descendants(MultiTableBase) == [MultiTableDerived] assert len(concrete_descendants(MultiTableDerived)) == 0 # Enhance_Base hierarchy assert concrete_descendants(Enhance_Base) == [Enhance_Inherit] assert len(concrete_descendants(Enhance_Inherit)) == 0 def test_route_to_ancestor(self): """ Test that finding routes to ancestors works correctly. """ from .models import ( Model2A, Model2B, Model2C, Model2D, Base, ModelX, ModelY, BlogBase, BlogA, RelationBase, RelationA, RelationB, RelationBC, Enhance_Base, Enhance_Inherit, PurpleHeadDuck, Duck, ) # Test direct parent (one hop) route = route_to_ancestor(Model2B, Model2A) assert len(route) == 1 assert route[0].model == Model2A assert route[0].link.name == "model2a_ptr" # Test grandparent (two hops) route = route_to_ancestor(Model2C, Model2A) assert len(route) == 2 assert route[0].model == Model2B assert route[0].link.name == "model2b_ptr" assert route[1].model == Model2A assert route[1].link.name == "model2a_ptr" # Test great-grandparent (three hops) route = route_to_ancestor(Model2D, Model2A) assert len(route) == 3 assert route[0].model == Model2C assert route[0].link.name == "model2c_ptr" assert route[1].model == Model2B assert route[1].link.name == "model2b_ptr" assert route[2].model == Model2A assert route[2].link.name == "model2a_ptr" # Test intermediate ancestor (skip one level) route = route_to_ancestor(Model2D, Model2B) assert len(route) == 2 assert route[0].model == Model2C assert route[0].link.name == "model2c_ptr" assert route[1].model == Model2B assert route[1].link.name == "model2b_ptr" route = route_to_ancestor(Model2D, Model2C) assert len(route) == 1 assert route[0].model == Model2C assert route[0].link.name == "model2c_ptr" # Test self (should return empty) assert route_to_ancestor(Model2A, Model2A) == [] assert route_to_ancestor(Model2B, Model2B) == [] assert route_to_ancestor(Model2D, Model2D) == [] # Test non-ancestor (should return empty) assert route_to_ancestor(Model2A, Model2B) == [] assert route_to_ancestor(Model2B, Model2C) == [] assert route_to_ancestor(Model2C, Model2D) == [] # Test unrelated models (should return empty) assert route_to_ancestor(ModelX, Model2A) == [] assert route_to_ancestor(Model2A, ModelX) == [] assert route_to_ancestor(BlogA, RelationA) == [] # Test different hierarchy - Base -> ModelX route = route_to_ancestor(ModelX, Base) assert len(route) == 1 assert route[0].model == Base assert route[0].link.name == "base_ptr" # Test different hierarchy - Base -> ModelY route = route_to_ancestor(ModelY, Base) assert len(route) == 1 assert route[0].model == Base assert route[0].link.name == "base_ptr" # Test BlogBase hierarchy route = route_to_ancestor(BlogA, BlogBase) assert len(route) == 1 assert route[0].model == BlogBase assert route[0].link.name == "blogbase_ptr" # Test multi-level RelationBase hierarchy route = route_to_ancestor(RelationBC, RelationBase) assert len(route) == 2 assert route[0].model == RelationB assert route[0].link.name == "relationb_ptr" assert route[1].model == RelationBase assert route[1].link.name == "relationbase_ptr" route = route_to_ancestor(RelationBC, RelationB) assert len(route) == 1 assert route[0].model == RelationB assert route[0].link.name == "relationb_ptr" route = route_to_ancestor(RelationB, RelationBase) assert len(route) == 1 assert route[0].model == RelationBase assert route[0].link.name == "relationbase_ptr" # Test multiple inheritance - Enhance_Inherit route = route_to_ancestor(Enhance_Inherit, Enhance_Base) assert len(route) == 1 assert route[0].model == Enhance_Base assert route[0].link.name == "enhance_base_ptr" route = route_to_ancestor(PurpleHeadDuck, Duck) assert len(route) == 1 assert route[0].model == Duck assert route[0].link.name == "duck_ptr" class PrepareForCopyTests(TransactionTestCase): def test_copy_polymorphic_objects(self): """ Test copying polymorphic objects with multi-level inheritance. https://github.com/jazzband/django-polymorphic/issues/414 This test verifies that the prepare_for_copy() method correctly handles copying objects with 2, 3, and 4 levels of inheritance. """ from polymorphic.utils import prepare_for_copy from polymorphic.tests.models import Model2B, Model2C, Model2D # Create original objects obj_b = Model2B.objects.create(field1="B1", field2="B2") obj_c = Model2C.objects.create(field1="C1", field2="C2", field3="C3") obj_d = Model2D.objects.create(field1="D1", field2="D2", field3="D3", field4="D4") original_b_pk = obj_b.pk original_c_pk = obj_c.pk original_d_pk = obj_d.pk # Note: Model2C and Model2D inherit from Model2B, so they're also counted in Model2B.objects # Initial counts: Model2B=3 (obj_b, obj_c, obj_d), Model2C=2 (obj_c, obj_d), Model2D=1 (obj_d) assert Model2B.objects.count() == 3 # obj_b + obj_c + obj_d assert Model2C.objects.count() == 2 # obj_c + obj_d assert Model2D.objects.count() == 1 # obj_d # Test 1: Copy Model2B (2-level inheritance) using new method copy_b = Model2B.objects.get(pk=obj_b.pk) copy_b.field1 = "B1_copy" prepare_for_copy(copy_b) copy_b.save() # Verify the copy assert copy_b.pk != original_b_pk assert copy_b.field1 == "B1_copy" assert copy_b.field2 == "B2" assert Model2B.objects.filter(pk=original_b_pk).exists() assert Model2B.objects.filter(pk=copy_b.pk).exists() # Now we have: obj_b, copy_b, obj_c, obj_d assert Model2B.objects.count() == 4 # Test 2: Copy Model2C (3-level inheritance) using new method # This is the main issue from #414 - previously failed copy_c = Model2C.objects.get(pk=obj_c.pk) copy_c.field1 = "C1_copy" prepare_for_copy(copy_c) copy_c.save() # Verify the copy assert copy_c.pk != original_c_pk assert copy_c.field1 == "C1_copy" assert copy_c.field2 == "C2" assert copy_c.field3 == "C3" assert Model2C.objects.filter(pk=original_c_pk).exists() assert Model2C.objects.filter(pk=copy_c.pk).exists() # Now we have Model2C: obj_c, copy_c, obj_d assert Model2C.objects.count() == 3 # And Model2B: obj_b, copy_b, obj_c, copy_c, obj_d assert Model2B.objects.count() == 5 # Test 3: Copy Model2D (4-level inheritance) using new method copy_d = Model2D.objects.get(pk=obj_d.pk) copy_d.field1 = "D1_copy" prepare_for_copy(copy_d) copy_d.save() # Verify the copy assert copy_d.pk != original_d_pk assert copy_d.field1 == "D1_copy" assert copy_d.field2 == "D2" assert copy_d.field3 == "D3" assert copy_d.field4 == "D4" assert Model2D.objects.filter(pk=original_d_pk).exists() assert Model2D.objects.filter(pk=copy_d.pk).exists() # Now we have Model2D: obj_d, copy_d assert Model2D.objects.count() == 2 # Model2C: obj_c, copy_c, obj_d, copy_d assert Model2C.objects.count() == 4 # Model2B: obj_b, copy_b, obj_c, copy_c, obj_d, copy_d assert Model2B.objects.count() == 6 # Test 4: Verify old manual method still works for 2-level inheritance manual_copy_b = Model2B.objects.get(pk=obj_b.pk) manual_copy_b.field1 = "B1_manual" manual_copy_b.pk = None manual_copy_b.id = None manual_copy_b.save() assert manual_copy_b.pk not in [original_b_pk, copy_b.pk] assert manual_copy_b.field1 == "B1_manual" assert manual_copy_b.field2 == "B2" # Now we have Model2B: obj_b, copy_b, manual_copy_b, obj_c, copy_c, obj_d, copy_d assert Model2B.objects.count() == 7 # Test 5: Verify that polymorphic queries work correctly on copied objects all_b = list(Model2B.objects.all().order_by("pk")) assert len(all_b) == 7 # obj_b, copy_b, manual_copy_b, obj_c, copy_c, obj_d, copy_d # Check that each is the correct type b_only = [obj for obj in all_b if type(obj).__name__ == "Model2B"] assert len(b_only) == 3 # obj_b, copy_b, manual_copy_b all_c = list(Model2C.objects.all().order_by("pk")) assert len(all_c) == 4 # obj_c, copy_c, obj_d, copy_d c_only = [obj for obj in all_c if type(obj).__name__ == "Model2C"] assert len(c_only) == 2 # obj_c, copy_c all_d = list(Model2D.objects.all().order_by("pk")) assert len(all_d) == 2 # obj_d, copy_d assert all(type(obj).__name__ == "Model2D" for obj in all_d) # Test 6: Verify polymorphic_ctype is set correctly on copied objects assert copy_b.polymorphic_ctype == ContentType.objects.get_for_model(Model2B) assert copy_c.polymorphic_ctype == ContentType.objects.get_for_model(Model2C) assert copy_d.polymorphic_ctype == ContentType.objects.get_for_model(Model2D) def test_prepare_for_copy_edge_cases(self): """ Test edge cases in prepare_for_copy() method. The method should only reset parent link fields within the inheritance chain, not regular OneToOneFields to external models. """ from polymorphic.utils import prepare_for_copy from polymorphic.tests.models import One2OneRelatingModel, Model2A, Model2C # Clean up One2OneRelatingModel.objects.all().delete() Model2A.objects.all().delete() # Create a Model2A instance to link to related_obj = Model2A.objects.create(field1="Related") # Create a One2OneRelatingModel with a regular OneToOneField obj_with_o2o = One2OneRelatingModel.objects.create(field1="Test1", one2one=related_obj) original_pk = obj_with_o2o.pk original_one2one_id = obj_with_o2o.one2one_id # Now copy the object copy_obj = One2OneRelatingModel.objects.get(pk=obj_with_o2o.pk) copy_obj.field1 = "Test1_copy" prepare_for_copy(copy_obj) # Verify that pk and polymorphic_ctype_id are reset assert copy_obj.pk is None assert copy_obj.id is None assert copy_obj.polymorphic_ctype_id is None assert copy_obj.one2one_id == original_one2one_id, ( "Regular OneToOneField should NOT be reset by prepare_for_copy()" ) assert copy_obj.one2one == related_obj # To save the copy, we need to create a new related object or clear the field # because the OneToOneField constraint prevents duplicate links new_related = Model2A.objects.create(field1="Related2") copy_obj.one2one = new_related copy_obj.save() # Verify the copy was created successfully assert copy_obj.pk != original_pk assert copy_obj.field1 == "Test1_copy" assert One2OneRelatingModel.objects.count() == 2 # Create a 3-level inheritance object obj_c = Model2C.objects.create(field1="C1", field2="C2", field3="C3") original_c_pk = obj_c.pk # Model2C -> Model2B -> Model2A # Model2C should have model2b_ptr (parent link) copy_c = Model2C.objects.get(pk=obj_c.pk) copy_c.field1 = "C1_copy" # Before reset, the parent link fields should have values assert copy_c.model2b_ptr_id is not None prepare_for_copy(copy_c) # After reset, parent link fields should be None assert copy_c.pk is None assert copy_c.id is None assert copy_c.polymorphic_ctype_id is None assert copy_c.model2b_ptr_id is None # Parent link should be reset copy_c.save() # Verify the copy was created assert copy_c.pk != original_c_pk assert copy_c.field1 == "C1_copy" assert copy_c.field2 == "C2" assert copy_c.field3 == "C3" # Create another object with OneToOneField to Model2A related_obj2 = Model2A.objects.create(field1="Related3") obj_with_o2o2 = One2OneRelatingModel.objects.create(field1="Test2", one2one=related_obj2) copy_obj2 = One2OneRelatingModel.objects.get(pk=obj_with_o2o2.pk) copy_obj2.field1 = "Test2_copy" # Store the one2one_id before reset one2one_id_before = copy_obj2.one2one_id prepare_for_copy(copy_obj2) # The one2one field should NOT be reset because Model2A is not in the inheritance tree # of One2OneRelatingModel assert copy_obj2.one2one_id == one2one_id_before, ( "OneToOneField to model outside inheritance tree should NOT be reset" ) # Create a new related object for the copy new_related2 = Model2A.objects.create(field1="Related4") copy_obj2.one2one = new_related2 copy_obj2.save() assert One2OneRelatingModel.objects.count() == 4 # original, copy, obj2, copy2 def test_prepare_for_copy_upcast(self): from polymorphic.utils import prepare_for_copy from polymorphic.tests.models import Model2B, Model2C c = Model2C.objects.create(field1="C1", field2="C2", field3="C3") c_as_b = Model2B.objects.non_polymorphic().get(pk=c.pk) # copy c as a b instance prepare_for_copy(c_as_b) c_as_b.save() assert Model2B.objects.count() == 2 assert Model2C.objects.count() == 1 c_as_b.refresh_from_db() assert c_as_b.field1 == "C1" assert c_as_b.field2 == "C2" assert not hasattr(c_as_b, "field3") assert c_as_b.polymorphic_ctype == ContentType.objects.get_for_model(Model2B) assert c_as_b.pk != c.pk def test_prepare_for_copy_plain(self): """ Test that prepare_for_copy works on non-polymorphic (plain) multi-table models. """ from polymorphic.utils import prepare_for_copy from polymorphic.tests.models import PlainC plain_c = PlainC.objects.create(field1="PC1", field2="PC2", field3="PC3") prepare_for_copy(plain_c) plain_c.save() plain_c.refresh_from_db() assert PlainC.objects.count() == 2 assert PlainC.objects.filter(field1="PC1").count() == 2 assert PlainC.objects.order_by("pk").last() == plain_c def test_copy_with_abstract_base(self): """ Test copying polymorphic objects with an abstract base class. """ from polymorphic.utils import prepare_for_copy from polymorphic.tests.models import RelationBase, RelationA, RelationB, RelationBC obase = RelationBase.objects.create(field_base="base") oa = RelationA.objects.create(field_base="A1", field_a="A2", fk=obase) ob = RelationB.objects.create(field_base="B1", field_b="B2", fk=oa) oc = RelationBC.objects.create(field_base="C1", field_b="C2", field_c="C3", fk=oa) oc.m2m.add(oa) oc.m2m.add(ob) assert set(oc.m2m.all()) == {oa, ob} prepare_for_copy(oc) oc.save() assert oc.m2m.count() == 0 # M2M should not be copied assert RelationBC.objects.count() == 2 assert oc.field_base == "C1" assert oc.field_b == "C2" assert oc.field_c == "C3" assert oc.fk == oa # FK to parent should remain unchanged def test_copy_with_proxies(self): """ Test that copying walks up proxy model chains correctly. """ from polymorphic.utils import prepare_for_copy from polymorphic.tests.models import Duck, PurpleHeadDuck daffy1 = PurpleHeadDuck.objects.create(name="daffy") assert Duck.objects.count() == 1 daffy2 = PurpleHeadDuck.objects.get(pk=daffy1.pk) prepare_for_copy(daffy2) daffy2.save() daffy2.refresh_from_db() assert daffy2.pk != daffy1.pk assert Duck.objects.count() == 2 assert PurpleHeadDuck.objects.count() == 2 assert Duck.objects.filter(name="daffy").count() == 2 daffy3 = Duck.objects.non_polymorphic().last() prepare_for_copy(daffy3) daffy3.save() assert Duck.objects.count() == 3 assert PurpleHeadDuck.objects.count() == 2 assert Duck.objects.filter(name="daffy").count() == 3 assert set(Duck.objects.all()) == {daffy1, daffy2, daffy3} def test_model_registration_and_utils_caches(self): """ Test that a polymorphic model that is not registered with Django does not appear in our subclass trees. """ from django.apps import apps from ..utils import concrete_descendants # warm up the cache assert concrete_descendants(Model2C) class UnregisteredModel(Model2C): pass # dynamic registration works (model meta class invalidates the\ # concrete_descendants cache) assert UnregisteredModel in concrete_descendants(Model2C) # this is a weird thing to do - we just do it for coverage apps.get_app_config(UnregisteredModel._meta.app_label).models.pop( UnregisteredModel._meta.model_name ) concrete_descendants.cache_clear() assert UnregisteredModel not in concrete_descendants(Model2C) django-polymorphic-4.10.2/src/polymorphic/tests/urls.py000066400000000000000000000014501513173623500232460ustar00rootroot00000000000000from django.contrib import admin from django.urls import path, include from .models import Model2C urlpatterns = [ path("admin/", admin.site.urls), path("examples/views", include("polymorphic.tests.examples.views.urls")), ] try: import extra_views # noqa: F401 urlpatterns.append( path( "examples/integrations/extra_views/", include( "polymorphic.tests.examples.integrations.extra_views.urls", namespace="extra_views" ), ) ) except ImportError: pass try: import rest_framework # noqa: F401 urlpatterns.append( path( "examples/integrations/drf/", include("polymorphic.tests.examples.integrations.drf.urls", namespace="drf"), ) ) except ImportError: pass django-polymorphic-4.10.2/src/polymorphic/tests/utils.py000066400000000000000000000175251513173623500234330ustar00rootroot00000000000000import os import shutil import re from pathlib import Path from functools import lru_cache from django.core.management import call_command from django_test_migrations.migrator import Migrator from django.contrib.auth import get_user_model from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.db import connections from django.urls import reverse from django.apps import apps from playwright.sync_api import sync_playwright, expect from polymorphic import tests from polymorphic.tests.models import Model2C DSN_RE = re.compile(r"^(?P[^:/]+)(:(?P\d+))?/(?P.+)$") @lru_cache() def is_sqlite_in_memory(db_name: str = "default") -> bool: from django.conf import settings return ( settings.DATABASES[db_name]["ENGINE"] == "django.db.backends.sqlite3" and settings.DATABASES[db_name]["NAME"] == ":memory:" ) @lru_cache() def is_oracle(db_name: str = "default") -> bool: from django.conf import settings return settings.DATABASES[db_name]["ENGINE"] == "django.db.backends.oracle" def get_subprocess_test_db_env(db_name: str = "default") -> dict[str, str]: """ If you need to run a test in a subprocess that accesses the active test database (e.g. to call management commands), you need to set up the environment variables so that the subprocess can connect to the correct test database. This function returns a copy of os.environ with the necessary variables set. """ env = os.environ.copy() db = connections[db_name].settings_dict # this is where django's renaming of test databases gets very annoying - we need # to make sure our subprocess invocation uses the test database - which it wont # do by default because it thinks we aren't in test mode. if is_oracle(db_name): dsn = db["NAME"] m = DSN_RE.match(dsn) if not m: raise AssertionError( f"Can't parse Oracle DSN from NAME={dsn!r}. " "Expected format like 'host:1521/service' or 'host/service'." ) host = m.group("host") port = m.group("port") or "1521" service = m.group("service") env["ORACLE_DATABASES"] = service env["ORACLE_USER"] = db["USER"] env["ORACLE_PASSWORD"] = db["PASSWORD"] # Only set non-empty values env["ORACLE_HOST"] = host env["ORACLE_PORTS"] = port else: env["PYTEST_DB_NAME"] = db["NAME"] return env class GeneratedMigrationsPerClassMixin: """ Generates migrations at class setup, applies them, and rolls them back at teardown. Configure: - apps_to_migrate = ["my_app", ...] - database = "default" (optional) """ apps_to_migrate: list[str] = [] database: str = "default" settings: str = os.environ.get("DJANGO_SETTINGS_MODULE", "polymorphic.tests.settings") @classmethod def setUpClass(cls): super().setUpClass() if not cls.apps_to_migrate: raise RuntimeError("Set apps_to_migrate = ['your_app', ...]") for app_label in cls.apps_to_migrate: call_command( "makemigrations", app_label, interactive=False, verbosity=0, ) # 2) Apply all migrations (up to latest) using django-test-migrations cls.migrator = Migrator(database=cls.database) cls._applied_states = {} for app_label in cls.apps_to_migrate: latest = cls._find_latest_migration_name(app_label) # apply_initial_migration applies all migrations up to and including `latest` cls._applied_states[app_label] = cls.migrator.apply_initial_migration( (app_label, latest) ) @classmethod def tearDownClass(cls): try: # Roll everything back / cleanup: if hasattr(cls, "migrator"): cls.migrator.reset() finally: # remove files for app_label in cls.apps_to_migrate: app_config = apps.get_app_config(app_label) # app *label* mig_dir = Path(app_config.path) / "migrations" for mig_file in mig_dir.glob("*.py"): if mig_file.name != "__init__.py" and mig_file.name[0:4].isdigit(): os.remove(mig_file) # also remove __pycache__ if exists pycache_dir = mig_dir / "__pycache__" if pycache_dir.exists() and pycache_dir.is_dir(): shutil.rmtree(pycache_dir) super().tearDownClass() @classmethod def _find_latest_migration_name(cls, app_label: str) -> str: """ Returns "000X_..." latest migration filename (without .py). """ app_config = apps.get_app_config(app_label) # app *label* mig_dir = Path(app_config.path) / "migrations" candidates = sorted( p for p in mig_dir.glob("*.py") if p.name != "__init__.py" and p.name[0:4].isdigit() ) if not candidates: raise RuntimeError(f"No migrations generated for {app_label}") return candidates[-1].stem class _GenericUITest(StaticLiveServerTestCase): """Generic admin form test using Playwright.""" HEADLESS = tests.HEADLESS admin_username = "admin" admin_password = "password" admin = None def admin_url(self): return f"{self.live_server_url}{reverse('admin:index')}" def add_url(self, model): path = reverse(f"admin:{model._meta.label_lower.replace('.', '_')}_add") return f"{self.live_server_url}{path}" def change_url(self, model, id): path = reverse( f"admin:{model._meta.label_lower.replace('.', '_')}_change", args=[id], ) return f"{self.live_server_url}{path}" def list_url(self, model): path = reverse(f"admin:{model._meta.label_lower.replace('.', '_')}_changelist") return f"{self.live_server_url}{path}" def get_object_ids(self, model): self.page.goto(self.list_url(model)) return self.page.eval_on_selector_all( "input[name='_selected_action']", "elements => elements.map(e => e.value)" ) @classmethod def setUpClass(cls): """Set up the test class with a live server and Playwright instance.""" os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "1" super().setUpClass() try: cls.playwright = sync_playwright().start() cls.browser = cls.playwright.chromium.launch(headless=cls.HEADLESS) except Exception as e: if "asyncio loop" in str(e) or "executable" in str(e).lower(): raise RuntimeError( "Playwright failed to start. This often happens if browser drivers are missing. " "Please run 'just install-playwright' to install them." ) from e raise @classmethod def tearDownClass(cls): """Clean up Playwright instance after tests.""" cls.browser.close() cls.playwright.stop() super().tearDownClass() del os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] def setUp(self): """Create an admin user before running tests.""" self.admin = get_user_model().objects.create_superuser( username=self.admin_username, email="admin@example.com", password=self.admin_password ) self.page = self.browser.new_page() # Log in to the Django admin self.page.goto(f"{self.live_server_url}/admin/login/") self.page.fill("input[name='username']", self.admin_username) self.page.fill("input[name='password']", self.admin_password) self.page.click("input[type='submit']") # Ensure login is successful expect(self.page).to_have_url(f"{self.live_server_url}/admin/") def tearDown(self): if self.page: self.page.close() django-polymorphic-4.10.2/src/polymorphic/utils.py000066400000000000000000000227201513173623500222620ustar00rootroot00000000000000from collections import defaultdict from dataclasses import dataclass from functools import lru_cache from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldError from django.db import DEFAULT_DB_ALIAS, models from django.db.models import Q, Subquery @dataclass(frozen=True) class ParentLinkInfo: """ Information about a parent table link in a polymorphic model. """ model: models.Model link: models.Field def reset_polymorphic_ctype(*models, **filters): """ Set the polymorphic content-type ID field to the proper model Sort the ``*models`` from base class to descending class, to make sure the content types are properly assigned. Add ``ignore_existing=True`` to skip models which already have a polymorphic content type. """ using = filters.pop("using", DEFAULT_DB_ALIAS) ignore_existing = filters.pop("ignore_existing", False) models = sort_by_subclass(*models) if ignore_existing: # When excluding models, make sure we don't ignore the models we # just assigned the an content type to. hence, start with child first. models = reversed(models) for new_model in models: new_ct = ContentType.objects.db_manager(using).get_for_model( new_model, for_concrete_model=False ) qs = new_model.objects.db_manager(using) if ignore_existing: qs = qs.filter(polymorphic_ctype__isnull=True) if filters: qs = qs.filter(**filters) qs.update(polymorphic_ctype=new_ct) def _compare_mro(cls1, cls2): if cls1 is cls2: return 0 try: index1 = cls1.mro().index(cls2) except ValueError: return -1 # cls2 not inherited by 1 try: index2 = cls2.mro().index(cls1) except ValueError: return 1 # cls1 not inherited by 2 return (index1 > index2) - (index1 < index2) # python 3 compatible cmp. def sort_by_subclass(*classes): """ Sort a series of models by their inheritance order. """ from functools import cmp_to_key return sorted(classes, key=cmp_to_key(_compare_mro)) @lru_cache(maxsize=None) def get_base_polymorphic_model(ChildModel, allow_abstract=False): """ First the first concrete model in the inheritance chain that inherited from the PolymorphicModel. """ from polymorphic.models import PolymorphicModel for Model in reversed(ChildModel.mro()): if ( issubclass(Model, PolymorphicModel) and Model is not PolymorphicModel and (allow_abstract or not Model._meta.abstract) ): return Model return None @lru_cache(maxsize=None) def route_to_ancestor(model_class, ancestor_model): """ Returns the first (highest mro precedence - depth first on parents) model inheritance route to the given ancestor model - or an empty list if no such route exists. Results are cached .. warning:: This only works for concrete ancestors! Returns a :class:`list` of :class:`ParentLinkInfo` """ route = [] def find_route(model, target_model, current_route): if model is target_model: return current_route for parent_model, field_to_parent in model._meta.parents.items(): if field_to_parent is not None: new_route = current_route + [ParentLinkInfo(parent_model, field_to_parent)] found_route = find_route(parent_model, target_model, new_route) if found_route is not None: return found_route else: return find_route(parent_model, target_model, current_route) return None found_route = find_route(model_class, ancestor_model, route) if found_route is None: return [] return found_route def is_model_loaded(model): try: apps.get_model(model._meta.app_label, model._meta.model_name) return True except LookupError: return False @lru_cache(maxsize=None) def concrete_descendants(model_class, include_proxy=False): """ Get a list of all concrete (non-abstract, non-proxy) descendant model classes in tree order with leaf descendants last. Results are cached. """ from django.apps import apps apps.check_models_ready() def add_concrete_descendants(model, result): """Add concrete descendants in tree order (ancestors before descendants).""" for sub_cls in model.__subclasses__(): # Add concrete models in pre-order (parent before children) if not sub_cls._meta.abstract and (include_proxy or not sub_cls._meta.proxy): if is_model_loaded(sub_cls): result.append(sub_cls) # Always recurse to find descendants through abstract and proxy models add_concrete_descendants(sub_cls, result) result = [] add_concrete_descendants(model_class, result) return result def prepare_for_copy(obj): """ Prepare a model instance for copying by resetting all primary keys and parent table pointers in the inheritance chain. **Copy semantics are application specific.** This function only resets the fields required to create a new instance when saved, it does not deep copy related objects or save the new instance (See :ref:`copying discussion in the Django documentation. `): .. code-block:: python from polymorphic.utils import prepare_for_copy original = YourModel.objects.get(pk=1) prepare_for_copy(original) # update any related fields here as needed original.save() # creates a new object in the database .. tip:: Preparation is at the inheritance level of the passed in model. This means you can copy and upcast at the same time. Suppose you have A->B->C inheritance chain, and you have an instance of C that you want to copy as a B instance: .. code-block:: python c = C.objects.create() c_as_b = B.objects.non_polymorphic().get(pk=c.pk) # copy c as a b instance prepare_for_copy(c_as_b) c_as_b.save() assert B.objects.count() == 2 assert C.objects.count() == 1 If you want polymorphic copying instead: .. code-block:: python prepare_for_copy(b_instance.get_real_instance()) **This function also works for non-polymorphic multi-table models.** :param obj: The model instance to prepare for copying. """ from polymorphic.models import PolymorphicModel obj.pk = None if isinstance(obj, PolymorphicModel): # we might be upcasting - allow ctype to be reset automatically on save obj.polymorphic_ctype_id = None def reset_parent_pointers(mdl): """ Reset all parent table pointers and pks in the inheritance chain. """ for parent, ptr in mdl._meta.parents.items(): reset_parent_pointers(parent) if ptr is not None: setattr(obj, ptr.attname, None) setattr(obj, parent._meta.pk.attname, None) reset_parent_pointers(obj) obj._state.adding = True # Mark as new object def _lazy_ctype(model, using=DEFAULT_DB_ALIAS): """ Return the content type id for the given model class if it is in the cache, otherwise return a subquery that can be used to match the content type as part of a larger query. Safe to call before apps are fully loaded. :param model: The model class to get the content type for. :return: The content type for the model class. :rtype: int or Subquery """ mgr = ContentType.objects.db_manager(using=using) if apps.models_ready and ( cid := mgr._cache.get(using, {}).get((model._meta.app_label, model._meta.model_name)) ): return cid return Q(app_label=model._meta.app_label) & Q(model=model._meta.model_name) def lazy_ctype(model, using=DEFAULT_DB_ALIAS): ctype = _lazy_ctype(model, using=using) return ( ctype if isinstance(ctype, ContentType) else Subquery(ContentType.objects.db_manager(using=using).filter(ctype).values("pk")[:1]) ) @lru_cache(maxsize=None) def _map_queryname_to_class(base_model, qry_name): """Try to match a model name in a query to a model class""" name_map = defaultdict(list) name_map[base_model.__name__.lower()].append(base_model) for cls in concrete_descendants(base_model, include_proxy=True): name_map[cls.__name__.lower()].append(cls) matches = name_map.get(qry_name.lower(), []) if len(matches) == 1: return matches[0] elif len(matches) > 1: raise FieldError( f"{qry_name} could refer to any of {[m._meta.label for m in matches]}. In " f"this case, please use the syntax: applabel__ModelName___field" ) # FIXME: raise a FieldError - upstream code currently relies on this AssertionError # and it will be thrown in legitimate cases because this function ends up being # called on subclasses of the original query model in _get_real_instances. That # code should be refactored to avoiid this. raise AssertionError(f"{qry_name} is not a subclass of {base_model._meta.label}") def _clear_utility_caches(): """Clear all lru_cache caches in this module.""" get_base_polymorphic_model.cache_clear() route_to_ancestor.cache_clear() concrete_descendants.cache_clear() _map_queryname_to_class.cache_clear() django-polymorphic-4.10.2/tox.ini000066400000000000000000000017071513173623500167310ustar00rootroot00000000000000[tox] envlist = py{310,311,312}-django{42} py{310,311,312,313}-django{51} py{310,311,312,313}-django{52} py{312,313,314}-django{60} # TODO: reinstate running on postgres: py310-django{42}-postgres docs [testenv] setenv = PYTHONWARNINGS = all postgres: DEFAULT_DATABASE = postgres:///default postgres: SECONDARY_DATABASE = postgres:///secondary deps = pytest pytest-cov pytest-django dj-database-url django42: Django ~= 4.2 django51: Django ~= 5.1 django52: Django ~= 5.2 django60: Django == 6.0rc1 djangomain: https://github.com/django/django/archive/main.tar.gz postgres: psycopg2 commands = pytest --cov --cov-report=term-missing --cov-report=xml . [testenv:docs] deps = Sphinx sphinx_rtd_theme -r{toxinidir}/docs/_ext/djangodummy/requirements.txt changedir = docs commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html [gh-actions] python = 3.10: py310 3.11: py311 3.12: py312 3.13: py313 3.14: py314 django-polymorphic-4.10.2/uv.lock000066400000000000000000014122641513173623500167270ustar00rootroot00000000000000version = 1 revision = 2 requires-python = ">=3.10, <4.0" resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] [[package]] name = "accessible-pygments" version = "0.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, ] [[package]] name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] [[package]] name = "anyio" version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] [[package]] name = "asgiref" version = "3.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, ] [[package]] name = "asttokens" version = "3.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] [[package]] name = "beautifulsoup4" version = "4.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] [[package]] name = "cachetools" version = "6.2.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, ] [[package]] name = "certifi" version = "2026.1.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] [[package]] name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] name = "cfgv" version = "3.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] name = "chardet" version = "5.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "click" version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] name = "cmarkgfm" version = "2025.10.22" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8f/0c/5010c87ceba51854dad42f45a3d28a3c67e81a21cfed8b20c34688aaa1b6/cmarkgfm-2025.10.22.tar.gz", hash = "sha256:5bec61007b65b919488442c838c58a6c8bf4741f5103c593b2ef180d39818eda", size = 146727, upload-time = "2025-10-22T23:13:22.639Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/63/b1d9e23ecdc03f55a2178147cd44e2768c64a8039c055cb87a2a902fb383/cmarkgfm-2025.10.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37e0b126a3868e1497f8e10c11a49b007f6f4b3104032d3a51bb84404c0f4486", size = 124568, upload-time = "2025-10-22T22:26:10.48Z" }, { url = "https://files.pythonhosted.org/packages/26/f3/48bd73c354af608a6a9437f05f88a80d47fd4ba02c8bea6e4ab7d4076c6a/cmarkgfm-2025.10.22-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1214acf1ef779b129e13a1a51e446425feaed64fd11124f44f78c93021bba853", size = 446066, upload-time = "2025-10-22T23:13:25.787Z" }, { url = "https://files.pythonhosted.org/packages/48/47/6167a6d346a38066f861f3390d91e6e1eaacd6b19ab7bc02a4a2781a2ff2/cmarkgfm-2025.10.22-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df2e8e1140e811d952c9eabc9471517f9193438e5eb17a9530f9559cbf216492", size = 449115, upload-time = "2025-10-22T23:13:27.228Z" }, { url = "https://files.pythonhosted.org/packages/18/9e/7f65aca3accb5716c862dd69ad6c9a0a9c9b010b3b4467cb7d897c4fa588/cmarkgfm-2025.10.22-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:30e8a48bc36e1e06e6938952e3d69e4c2a73cf2dcee1071942a7c036df1982ff", size = 441850, upload-time = "2025-10-22T23:13:28.585Z" }, { url = "https://files.pythonhosted.org/packages/ba/dd/c4032fe9c384ffc0e345e6ba26cd5133635e563a07a4e928ebc11eaccf6d/cmarkgfm-2025.10.22-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8d7ab8333f9cca9d4894d668ab06b19a97a30734d19c2e53ed83e33fe16d1891", size = 447711, upload-time = "2025-10-22T23:13:30.057Z" }, { url = "https://files.pythonhosted.org/packages/67/79/071c265f7b3afb29c5a637add46628fd8694fdba58bf5ad2fa2d1f70ea89/cmarkgfm-2025.10.22-cp310-cp310-win32.whl", hash = "sha256:451da49653abcde96d4671824c37acc900f6d01f69687ebeb0bd59ebf99738e0", size = 116509, upload-time = "2025-10-22T22:34:45.834Z" }, { url = "https://files.pythonhosted.org/packages/46/6a/08d88a53d8a4aaa9b172d38692d55863a4e19c573b8ee96f51e7cbea85cb/cmarkgfm-2025.10.22-cp310-cp310-win_amd64.whl", hash = "sha256:a815dab1d0f2e7af95613b26101143eb44dc92a3b44449096fa97ed54add822e", size = 127293, upload-time = "2025-10-22T22:34:47.574Z" }, { url = "https://files.pythonhosted.org/packages/1c/f2/c39553bc81b6d1bd694580b682a243274e1c54596a1ef7c4d3d73e344f3f/cmarkgfm-2025.10.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18aba514abb3eedc04c3df7dd5a7743a92d7f313a04a3f7e17a059befce6fd5f", size = 124568, upload-time = "2025-10-22T22:26:11.757Z" }, { url = "https://files.pythonhosted.org/packages/46/db/8d00199e3421ecb0d01d0862e67f83a5bdcb2aabe26da8b7df0272df070b/cmarkgfm-2025.10.22-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3da228e10238411fc6823e2d4db4d514ca41d93629a6f8be751325a5477288b9", size = 446056, upload-time = "2025-10-22T23:13:31.648Z" }, { url = "https://files.pythonhosted.org/packages/b9/19/72241f8f46feb14ce465ffb9246e8fd741e378ed23c487d5532e67f7d768/cmarkgfm-2025.10.22-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:76e1cd82deb79d0d6c1a0a9822116c277c1f7c43496cd151c340999ac4721dec", size = 449123, upload-time = "2025-10-22T23:13:33.053Z" }, { url = "https://files.pythonhosted.org/packages/c4/5c/5cf4733d9eeade65b5a74ef3ed7ff518ef305a684bde7b3e59262ca6974c/cmarkgfm-2025.10.22-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:87fd3616505159090b031a9601b2a24ebb2ee999abc562d924b99711fc6bb498", size = 441840, upload-time = "2025-10-22T23:13:34.242Z" }, { url = "https://files.pythonhosted.org/packages/ba/bf/3a09943ab4769bc89c17eb8f229d543283087e5150de52efcbefec5227e3/cmarkgfm-2025.10.22-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f848d9698023ac9e352d73d92ab58119cb1268b12c3100578cf2d1ca1aeab2bf", size = 447704, upload-time = "2025-10-22T23:13:35.606Z" }, { url = "https://files.pythonhosted.org/packages/b3/3b/76dc0b6e7521c02d67d28c05ee0cda3b90ab06da574d404f7706d105f26e/cmarkgfm-2025.10.22-cp311-cp311-win32.whl", hash = "sha256:93f34a753939b034a478a36687c6fef9010023e2cbe451b0ec83205e34252419", size = 116509, upload-time = "2025-10-22T22:34:48.724Z" }, { url = "https://files.pythonhosted.org/packages/00/d9/118b0ce17316789aef6c81af99a08d3425c2fb8b0698ff394b701450bc8a/cmarkgfm-2025.10.22-cp311-cp311-win_amd64.whl", hash = "sha256:43cb1e912675dd91fba97db47f8de7c19c0b3cf6456d188ff584c120bcaa12b9", size = 127295, upload-time = "2025-10-22T22:34:49.603Z" }, { url = "https://files.pythonhosted.org/packages/b8/e2/72098f7fccd3e7761a07b3f2be426fb2ee8cab21dd8e5215bed5fe6ec0a2/cmarkgfm-2025.10.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:74a22cf245b32a918325a2895a9a3f6e737f0b10368b39e9a99d9e76fda4a78a", size = 124616, upload-time = "2025-10-22T22:26:12.911Z" }, { url = "https://files.pythonhosted.org/packages/8b/a7/b8f20e4012cb4ebd9862423d6eb90689adacd3aafaac261c3c52db328195/cmarkgfm-2025.10.22-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68368a18b90e2dd795182c6fc34cee3180b2c8b380cad50c7fbd5563abff01b1", size = 446618, upload-time = "2025-10-22T23:13:36.811Z" }, { url = "https://files.pythonhosted.org/packages/15/9e/f547d80ccaa4c7b6cffbc4af1bda508c1ba2f50733d5528beff81df1dd42/cmarkgfm-2025.10.22-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:115fd0feaf93806b3a7c980615649b3c76a49c9dd89d5ea0c6240e10c6c71cee", size = 449859, upload-time = "2025-10-22T23:13:38.23Z" }, { url = "https://files.pythonhosted.org/packages/16/2a/b8d9897db40e4575bc2caff7d6d7c2c99f0c06177e3e3e5e798cf490611f/cmarkgfm-2025.10.22-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:69ccb4afa039f5b81d35de95fe935405577e115f367dda534309d66a455db5cb", size = 442251, upload-time = "2025-10-22T23:13:39.272Z" }, { url = "https://files.pythonhosted.org/packages/dc/71/d99e03fb8176392525f1ce9ceba67f8b111524e8c1477dea69cbdcb9ae22/cmarkgfm-2025.10.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9bed95895226cba280a96543f49d46b469b9e42528b119f280f9852cd4fc7749", size = 448305, upload-time = "2025-10-22T23:13:40.311Z" }, { url = "https://files.pythonhosted.org/packages/31/26/29db64e5cf6937b2bad8a4a0fe31459dbf9250ce225bbe1c891328ba0457/cmarkgfm-2025.10.22-cp312-cp312-win32.whl", hash = "sha256:fdf0a4689fb6febcbcaf675f2011a8074b100a4fc323f5754f627183ce492694", size = 116527, upload-time = "2025-10-22T22:34:50.768Z" }, { url = "https://files.pythonhosted.org/packages/42/31/e1356399d49f2a9e255b98a406c03a8c78848d3a4b41fe300d6c6a749276/cmarkgfm-2025.10.22-cp312-cp312-win_amd64.whl", hash = "sha256:fc14ae28769b501f61a7364a1188c827dfcf839213f02d0159801bb71c8ae989", size = 127292, upload-time = "2025-10-22T22:34:51.954Z" }, { url = "https://files.pythonhosted.org/packages/e0/6b/5da9daa0fa3d6b41f1fc1755151d459625a1376e2d968aa3b3bd42589e93/cmarkgfm-2025.10.22-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8030955c836827b95f46f8fc520a58ab2a03fb23d4b56e2d976618099273298", size = 124618, upload-time = "2025-10-22T22:26:14.002Z" }, { url = "https://files.pythonhosted.org/packages/ed/78/896e022b155b663b6129830a2b17b4907661e94f03e4ca8d64a895d686ed/cmarkgfm-2025.10.22-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:862b7f15ecf040fe9cd82f3be208286daddc5af94e1a20091af8451a0fe5fe74", size = 446642, upload-time = "2025-10-22T23:13:41.508Z" }, { url = "https://files.pythonhosted.org/packages/00/8c/c728c4129a285bd5fb10225e69089f96313e5b0529fc423514625fed9e1f/cmarkgfm-2025.10.22-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77840ddd24152881c2d374eae5e1baca462d24c2d78a937b6f30a12c2685cc0c", size = 449859, upload-time = "2025-10-22T23:13:43.024Z" }, { url = "https://files.pythonhosted.org/packages/c3/ff/e8a7b4382c2caac800a0a15ccb58eee8608a1b83f8222cdf2974d06736ab/cmarkgfm-2025.10.22-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba7693b9a4c30b2ae2d07d10c4bd3fd01dfaaaaa67c93784923b792dd10bb037", size = 442239, upload-time = "2025-10-22T23:13:44.458Z" }, { url = "https://files.pythonhosted.org/packages/58/34/7e67d73b5c243e1e9d376f0eb22cdba32f912a98d70ea63fe645df3f149b/cmarkgfm-2025.10.22-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03435c2f57ed49be0e83b2743af5dced98c1287fb9ae41a12b8055cff984bf58", size = 448306, upload-time = "2025-10-22T23:13:45.977Z" }, { url = "https://files.pythonhosted.org/packages/99/1f/4441ea20cc5ee2f8e417a5920184c5ff293073c569ab50efce6187f32d96/cmarkgfm-2025.10.22-cp313-cp313-win32.whl", hash = "sha256:aee2bf397cdf133025a2e66c6281e4fb6bd70420e3734b6dcf787ea9c2aadd78", size = 116531, upload-time = "2025-10-22T22:34:52.827Z" }, { url = "https://files.pythonhosted.org/packages/74/c3/75407cc385a5b6e93b3493c7862e21b72d55a2e8c6aec0028fba1ae241b7/cmarkgfm-2025.10.22-cp313-cp313-win_amd64.whl", hash = "sha256:f41b76d274c8886d0a440d6577cc0d73d0ea631c3bb07758adce74ba6911d790", size = 127290, upload-time = "2025-10-22T22:34:54.079Z" }, { url = "https://files.pythonhosted.org/packages/a0/4d/e188bc3739d4ba469989a265fc67efb6f3a1e8bb5a892644122d9f443196/cmarkgfm-2025.10.22-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:974eb8a69d6835eeaf11cb8f7ed0ad4cb4ddda9223693ad02aeb56cb0c036afb", size = 124591, upload-time = "2025-10-22T22:26:15.08Z" }, { url = "https://files.pythonhosted.org/packages/48/21/4880cc0ef701aa70ecedf879f0937df996457d82401ccb1739c0c59368fd/cmarkgfm-2025.10.22-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a591b213ab226232dee0b6ac8873560f01bd8cf423310bd8ce3f1c7cf913fd1f", size = 446538, upload-time = "2025-10-22T23:13:47.023Z" }, { url = "https://files.pythonhosted.org/packages/7c/b7/be78b936cf02a5a4479e6727c6d69633fd87efda93b6a2d8dcf69d231b46/cmarkgfm-2025.10.22-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54070905b888d0d4590e03f60c5153dd456f2297ff5ff9fc43ba6d561f2eff72", size = 449842, upload-time = "2025-10-22T23:13:48.481Z" }, { url = "https://files.pythonhosted.org/packages/ff/bd/b666242d5ee74684dad1dd56088a3cf0a4680e17d3f81ef2188d0a59fb7f/cmarkgfm-2025.10.22-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:37eef93238957bb238669810e1e3fe835706f9cbb25362b5ae8bbd51e39af45f", size = 442180, upload-time = "2025-10-22T23:13:49.512Z" }, { url = "https://files.pythonhosted.org/packages/31/b9/f1ab6bc256d1b6738d3d07a6927ed1a922b8698d15c475c4009f43184209/cmarkgfm-2025.10.22-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a6ee1d36735abaed7af1c8459bfabe664c3f5472bf65a390f52d5e12626304b9", size = 448279, upload-time = "2025-10-22T23:13:51.004Z" }, { url = "https://files.pythonhosted.org/packages/eb/b8/cad4107732c68c7ae1ccd2674b3996f818ab9e2f3f8c65f8f44100bca4ef/cmarkgfm-2025.10.22-cp314-cp314-win32.whl", hash = "sha256:60e7745b429d5e3019380750b3cfaf10da4a5461ead3adf9c149251d8a6e1a3c", size = 121142, upload-time = "2025-10-22T22:34:54.923Z" }, { url = "https://files.pythonhosted.org/packages/f6/bf/3e4670bb9b6926b41d453b852244e4f7a858666fdda922b5e9fb5097839f/cmarkgfm-2025.10.22-cp314-cp314-win_amd64.whl", hash = "sha256:ee90cbccd9521aa51e8d619284bb7904c5b64387eef86cbad50717b8d943ce6d", size = 132015, upload-time = "2025-10-22T22:34:56.088Z" }, { url = "https://files.pythonhosted.org/packages/27/b1/15c3bbc97dcade85e947bbbf371c53a4d7e47d442a352007864b7cfdd82e/cmarkgfm-2025.10.22-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a8d39b0c2c1c58d81a1294ed99200cba1250ec217c079b36aff11ca6b2ca4881", size = 124835, upload-time = "2025-10-22T22:26:16.183Z" }, { url = "https://files.pythonhosted.org/packages/78/97/19eead1b69c3016a771c6290c69145d2a953ccd223fe9d2056cd7ada4f86/cmarkgfm-2025.10.22-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:481740a8ab020c4b8ee49746ea6ac45ec7b68b71740d12c696a64c30e26f6f49", size = 453069, upload-time = "2025-10-22T23:13:51.976Z" }, { url = "https://files.pythonhosted.org/packages/4e/61/078efedfb3d87791e4184dd30e7e771dd1d63c8f9211875492b8a8a0bab9/cmarkgfm-2025.10.22-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367d1ab78f26af73b866a165358382c6e8e66d49da73621e832febeaeeb6400c", size = 456002, upload-time = "2025-10-22T23:13:53.357Z" }, { url = "https://files.pythonhosted.org/packages/fd/4e/635fdab0cb143fd93db6b8da7bcc23bb2fa26ccfe6d5e7cdbff33445d3e6/cmarkgfm-2025.10.22-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2176dc0e5e4966ca4746dcbd26324adbe17be86f48756f28438d157ae1f26520", size = 449302, upload-time = "2025-10-22T23:13:54.393Z" }, { url = "https://files.pythonhosted.org/packages/f0/2a/c697a739c30c3d5f31b021e706b5edaf82ce28599c56770e5f91c4e6fe66/cmarkgfm-2025.10.22-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c96d1238d91cf35e9c022af2e8c0be0a7ac227eb94497ffdf10584a684e38a3e", size = 453913, upload-time = "2025-10-22T23:13:55.573Z" }, { url = "https://files.pythonhosted.org/packages/b1/7a/39b705eff24bb25182ca11fd8788753e9ec94e605b9c27a4f01a9fd2b3e2/cmarkgfm-2025.10.22-cp314-cp314t-win32.whl", hash = "sha256:905a773bc866ccb4dc97a343057e2bfe07934522e1380831be413d3f93626f62", size = 121403, upload-time = "2025-10-22T22:34:57.24Z" }, { url = "https://files.pythonhosted.org/packages/bc/4b/9ad83c26fb8cff61cc03b55c301e3069cb340506ad3ee56e9ab26ce0ac75/cmarkgfm-2025.10.22-cp314-cp314t-win_amd64.whl", hash = "sha256:f2a04d119d09f7f5c8b565b1e8c691596bfbc59d8cabac4d7fa542a069c2c70f", size = 132298, upload-time = "2025-10-22T22:34:58.379Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.13.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "cryptography" version = "46.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, ] [[package]] name = "cx-oracle" version = "8.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e8/16/13c265afc984796fe38ee928733569b599cfd657245ddd1afad238b66656/cx_Oracle-8.3.0.tar.gz", hash = "sha256:3b2d215af4441463c97ea469b9cc307460739f89fdfa8ea222ea3518f1a424d9", size = 363886, upload-time = "2021-11-04T22:08:34.141Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a9/b7/c2d0223fb4f1013b090cf82f3ce56f36f33b79a48f9c33b36717c2977b04/cx_Oracle-8.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b6a23da225f03f50a81980c61dbd6a358c3575f212ca7f4c22bb65a9faf94f7f", size = 892553, upload-time = "2021-11-04T22:08:43.072Z" }, { url = "https://files.pythonhosted.org/packages/68/5b/8294aa9db4b54a3be10bec66c812d2527ca02a3290b1475c0e5faa1e70bc/cx_Oracle-8.3.0-cp310-cp310-win32.whl", hash = "sha256:715a8bbda5982af484ded14d184304cc552c1096c82471dd2948298470e88a04", size = 147835, upload-time = "2021-11-04T22:08:44.125Z" }, { url = "https://files.pythonhosted.org/packages/67/a3/b671dee7f34971acf553df0c28818da6bf53b1a64901303f904c12ff443a/cx_Oracle-8.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:07f01608dfb6603a8f2a868fc7c7bdc951480f187df8dbc50f4d48c884874e6a", size = 213059, upload-time = "2021-11-04T22:08:46.164Z" }, ] [[package]] name = "decorator" version = "5.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] [[package]] name = "distlib" version = "0.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] [[package]] name = "dj-database-url" version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/c6/88676a7333fb7c668e626b55f8bfc8527dd863973eb1c40412b95d27747d/dj_database_url-3.1.0.tar.gz", hash = "sha256:d80218426b83f9302c8d27d4fccf52de5cf0cab179f0645fb2839f37605d1353", size = 7924, upload-time = "2026-01-04T09:18:32.693Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/68/1b/e84f7472ab0bdacc3fd09556eb4dd40d88246941d465cc103b36a8dabcd8/dj_database_url-3.1.0-py3-none-any.whl", hash = "sha256:155a56fbbecbaaf1348ccd73bf29138b4c9988363ba08261a0f0145e392e638c", size = 8849, upload-time = "2026-01-04T09:18:43.77Z" }, ] [[package]] name = "django" version = "5.2.10" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] dependencies = [ { name = "asgiref", marker = "python_full_version < '3.12'" }, { name = "sqlparse", marker = "python_full_version < '3.12'" }, { name = "tzdata", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e6/e5/2671df24bf0ded831768ef79532e5a7922485411a5696f6d979568591a37/django-5.2.10.tar.gz", hash = "sha256:74df100784c288c50a2b5cad59631d71214f40f72051d5af3fdf220c20bdbbbe", size = 10880754, upload-time = "2026-01-06T18:55:26.817Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/f1a7cd896daec85832136ab509d9b2a6daed4939dbe26313af3e95fc5f5e/django-5.2.10-py3-none-any.whl", hash = "sha256:cf85067a64250c95d5f9067b056c5eaa80591929f7e16fbcd997746e40d6c45c", size = 8290820, upload-time = "2026-01-06T18:55:20.009Z" }, ] [[package]] name = "django" version = "6.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ { name = "asgiref", marker = "python_full_version >= '3.12'" }, { name = "sqlparse", marker = "python_full_version >= '3.12'" }, { name = "tzdata", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b5/9b/016f7e55e855ee738a352b05139d4f8b278d0b451bd01ebef07456ef3b0e/django-6.0.1.tar.gz", hash = "sha256:ed76a7af4da21551573b3d9dfc1f53e20dd2e6c7d70a3adc93eedb6338130a5f", size = 11069565, upload-time = "2026-01-06T18:55:53.069Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/95/b5/814ed98bd21235c116fd3436a7ed44d47560329a6d694ec8aac2982dbb93/django-6.0.1-py3-none-any.whl", hash = "sha256:a92a4ff14f664a896f9849009cb8afaca7abe0d6fc53325f3d1895a15253433d", size = 8338791, upload-time = "2026-01-06T18:55:46.175Z" }, ] [[package]] name = "django-extra-views" version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/61/c6d2ced11fc4235d2aeb422ad5af3b516effd942e2be5a263a5d93dcb1e2/django_extra_views-0.16.0.tar.gz", hash = "sha256:7f8e07bd6c9388816a7c08d752661172d4078758c2079fc0cadddfcf5cd38ae3", size = 13235, upload-time = "2025-04-22T15:10:58.475Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/6a/511cf7ad5ac8d0890eb7ec374668759e9fbb263af98abb516fdc14be2633/django_extra_views-0.16.0-py2.py3-none-any.whl", hash = "sha256:af990d1779813b728231e94bcc703614fef60015c4c31d53185c5f4f898a37be", size = 15344, upload-time = "2025-04-22T15:10:56.814Z" }, ] [[package]] name = "django-filter" version = "25.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2c/e4/465d2699cd388c0005fb8d6ae6709f239917c6d8790ac35719676fffdcf3/django_filter-25.2.tar.gz", hash = "sha256:760e984a931f4468d096f5541787efb8998c61217b73006163bf2f9523fe8f23", size = 143818, upload-time = "2025-10-05T09:51:31.521Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3", size = 94145, upload-time = "2025-10-05T09:51:29.728Z" }, ] [[package]] name = "django-guardian" version = "3.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e2/f9/bcff6a931298b9eb55e1550b55ab964fab747f594ba6d2d81cbe19736c5f/django_guardian-3.2.0.tar.gz", hash = "sha256:9e18ecd2e211b665972690c2d03d27bce0ea4932b5efac24a4bb9d526950a69e", size = 99940, upload-time = "2025-09-16T10:35:53.609Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2f/23/63a7d868373a73d25c4a5c2dd3cce3aaeb22fbee82560d42b6e93ba01403/django_guardian-3.2.0-py3-none-any.whl", hash = "sha256:0768565a057988a93fc4a1d93649c4a794abfd7473a8408a079cfbf83c559d77", size = 134674, upload-time = "2025-09-16T10:35:51.69Z" }, ] [[package]] name = "django-polymorphic" version = "4.10.2" source = { editable = "." } dependencies = [ { name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] [package.dev-dependencies] cx-oracle = [ { name = "cx-oracle" }, ] dev = [ { name = "coverage" }, { name = "dj-database-url" }, { name = "django-test-migrations" }, { name = "ipdb" }, { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-django" }, { name = "pytest-mock" }, { name = "pytest-playwright" }, { name = "ruff" }, { name = "tomlkit" }, { name = "tox" }, { name = "tox-uv" }, ] docs = [ { name = "django-extra-views" }, { name = "djangorestframework" }, { name = "doc8" }, { name = "furo" }, { name = "readme-renderer", extra = ["md"] }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-autobuild", version = "2025.8.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-django" }, ] drf = [ { name = "django-filter" }, { name = "djangorestframework" }, ] extra-views = [ { name = "django-extra-views" }, ] guardian = [ { name = "django-guardian" }, ] mysql = [ { name = "mysqlclient" }, ] oracledb = [ { name = "oracledb" }, ] psycopg2 = [ { name = "psycopg2" }, ] psycopg3 = [ { name = "psycopg" }, ] reversion = [ { name = "django-reversion" }, ] [package.metadata] requires-dist = [{ name = "django", specifier = ">=4.2" }] [package.metadata.requires-dev] cx-oracle = [{ name = "cx-oracle", specifier = ">=8.3.0" }] dev = [ { name = "coverage", specifier = ">=7.6.1" }, { name = "dj-database-url", specifier = ">=2.2.0" }, { name = "django-test-migrations", specifier = ">=1.5.0" }, { name = "ipdb", specifier = ">=0.13.13" }, { name = "ipython", specifier = ">=8.18.1" }, { name = "mypy", specifier = ">=1.14.1" }, { name = "pre-commit", specifier = ">=3.5.0" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-cov", specifier = ">=5.0.0" }, { name = "pytest-django", specifier = ">=4.10.0" }, { name = "pytest-mock", specifier = ">=3.15.1" }, { name = "pytest-playwright", specifier = ">=0.7.2" }, { name = "ruff", specifier = ">=0.9.8" }, { name = "tomlkit", specifier = ">=0.13.3" }, { name = "tox", specifier = ">=4.24.1" }, { name = "tox-uv", specifier = ">=1.13.1" }, ] docs = [ { name = "django-extra-views", specifier = ">=0.16.0" }, { name = "djangorestframework", specifier = ">=3.16.1" }, { name = "doc8", specifier = ">=1.1.2" }, { name = "furo", specifier = ">=2025.7.19" }, { name = "readme-renderer", extras = ["md"], specifier = ">=43.0" }, { name = "sphinx", specifier = ">=7.1.2" }, { name = "sphinx-autobuild", specifier = ">=2024.10.3" }, { name = "sphinxcontrib-django", specifier = ">=2.5" }, ] drf = [ { name = "django-filter", specifier = ">=24.0" }, { name = "djangorestframework", specifier = ">=3.16.1" }, ] extra-views = [{ name = "django-extra-views", specifier = ">=0.16.0" }] guardian = [{ name = "django-guardian", specifier = ">=2.4.0" }] mysql = [{ name = "mysqlclient", specifier = ">=1.4.0" }] oracledb = [{ name = "oracledb", specifier = ">=2.3.0" }] psycopg2 = [{ name = "psycopg2", specifier = ">=2.9.10" }] psycopg3 = [{ name = "psycopg" }] reversion = [{ name = "django-reversion", specifier = ">=6.1.0" }] [[package]] name = "django-reversion" version = "6.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9b/8b/72000ad3ba05cb72134f62ad093267e3d83d8fdf1406102d2a1a2ec7d55e/django_reversion-6.1.0.tar.gz", hash = "sha256:31dd7fee02e2a21af7c2d0a61c1c0f27ba07df2317c9a0a3f31d3ee40069025d", size = 76140, upload-time = "2025-12-12T20:23:51.948Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5e/8e/c80904ce5cb94f78cdd7d794cb7b4a8e8891a7515621bbce5f2a45de6555/django_reversion-6.1.0-py3-none-any.whl", hash = "sha256:1f5815791d9accdb0bad9d5982668f217dc65225be57041ae9641dbdcd838ce3", size = 86196, upload-time = "2025-12-12T20:23:50.18Z" }, ] [[package]] name = "django-test-migrations" version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/ed/7fc6f8e89d83565fc4acb93ae0a2387d885ac83cda445cb6c570f302bf55/django_test_migrations-1.5.0.tar.gz", hash = "sha256:1cbff04b1e82c5564a6f635284907b381cc11a2ff883adff46776d9126824f07", size = 20143, upload-time = "2025-04-18T10:15:38.547Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c0/fe/38789c69f71adff9156bda7542d8fd05fcde1a109cf67bd7a1a139f8199f/django_test_migrations-1.5.0-py3-none-any.whl", hash = "sha256:96a08f085fc8bfaa53d44618341d82a2d22fd194c821cd81b147b66f0bec0da8", size = 25099, upload-time = "2025-04-18T10:15:37.16Z" }, ] [[package]] name = "djangorestframework" version = "3.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735, upload-time = "2025-08-06T17:50:53.251Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" }, ] [[package]] name = "doc8" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "pygments" }, { name = "restructuredtext-lint" }, { name = "stevedore" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/91/88bb55225046a2ee9c2243d47346c78d2ed861c769168f451568625ad670/doc8-2.0.0.tar.gz", hash = "sha256:1267ad32758971fbcf991442417a3935c7bc9e52550e73622e0e56ba55ea1d40", size = 28436, upload-time = "2025-06-13T13:08:53.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a2/e9/90b7d243364d3dce38c8c2a1b8c103d7a8d1383c2b24c735fae0eee038dd/doc8-2.0.0-py3-none-any.whl", hash = "sha256:9862710027f793c25f9b1899150660e4bf1d4c9a6738742e71f32011e2e3f590", size = 25861, upload-time = "2025-06-13T13:08:51.839Z" }, ] [[package]] name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "executing" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] [[package]] name = "filelock" version = "3.20.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] name = "furo" version = "2025.12.19" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accessible-pygments" }, { name = "beautifulsoup4" }, { name = "pygments" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-basic-ng" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ec/20/5f5ad4da6a5a27c80f2ed2ee9aee3f9e36c66e56e21c00fde467b2f8f88f/furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7", size = 1661473, upload-time = "2025-12-19T17:34:40.889Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" }, ] [[package]] name = "greenlet" version = "3.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/6a/33d1702184d94106d3cdd7bfb788e19723206fce152e303473ca3b946c7b/greenlet-3.3.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d", size = 273658, upload-time = "2025-12-04T14:23:37.494Z" }, { url = "https://files.pythonhosted.org/packages/d6/b7/2b5805bbf1907c26e434f4e448cd8b696a0b71725204fa21a211ff0c04a7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb", size = 574810, upload-time = "2025-12-04T14:50:04.154Z" }, { url = "https://files.pythonhosted.org/packages/94/38/343242ec12eddf3d8458c73f555c084359883d4ddc674240d9e61ec51fd6/greenlet-3.3.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd", size = 586248, upload-time = "2025-12-04T14:57:39.35Z" }, { url = "https://files.pythonhosted.org/packages/f0/d0/0ae86792fb212e4384041e0ef8e7bc66f59a54912ce407d26a966ed2914d/greenlet-3.3.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b299a0cb979f5d7197442dccc3aee67fce53500cd88951b7e6c35575701c980b", size = 597403, upload-time = "2025-12-04T15:07:10.831Z" }, { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, { url = "https://files.pythonhosted.org/packages/f4/6b/d4e73f5dfa888364bbf02efa85616c6714ae7c631c201349782e5b428925/greenlet-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b49e7ed51876b459bd645d83db257f0180e345d3f768a35a85437a24d5a49082", size = 300740, upload-time = "2025-12-04T14:47:52.773Z" }, { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "identify" version = "2.6.16" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, ] [[package]] name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "imagesize" version = "1.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "ipdb" version = "0.13.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "decorator" }, { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/4c/b075da0092003d9a55cf2ecc1cae9384a1ca4f650d51b00fc59875fe76f6/ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4", size = 12130, upload-time = "2023-03-09T15:40:55.021Z" }, ] [[package]] name = "ipython" version = "8.38.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, { name = "decorator", marker = "python_full_version < '3.11'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "jedi", marker = "python_full_version < '3.11'" }, { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, { name = "pygments", marker = "python_full_version < '3.11'" }, { name = "stack-data", marker = "python_full_version < '3.11'" }, { name = "traitlets", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" }, ] [[package]] name = "ipython" version = "9.9.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, { name = "decorator", marker = "python_full_version >= '3.11'" }, { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, { name = "jedi", marker = "python_full_version >= '3.11'" }, { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, { name = "pygments", marker = "python_full_version >= '3.11'" }, { name = "stack-data", marker = "python_full_version >= '3.11'" }, { name = "traitlets", marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/dd/fb08d22ec0c27e73c8bc8f71810709870d51cadaf27b7ddd3f011236c100/ipython-9.9.0.tar.gz", hash = "sha256:48fbed1b2de5e2c7177eefa144aba7fcb82dac514f09b57e2ac9da34ddb54220", size = 4425043, upload-time = "2026-01-05T12:36:46.233Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.0-py3-none-any.whl", hash = "sha256:b457fe9165df2b84e8ec909a97abcf2ed88f565970efba16b1f7229c283d252b", size = 621431, upload-time = "2026-01-05T12:36:44.669Z" }, ] [[package]] name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] [[package]] name = "jedi" version = "0.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "librt" version = "0.7.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "matplotlib-inline" version = "0.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] [[package]] name = "mypy" version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "mysqlclient" version = "2.2.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/61/68/810093cb579daae426794bbd9d88aa830fae296e85172d18cb0f0e5dd4bc/mysqlclient-2.2.7.tar.gz", hash = "sha256:24ae22b59416d5fcce7e99c9d37548350b4565baac82f95e149cac6ce4163845", size = 91383, upload-time = "2025-01-10T12:06:00.763Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/24/cdaaef42aac7d53c0a01bb638da64961c293b1b6d204efd47400a68029d4/mysqlclient-2.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:2e3c11f7625029d7276ca506f8960a7fd3c5a0a0122c9e7404e6a8fe961b3d22", size = 207748, upload-time = "2025-01-10T11:56:24.357Z" }, { url = "https://files.pythonhosted.org/packages/ef/e3/3e2de3f93cd60dd63bd229ec3e3b679f682982614bf513d046c2722aa4ce/mysqlclient-2.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:a22d99d26baf4af68ebef430e3131bb5a9b722b79a9fcfac6d9bbf8a88800687", size = 207745, upload-time = "2025-01-10T11:56:28.67Z" }, { url = "https://files.pythonhosted.org/packages/bb/b5/2a8a4bcba3440550f358b839638fe8ec9146fa3c9194890b4998a530c926/mysqlclient-2.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:4b4c0200890837fc64014cc938ef2273252ab544c1b12a6c1d674c23943f3f2e", size = 208032, upload-time = "2025-01-10T11:56:29.879Z" }, { url = "https://files.pythonhosted.org/packages/29/01/e80141f1cd0459e4c9a5dd309dee135bbae41d6c6c121252fdd853001a8a/mysqlclient-2.2.7-cp313-cp313-win_amd64.whl", hash = "sha256:201a6faa301011dd07bca6b651fe5aaa546d7c9a5426835a06c3172e1056a3c5", size = 208000, upload-time = "2025-01-10T11:56:32.293Z" }, { url = "https://files.pythonhosted.org/packages/16/cc/5b1570be9f8597ee41e2a0bd7b62ba861ec2c81898d9449f3d6bfbe15d29/mysqlclient-2.2.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92af368ed9c9144737af569c86d3b6c74a012a6f6b792eb868384787b52bb585", size = 207800, upload-time = "2025-01-10T11:56:36.023Z" }, ] [[package]] name = "nh3" version = "0.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/01/a1eda067c0ba823e5e2bb033864ae4854549e49fb6f3407d2da949106bfb/nh3-0.3.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d", size = 1419839, upload-time = "2025-10-30T11:17:09.956Z" }, { url = "https://files.pythonhosted.org/packages/30/57/07826ff65d59e7e9cc789ef1dc405f660cabd7458a1864ab58aefa17411b/nh3-0.3.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130", size = 791183, upload-time = "2025-10-30T11:17:11.99Z" }, { url = "https://files.pythonhosted.org/packages/af/2f/e8a86f861ad83f3bb5455f596d5c802e34fcdb8c53a489083a70fd301333/nh3-0.3.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b", size = 829127, upload-time = "2025-10-30T11:17:13.192Z" }, { url = "https://files.pythonhosted.org/packages/d8/97/77aef4daf0479754e8e90c7f8f48f3b7b8725a3b8c0df45f2258017a6895/nh3-0.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5", size = 997131, upload-time = "2025-10-30T11:17:14.677Z" }, { url = "https://files.pythonhosted.org/packages/41/ee/fd8140e4df9d52143e89951dd0d797f5546004c6043285289fbbe3112293/nh3-0.3.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31", size = 1068783, upload-time = "2025-10-30T11:17:15.861Z" }, { url = "https://files.pythonhosted.org/packages/87/64/bdd9631779e2d588b08391f7555828f352e7f6427889daf2fa424bfc90c9/nh3-0.3.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99", size = 994732, upload-time = "2025-10-30T11:17:17.155Z" }, { url = "https://files.pythonhosted.org/packages/79/66/90190033654f1f28ca98e3d76b8be1194505583f9426b0dcde782a3970a2/nh3-0.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868", size = 975997, upload-time = "2025-10-30T11:17:18.77Z" }, { url = "https://files.pythonhosted.org/packages/34/30/ebf8e2e8d71fdb5a5d5d8836207177aed1682df819cbde7f42f16898946c/nh3-0.3.2-cp314-cp314t-win32.whl", hash = "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93", size = 583364, upload-time = "2025-10-30T11:17:20.286Z" }, { url = "https://files.pythonhosted.org/packages/94/ae/95c52b5a75da429f11ca8902c2128f64daafdc77758d370e4cc310ecda55/nh3-0.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13", size = 589982, upload-time = "2025-10-30T11:17:21.384Z" }, { url = "https://files.pythonhosted.org/packages/b4/bd/c7d862a4381b95f2469704de32c0ad419def0f4a84b7a138a79532238114/nh3-0.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80", size = 577126, upload-time = "2025-10-30T11:17:22.755Z" }, { url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" }, { url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" }, { url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" }, { url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" }, { url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" }, { url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" }, { url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" }, { url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" }, { url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" }, { url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" }, { url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" }, { url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" }, { url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" }, { url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" }, { url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" }, ] [[package]] name = "nodeenv" version = "1.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "oracledb" version = "3.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/9d/4e86cd410294ebbb1f90a609aaae61c5fa064a5c10e501de3f4c67664e6c/oracledb-3.4.1.tar.gz", hash = "sha256:f5920df5ac9446579e8409607bba31dc2d23a2286a5b0ea17cb0d78d419392a6", size = 852693, upload-time = "2025-11-12T03:21:36.157Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4b/70/05645e72a67b45396a248a7949d89c91dc7a1ab5f7cedad110d9804e29d5/oracledb-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dfe18061f064d0455fad10d9301f6f92df9e32d18d75fb32802caf1ced4b304c", size = 4243226, upload-time = "2025-11-12T03:21:41.734Z" }, { url = "https://files.pythonhosted.org/packages/7e/cc/f3a78ae31f87e41378c7bc60928fa5432d4eba80806cb0086edc11803a22/oracledb-3.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84055d6fd093a4d7b8ed653f433531e4c4cc161f7261d78efd7f6a65a1f19444", size = 2426914, upload-time = "2025-11-12T03:21:43.641Z" }, { url = "https://files.pythonhosted.org/packages/a6/a6/3d3dabbec2651851f13fdb7c318a3c50780090235d340d851f7cb8deeeec/oracledb-3.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9e20b6cd3245e84c30188874c524bb3c67c79b7a04fcb864e6ac39f55eae826", size = 2605903, upload-time = "2025-11-12T03:21:45.378Z" }, { url = "https://files.pythonhosted.org/packages/ae/59/aa174fc8f5629b890424702edf582a8a635acaa0db1315b16160d703a887/oracledb-3.4.1-cp310-cp310-win32.whl", hash = "sha256:abedb0bf464bcf14d83e245eae000e03cad8ac68c945eb09cc46002d800fbf00", size = 1490352, upload-time = "2025-11-12T03:21:46.732Z" }, { url = "https://files.pythonhosted.org/packages/8a/1c/9dded6efc747d8980667584c8464295d80d205f8a131e31cacfb274b6ed5/oracledb-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ee604bb0f3acb5680782818f973445b8cd168e72a73b5ca2cd9807140afadee", size = 1837541, upload-time = "2025-11-12T03:21:48.571Z" }, { url = "https://files.pythonhosted.org/packages/ed/9e/5901349b8797fabc7c6f78230376bfbd5541a847f1eb34be23bfb971add7/oracledb-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:20b268be64994d0f636df9ff7613dcce420133f373d0d7fc84a31dd2f07322c0", size = 4226376, upload-time = "2025-11-12T03:21:49.959Z" }, { url = "https://files.pythonhosted.org/packages/fc/c0/951d2ab8c04df9da309a82e211d19223a64dbbcfdd79f5f1aba6d8736408/oracledb-3.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d493946318d99a0f0e3f01d7c64c08ddae66f0aac735fa23c1eb94949d9db0f5", size = 2422323, upload-time = "2025-11-12T03:21:51.583Z" }, { url = "https://files.pythonhosted.org/packages/a8/7c/82843dd7e55dec6331c0c7737e32523eb2f6156c6469055e2cb752e848f4/oracledb-3.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d64fda2fa5d3e82c58b2c5126ab5511bccb84f8b47eedfe9f17e9c100fe7683", size = 2601267, upload-time = "2025-11-12T03:21:52.978Z" }, { url = "https://files.pythonhosted.org/packages/27/3f/67b50042f955574fca574a2234ba4af421e9268601bceb49efd9c43c6bc8/oracledb-3.4.1-cp311-cp311-win32.whl", hash = "sha256:cd80aa4c4dec7347c6d2909fbaf7e35a5253341ff2cb6f3782ab7ca712bf0405", size = 1488075, upload-time = "2025-11-12T03:21:54.704Z" }, { url = "https://files.pythonhosted.org/packages/8d/14/bab071234d61e84c65712902dd0edec825d82b3198ffddc977c9ea9a91f3/oracledb-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e01e8696009cec4ebcb9fe678b23b8223595dc186c065899660cac4c1fc189b", size = 1843449, upload-time = "2025-11-12T03:21:56.342Z" }, { url = "https://files.pythonhosted.org/packages/f7/d9/98367ba2c358de366de70b505531f9717cdfa7e29eff0c9ad113eecfce96/oracledb-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1c3f92c023ef1983e0e7f9a1b4a31df8568974c28c06ab0a574b1126e45083a8", size = 4222133, upload-time = "2025-11-12T03:21:58.212Z" }, { url = "https://files.pythonhosted.org/packages/36/52/48ad2f7dae6288a2ddf0ac536d46ce4883d2d10ec7e16afbbd48f1ec0ff3/oracledb-3.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251211d64b90cc42d00ec2d2893873bc02ff4bc22125e9fc5a7f148a6208fd88", size = 2230374, upload-time = "2025-11-12T03:21:59.656Z" }, { url = "https://files.pythonhosted.org/packages/8d/08/60d4301b4f72f099ed2252f8d0eb143e6fe9e5c8f4c2705c3163cea36808/oracledb-3.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea529a5e6036fae3e2bc195fa76b6f48cd9c431e68c74ef78ee6a5e39c855c39", size = 2421755, upload-time = "2025-11-12T03:22:01.543Z" }, { url = "https://files.pythonhosted.org/packages/48/35/412a90019a030f5dff0c031319733c6b8dd477832bafa88b733b4b3ec57b/oracledb-3.4.1-cp312-cp312-win32.whl", hash = "sha256:94e8e6d63b45fedd4e243147cb25dea1a0f6599d83852f3979fe725a8533e85a", size = 1449688, upload-time = "2025-11-12T03:22:03.422Z" }, { url = "https://files.pythonhosted.org/packages/7b/01/ae9eca3055dc625923564ca653ca99ddd8eda95e44953ce55c18aba55066/oracledb-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:84f15c483f9ec80dcded925df6ff473c69a293cd694d09b69abb911500659df4", size = 1794622, upload-time = "2025-11-12T03:22:04.941Z" }, { url = "https://files.pythonhosted.org/packages/f0/4d/e32db901340dc6fc824d0d3b5e4660fe0199fba8adb0e81ac08b639c8ab9/oracledb-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ad817807b293e371c951af8ee67a56a5af88a5680a54fe79dfc7b9393ca128aa", size = 4206469, upload-time = "2025-11-12T03:22:06.881Z" }, { url = "https://files.pythonhosted.org/packages/cf/68/1a038f29523eea19e42f4dd765bf523752408816b5ff21e8b998d8b25457/oracledb-3.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34b9bc25eae217defa3f4b8289b4915cd1101aaeeec33c7bace74f927996d452", size = 2233055, upload-time = "2025-11-12T03:22:08.259Z" }, { url = "https://files.pythonhosted.org/packages/b9/66/a51243553ac6b0e1bc2cfd4db8a2f3299b1b60c9231d7c9133ee1442d15b/oracledb-3.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be6575759ba56ab3758f82bfbb74f75288ce69190e19c087793050cb012c0aa1", size = 2443312, upload-time = "2025-11-12T03:22:09.615Z" }, { url = "https://files.pythonhosted.org/packages/f7/57/a6056d4432c07a959fd1032dd45bfaff69b91ac7e1204dbccf7bf7b4a91d/oracledb-3.4.1-cp313-cp313-win32.whl", hash = "sha256:635587e5f28be83ec0bf72e4bfb2f3a4544c0f8e303f2327f376d57116894541", size = 1453553, upload-time = "2025-11-12T03:22:11.045Z" }, { url = "https://files.pythonhosted.org/packages/6a/57/dca415d8dd18a2a030a9402d49039493cdce6acfd37c8a038a4ede2328e6/oracledb-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:354177708352e124c0f97ceccbe34be05e7f3ce7040a7dd3c2ebd857145ffe74", size = 1794005, upload-time = "2025-11-12T03:22:12.694Z" }, { url = "https://files.pythonhosted.org/packages/59/07/dff7b9e6242b627d56f3fa6ad6639802003e1e5fbcc883d0ce27d82455ad/oracledb-3.4.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:3ec1f9dd7310da7cbf219c2a05bb52df08da950c95ad2ace8a289854947bdc6b", size = 4247946, upload-time = "2025-11-12T03:22:14.473Z" }, { url = "https://files.pythonhosted.org/packages/1f/95/739868c6f312683cc3afe9534644b4ce2d054fe137d8f7a1e7786df9f5aa/oracledb-3.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:337a67d6c91015dfe7a2a1915f65c74adad26fcd428daaead296d91c92f09ad1", size = 2271628, upload-time = "2025-11-12T03:22:15.956Z" }, { url = "https://files.pythonhosted.org/packages/fb/7c/307da513f5fb68e6454beb5bc1c715ec09a70d2af70a28b9fa6001c1b09b/oracledb-3.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d5ffe4dd26e8012de433ec69f93be5737d81b04324072ec36dad37eb778fd9d", size = 2455603, upload-time = "2025-11-12T03:22:18.112Z" }, { url = "https://files.pythonhosted.org/packages/c5/1a/af5bd7239cebfc33541432cfcba75893a3f2f44fa66648e6d8ce1fe96b0c/oracledb-3.4.1-cp314-cp314-win32.whl", hash = "sha256:693ef5f8c420545511096b3bc9a3861617222717321bc78c776afbbb6c16c5b9", size = 1474932, upload-time = "2025-11-12T03:22:19.574Z" }, { url = "https://files.pythonhosted.org/packages/f1/ee/79d2ed18fd234bcbd407c1b36372dc898cf68de825ec650df7b1627acb51/oracledb-3.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:6adb483d7120cdd056173b71c901f71dbe2265c5bd402f768b0b1ab27af519b1", size = 1837566, upload-time = "2025-11-12T03:22:20.959Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "parso" version = "0.8.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, ] [[package]] name = "pathspec" version = "1.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, ] [[package]] name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] [[package]] name = "platformdirs" version = "4.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] [[package]] name = "playwright" version = "1.57.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet" }, { name = "pyee" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ed/b6/e17543cea8290ae4dced10be21d5a43c360096aa2cce0aa7039e60c50df3/playwright-1.57.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:9351c1ac3dfd9b3820fe7fc4340d96c0d3736bb68097b9b7a69bd45d25e9370c", size = 41985039, upload-time = "2025-12-09T08:06:18.408Z" }, { url = "https://files.pythonhosted.org/packages/8b/04/ef95b67e1ff59c080b2effd1a9a96984d6953f667c91dfe9d77c838fc956/playwright-1.57.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4a9d65027bce48eeba842408bcc1421502dfd7e41e28d207e94260fa93ca67e", size = 40775575, upload-time = "2025-12-09T08:06:22.105Z" }, { url = "https://files.pythonhosted.org/packages/60/bd/5563850322a663956c927eefcf1457d12917e8f118c214410e815f2147d1/playwright-1.57.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:99104771abc4eafee48f47dac2369e0015516dc1ce8c409807d2dd440828b9a4", size = 41985042, upload-time = "2025-12-09T08:06:25.357Z" }, { url = "https://files.pythonhosted.org/packages/56/61/3a803cb5ae0321715bfd5247ea871d25b32c8f372aeb70550a90c5f586df/playwright-1.57.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:284ed5a706b7c389a06caa431b2f0ba9ac4130113c3a779767dda758c2497bb1", size = 45975252, upload-time = "2025-12-09T08:06:29.186Z" }, { url = "https://files.pythonhosted.org/packages/83/d7/b72eb59dfbea0013a7f9731878df8c670f5f35318cedb010c8a30292c118/playwright-1.57.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a1bae6c0a07839cdeaddbc0756b3b2b85e476c07945f64ece08f1f956a86f1", size = 45706917, upload-time = "2025-12-09T08:06:32.549Z" }, { url = "https://files.pythonhosted.org/packages/e4/09/3fc9ebd7c95ee54ba6a68d5c0bc23e449f7235f4603fc60534a364934c16/playwright-1.57.0-py3-none-win32.whl", hash = "sha256:1dd93b265688da46e91ecb0606d36f777f8eadcf7fbef12f6426b20bf0c9137c", size = 36553860, upload-time = "2025-12-09T08:06:35.864Z" }, { url = "https://files.pythonhosted.org/packages/58/d4/dcdfd2a33096aeda6ca0d15584800443dd2be64becca8f315634044b135b/playwright-1.57.0-py3-none-win_amd64.whl", hash = "sha256:6caefb08ed2c6f29d33b8088d05d09376946e49a73be19271c8cd5384b82b14c", size = 36553864, upload-time = "2025-12-09T08:06:38.915Z" }, { url = "https://files.pythonhosted.org/packages/6a/60/fe31d7e6b8907789dcb0584f88be741ba388413e4fbce35f1eba4e3073de/playwright-1.57.0-py3-none-win_arm64.whl", hash = "sha256:5f065f5a133dbc15e6e7c71e7bc04f258195755b1c32a432b792e28338c8335e", size = 32837940, upload-time = "2025-12-09T08:06:42.268Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pprintpp" version = "0.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/06/1a/7737e7a0774da3c3824d654993cf57adc915cb04660212f03406334d8c0b/pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403", size = 17995, upload-time = "2018-07-01T01:42:34.87Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4e/d1/e4ed95fdd3ef13b78630280d9e9e240aeb65cc7c544ec57106149c3942fb/pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d", size = 16952, upload-time = "2018-07-01T01:42:36.496Z" }, ] [[package]] name = "pre-commit" version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] [[package]] name = "prompt-toolkit" version = "3.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] [[package]] name = "psycopg" version = "3.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" }, ] [[package]] name = "psycopg2" version = "2.9.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/89/8d/9d12bc8677c24dad342ec777529bce705b3e785fa05d85122b5502b9ab55/psycopg2-2.9.11.tar.gz", hash = "sha256:964d31caf728e217c697ff77ea69c2ba0865fa41ec20bb00f0977e62fdcc52e3", size = 379598, upload-time = "2025-10-10T11:14:46.075Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e3/ba/b7672ed9d0be238265972ef52a7a8c9e9e815ca2a7dc19a1b2e4b5b637f0/psycopg2-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:103e857f46bb76908768ead4e2d0ba1d1a130e7b8ed77d3ae91e8b33481813e8", size = 2713725, upload-time = "2025-10-10T11:10:09.391Z" }, { url = "https://files.pythonhosted.org/packages/86/fe/d6dce306fd7b61e312757ba4d068617f562824b9c6d3e4a39fc578ea2814/psycopg2-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:210daed32e18f35e3140a1ebe059ac29209dd96468f2f7559aa59f75ee82a5cb", size = 2713723, upload-time = "2025-10-10T11:10:12.957Z" }, { url = "https://files.pythonhosted.org/packages/b5/bf/635fbe5dd10ed200afbbfbe98f8602829252ca1cce81cc48fb25ed8dadc0/psycopg2-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:e03e4a6dbe87ff81540b434f2e5dc2bddad10296db5eea7bdc995bf5f4162938", size = 2713969, upload-time = "2025-10-10T11:10:15.946Z" }, { url = "https://files.pythonhosted.org/packages/88/5a/18c8cb13fc6908dc41a483d2c14d927a7a3f29883748747e8cb625da6587/psycopg2-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:8dc379166b5b7d5ea66dcebf433011dfc51a7bb8a5fc12367fa05668e5fc53c8", size = 2714048, upload-time = "2025-10-10T11:10:19.816Z" }, { url = "https://files.pythonhosted.org/packages/47/08/737aa39c78d705a7ce58248d00eeba0e9fc36be488f9b672b88736fbb1f7/psycopg2-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:f10a48acba5fe6e312b891f290b4d2ca595fc9a06850fe53320beac353575578", size = 2803738, upload-time = "2025-10-10T11:10:23.196Z" }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] [[package]] name = "pure-eval" version = "0.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] [[package]] name = "pycparser" version = "2.23" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] [[package]] name = "pyee" version = "13.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyproject-api" version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330", size = 22785, upload-time = "2025-10-09T19:12:27.21Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09", size = 13218, upload-time = "2025-10-09T19:12:24.428Z" }, ] [[package]] name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] name = "pytest-base-url" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/1a/b64ac368de6b993135cb70ca4e5d958a5c268094a3a2a4cac6f0021b6c4f/pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45", size = 6702, upload-time = "2024-01-31T22:43:00.81Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/1c/b00940ab9eb8ede7897443b771987f2f4a76f06be02f1b3f01eb7567e24a/pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6", size = 5302, upload-time = "2024-01-31T22:42:58.897Z" }, ] [[package]] name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] name = "pytest-django" version = "4.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202, upload-time = "2025-04-03T18:56:09.338Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" }, ] [[package]] name = "pytest-mock" version = "3.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] [[package]] name = "pytest-playwright" version = "0.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "playwright" }, { name = "pytest" }, { name = "pytest-base-url" }, { name = "python-slugify" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e8/6b/913e36aa421b35689ec95ed953ff7e8df3f2ee1c7b8ab2a3f1fd39d95faf/pytest_playwright-0.7.2.tar.gz", hash = "sha256:247b61123b28c7e8febb993a187a07e54f14a9aa04edc166f7a976d88f04c770", size = 16928, upload-time = "2025-11-24T03:43:22.53Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/61/4d333d8354ea2bea2c2f01bad0a4aa3c1262de20e1241f78e73360e9b620/pytest_playwright-0.7.2-py3-none-any.whl", hash = "sha256:8084e015b2b3ecff483c2160f1c8219b38b66c0d4578b23c0f700d1b0240ea38", size = 16881, upload-time = "2025-11-24T03:43:24.423Z" }, ] [[package]] name = "python-slugify" version = "8.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "text-unidecode" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "readme-renderer" version = "44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "nh3" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, ] [package.optional-dependencies] md = [ { name = "cmarkgfm" }, ] [[package]] name = "requests" version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] name = "restructuredtext-lint" version = "2.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ca/e6/eefcad2228f4124f17e01064428fbcd0ade06a274f3063ce3a126a569d6b/restructuredtext_lint-2.0.2.tar.gz", hash = "sha256:dd25209b9e0b726929d8306339faf723734a3137db382bcf27294fa18a6bc52b", size = 17494, upload-time = "2025-11-23T08:05:18.585Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/af/63/ac52b32b33ae62f2076ed5c4f6b00e065e3ccbb2063e9a2e813b2bfc95bf/restructuredtext_lint-2.0.2-py3-none-any.whl", hash = "sha256:374c0d3e7e0867b2335146a145343ac619400623716b211b9a010c94426bbed7", size = 14198, upload-time = "2025-11-23T08:05:23.267Z" }, ] [[package]] name = "roman-numerals" version = "4.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] [[package]] name = "ruff" version = "0.14.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" }, { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" }, { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" }, { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" }, { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" }, { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" }, { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" }, { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" }, { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" }, { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" }, { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" }, { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" }, { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" }, { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" }, { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" }, { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" }, { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" }, { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, ] [[package]] name = "snowballstemmer" version = "3.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] [[package]] name = "soupsieve" version = "2.8.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" }, ] [[package]] name = "sphinx" version = "8.1.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ { name = "alabaster", marker = "python_full_version < '3.11'" }, { name = "babel", marker = "python_full_version < '3.11'" }, { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, { name = "docutils", marker = "python_full_version < '3.11'" }, { name = "imagesize", marker = "python_full_version < '3.11'" }, { name = "jinja2", marker = "python_full_version < '3.11'" }, { name = "packaging", marker = "python_full_version < '3.11'" }, { name = "pygments", marker = "python_full_version < '3.11'" }, { name = "requests", marker = "python_full_version < '3.11'" }, { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, ] [[package]] name = "sphinx" version = "9.0.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ { name = "alabaster", marker = "python_full_version == '3.11.*'" }, { name = "babel", marker = "python_full_version == '3.11.*'" }, { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, { name = "docutils", marker = "python_full_version == '3.11.*'" }, { name = "imagesize", marker = "python_full_version == '3.11.*'" }, { name = "jinja2", marker = "python_full_version == '3.11.*'" }, { name = "packaging", marker = "python_full_version == '3.11.*'" }, { name = "pygments", marker = "python_full_version == '3.11.*'" }, { name = "requests", marker = "python_full_version == '3.11.*'" }, { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, ] [[package]] name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ { name = "alabaster", marker = "python_full_version >= '3.12'" }, { name = "babel", marker = "python_full_version >= '3.12'" }, { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, { name = "docutils", marker = "python_full_version >= '3.12'" }, { name = "imagesize", marker = "python_full_version >= '3.12'" }, { name = "jinja2", marker = "python_full_version >= '3.12'" }, { name = "packaging", marker = "python_full_version >= '3.12'" }, { name = "pygments", marker = "python_full_version >= '3.12'" }, { name = "requests", marker = "python_full_version >= '3.12'" }, { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, ] [[package]] name = "sphinx-autobuild" version = "2024.10.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "starlette", marker = "python_full_version < '3.11'" }, { name = "uvicorn", marker = "python_full_version < '3.11'" }, { name = "watchfiles", marker = "python_full_version < '3.11'" }, { name = "websockets", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023, upload-time = "2024-10-02T23:15:30.172Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/c0/eba125db38c84d3c74717008fd3cb5000b68cd7e2cbafd1349c6a38c3d3b/sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa", size = 11908, upload-time = "2024-10-02T23:15:28.739Z" }, ] [[package]] name = "sphinx-autobuild" version = "2025.8.25" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.11'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "starlette", marker = "python_full_version >= '3.11'" }, { name = "uvicorn", marker = "python_full_version >= '3.11'" }, { name = "watchfiles", marker = "python_full_version >= '3.11'" }, { name = "websockets", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, ] [[package]] name = "sphinx-basic-ng" version = "1.0.0b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" }, ] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, ] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, ] [[package]] name = "sphinxcontrib-django" version = "2.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pprintpp" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dc/28/92f6d685899fbd74a6c575c50dcc1abb8ab69c6da0160bc99d557d2104d1/sphinxcontrib-django-2.5.tar.gz", hash = "sha256:45a54c0cc1f641d6c15872828862f0738348ca8d7d5b92777bcaa530678c2cc4", size = 23788, upload-time = "2023-09-26T17:54:36.259Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/45/3c/2de4b64abb71cf0f27f9e4401695d8843efed9fdd89ab49957f157a23519/sphinxcontrib_django-2.5-py3-none-any.whl", hash = "sha256:70148af4ccbb5184c5b1add939c3827c79a29a7d20ed18c25ceab347e3f14ca8", size = 21538, upload-time = "2023-09-26T17:54:34.404Z" }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, ] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] [[package]] name = "sqlparse" version = "0.5.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, ] [[package]] name = "stack-data" version = "0.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asttokens" }, { name = "executing" }, { name = "pure-eval" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] [[package]] name = "starlette" version = "0.51.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e7/65/5a1fadcc40c5fdc7df421a7506b79633af8f5d5e3a95c3e72acacec644b9/starlette-0.51.0.tar.gz", hash = "sha256:4c4fda9b1bc67f84037d3d14a5112e523509c369d9d47b111b2f984b0cc5ba6c", size = 2647658, upload-time = "2026-01-10T20:23:15.043Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/c4/09985a03dba389d4fe16a9014147a7b02fa76ef3519bf5846462a485876d/starlette-0.51.0-py3-none-any.whl", hash = "sha256:fb460a3d6fd3c958d729fdd96aee297f89a51b0181f16401fe8fd4cb6129165d", size = 74133, upload-time = "2026-01-10T20:23:13.445Z" }, ] [[package]] name = "stevedore" version = "5.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/5b/496f8abebd10c3301129abba7ddafd46c71d799a70c44ab080323987c4c9/stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945", size = 516074, upload-time = "2025-11-20T10:06:07.264Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428, upload-time = "2025-11-20T10:06:05.946Z" }, ] [[package]] name = "text-unidecode" version = "1.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, ] [[package]] name = "tomli" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "tomlkit" version = "0.14.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, ] [[package]] name = "tox" version = "4.34.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "chardet" }, { name = "colorama" }, { name = "filelock" }, { name = "packaging" }, { name = "platformdirs" }, { name = "pluggy" }, { name = "pyproject-api" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/9b/5909f40b281ebd37c2f83de5087b9cb8a9a64c33745f334be0aeaedadbbc/tox-4.34.1.tar.gz", hash = "sha256:ef1e82974c2f5ea02954d590ee0b967fad500c3879b264ea19efb9a554f3cc60", size = 205306, upload-time = "2026-01-09T17:42:59.895Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/0f/fe6629e277ce615e53d0a0b65dc23c88b15a402bb7dbf771f17bbd18f1c4/tox-4.34.1-py3-none-any.whl", hash = "sha256:5610d69708bab578d618959b023f8d7d5d3386ed14a2392aeebf9c583615af60", size = 176812, upload-time = "2026-01-09T17:42:58.629Z" }, ] [[package]] name = "tox-uv" version = "1.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tox" }, { name = "uv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4f/90/06752775b8cfadba8856190f5beae9f552547e0f287e0246677972107375/tox_uv-1.29.0.tar.gz", hash = "sha256:30fa9e6ad507df49d3c6a2f88894256bcf90f18e240a00764da6ecab1db24895", size = 23427, upload-time = "2025-10-09T20:40:27.384Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5c/17/221d62937c4130b044bb437caac4181e7e13d5536bbede65264db1f0ac9f/tox_uv-1.29.0-py3-none-any.whl", hash = "sha256:b1d251286edeeb4bc4af1e24c8acfdd9404700143c2199ccdbb4ea195f7de6cc", size = 17254, upload-time = "2025-10-09T20:40:25.885Z" }, ] [[package]] name = "traitlets" version = "5.14.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "tzdata" version = "2025.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] name = "urllib3" version = "2.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "uv" version = "0.9.25" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/41/b3/c2a6afd3d8f8f9f5d9c65fcdff1b80fb5bdaba21c8b0e99dd196e71d311f/uv-0.9.25.tar.gz", hash = "sha256:8625de8f40e7b669713e293ab4f7044bca9aa7f7c739f17dc1fd0cb765e69f28", size = 3863318, upload-time = "2026-01-13T23:20:16.141Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/13/e1/9284199aed638643a4feadf8b3283c1d43b3c3adcbdac367f26a8f5e398f/uv-0.9.25-py3-none-linux_armv6l.whl", hash = "sha256:db51f37b3f6c94f4371d8e26ee8adeb9b1b1447c5fda8cc47608694e49ea5031", size = 21479938, upload-time = "2026-01-13T23:21:13.011Z" }, { url = "https://files.pythonhosted.org/packages/6d/5c/79dc42e1abf0afc021823c688ff04e4283f9e72d20ca4af0027aa7ed29df/uv-0.9.25-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e47a9da2ddd33b5e7efb8068a24de24e24fd0d88a99e0c4a7e2328424783eab8", size = 20681034, upload-time = "2026-01-13T23:20:19.269Z" }, { url = "https://files.pythonhosted.org/packages/7c/0b/997f279db671fe4b1cf87ad252719c1b7c47a9546efd6c2594b5648ea983/uv-0.9.25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:79af8c9b885b507a82087e45161a4bda7f2382682867dc95f7e6d22514ac844d", size = 19096089, upload-time = "2026-01-13T23:20:55.021Z" }, { url = "https://files.pythonhosted.org/packages/d5/60/a7682177fe76501b403d464b4fee25c1ee4089fe56caf7cb87c2e6741375/uv-0.9.25-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:6ca6bdd3fe4b1730d1e3d10a4ce23b269915a60712379d3318ecea9a4ff861fd", size = 20848810, upload-time = "2026-01-13T23:20:13.916Z" }, { url = "https://files.pythonhosted.org/packages/4c/c1/01d5df4cbec33da51fc85868f129562cbd1488290465107c03bed90d8ca4/uv-0.9.25-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d993b9c590ac76f805e17441125d67c7774b1ba05340dc987d3de01852226b6", size = 21095071, upload-time = "2026-01-13T23:20:44.488Z" }, { url = "https://files.pythonhosted.org/packages/6d/fe/f7cd2f02b0e0974dd95f732efd12bd36a3e8419d53f4d1d49744d2e3d979/uv-0.9.25-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4b72881d0c66ad77844451dbdbcada87242c0d39c6bfd0f89ac30b917a3cfc3", size = 22070541, upload-time = "2026-01-13T23:21:16.936Z" }, { url = "https://files.pythonhosted.org/packages/f8/e6/ef53b6d69b303eca6aa56ad97eb322f6cc5b9571c403e4e64313f1ccfb81/uv-0.9.25-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac0dfb6191e91723a69be533102f98ffa5739cba57c3dfc5f78940c27cf0d7e8", size = 23663768, upload-time = "2026-01-13T23:20:29.808Z" }, { url = "https://files.pythonhosted.org/packages/0d/f8/f0e01ddfc62cb4b8ec5c6d94e46fc77035c0cd77865d7958144caadf8ad9/uv-0.9.25-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41ae0f2df7c931b72949345134070efa919174321c5bd403954db960fa4c2d7d", size = 23235860, upload-time = "2026-01-13T23:20:58.724Z" }, { url = "https://files.pythonhosted.org/packages/6a/56/905257af2c63ffaec9add9cce5d34f851f418d42e6f4e73fee18adecd499/uv-0.9.25-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bf02fcea14b8bec42b9c04094cc5b527c2cd53b606c06e7bdabfbd943b4512c", size = 22236426, upload-time = "2026-01-13T23:20:40.995Z" }, { url = "https://files.pythonhosted.org/packages/bb/ce/909feee469647b7929967397dcb1b6b317cfca07dc3fc0699b3cab700daf/uv-0.9.25-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642f993d8c74ecd52b192d5f3168433c4efa81b8bb19c5ac97c25f27a44557cb", size = 22294538, upload-time = "2026-01-13T23:21:09.521Z" }, { url = "https://files.pythonhosted.org/packages/82/be/ac7cd3c45c6baf0d5181133d3bda13f843f76799809374095b6fc7122a96/uv-0.9.25-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:564b5db5e148670fdbcfd962ee8292c0764c1be0c765f63b620600a3c81087d1", size = 20963345, upload-time = "2026-01-13T23:20:25.706Z" }, { url = "https://files.pythonhosted.org/packages/19/fd/7b6191cef8da4ad451209dde083123b1ac9d10d6c2c1554a1de64aa41ad8/uv-0.9.25-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:991cfb872ef3bc0cc5e88f4d3f68adf181218a3a57860f523ff25279e4cf6657", size = 22205573, upload-time = "2026-01-13T23:20:33.611Z" }, { url = "https://files.pythonhosted.org/packages/15/80/8d6809df5e5ddf862f963fbfc8b2a25c286dc36724e50c7536e429d718be/uv-0.9.25-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:e1b4ab678c6816fe41e3090777393cf57a0f4ef122f99e9447d789ab83863a78", size = 21036715, upload-time = "2026-01-13T23:20:51.413Z" }, { url = "https://files.pythonhosted.org/packages/bb/78/e3cb00bf90a359fa8106e2446bad07e49922b41e096e4d3b335b0065117a/uv-0.9.25-py3-none-musllinux_1_1_i686.whl", hash = "sha256:aa7db0ab689c3df34bdd46f83d2281d268161677ccd204804a87172150a654ef", size = 21505379, upload-time = "2026-01-13T23:21:06.045Z" }, { url = "https://files.pythonhosted.org/packages/86/36/07f69f45878175d2907110858e5c6631a1b712420d229012296c1462b133/uv-0.9.25-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a658e47e54f11dac9b2751fba4ad966a15db46c386497cf51c1c02f656508358", size = 22520308, upload-time = "2026-01-13T23:20:09.704Z" }, { url = "https://files.pythonhosted.org/packages/1d/1b/2d457ee7e2dd35fc22ae6f656bb45b781b33083d4f0a40901b9ae59e0b10/uv-0.9.25-py3-none-win32.whl", hash = "sha256:4df14479f034f6d4dca9f52230f912772f56ceead3354c7b186a34927c22188a", size = 20263705, upload-time = "2026-01-13T23:20:47.814Z" }, { url = "https://files.pythonhosted.org/packages/5c/0b/05ad2dc53dab2c8aa2e112ef1f9227a7b625ba3507bedd7b31153d73aa5f/uv-0.9.25-py3-none-win_amd64.whl", hash = "sha256:001629fbc2a955c35f373311591c6952be010a935b0bc6244dc61da108e4593d", size = 22311694, upload-time = "2026-01-13T23:21:02.562Z" }, { url = "https://files.pythonhosted.org/packages/54/4e/99788924989082356d6aa79d8bfdba1a2e495efaeae346fd8fec83d3f078/uv-0.9.25-py3-none-win_arm64.whl", hash = "sha256:ea26319abf9f5e302af0d230c0f13f02591313e5ffadac34931f963ef4d7833d", size = 20645549, upload-time = "2026-01-13T23:20:37.201Z" }, ] [[package]] name = "uvicorn" version = "0.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] [[package]] name = "virtualenv" version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]] name = "watchfiles" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, ] [[package]] name = "wcwidth" version = "0.2.14" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, ] [[package]] name = "websockets" version = "16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ]