From a813644990e92404c596dff0ac34ad1c3215dfa0 Mon Sep 17 00:00:00 2001 From: Peter Urda Date: Mon, 2 Feb 2026 23:45:37 -0800 Subject: [PATCH] v6.0.0 release commit --- .github/workflows/testing-42.yaml | 11 +- .github/workflows/testing-52.yaml | 11 +- .github/workflows/testing-60.yaml | 11 +- .gitignore | 3 + CHANGELOG.md | 6 + Makefile | 124 +++++++-------- pyproject.toml | 19 +++ scripts/local_integration.sh | 2 +- scripts/testpypi_integration.sh | 145 ++++++++++++++++++ .../test_integration_local_server.py | 4 +- tox.ini | 70 +++++++-- uv.lock | 35 +++++ 12 files changed, 359 insertions(+), 82 deletions(-) create mode 100755 scripts/testpypi_integration.sh diff --git a/.github/workflows/testing-42.yaml b/.github/workflows/testing-42.yaml index 5e94c72..4969ae0 100644 --- a/.github/workflows/testing-42.yaml +++ b/.github/workflows/testing-42.yaml @@ -99,7 +99,16 @@ jobs: - name: Local Integration Testing run: make test-integration - - name: Upload coverage reports to Codecov + - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + + - name: Upload test results to Codecov + uses: codecov/codecov-action@v5 + if: ${{ !cancelled() }} + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: junit.xml + report_type: test_results diff --git a/.github/workflows/testing-52.yaml b/.github/workflows/testing-52.yaml index 5f54b97..9614070 100644 --- a/.github/workflows/testing-52.yaml +++ b/.github/workflows/testing-52.yaml @@ -101,7 +101,16 @@ jobs: - name: Local Integration Testing run: make test-integration - - name: Upload coverage reports to Codecov + - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + + - name: Upload test results to Codecov + uses: codecov/codecov-action@v5 + if: ${{ !cancelled() }} + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: junit.xml + report_type: test_results diff --git a/.github/workflows/testing-60.yaml b/.github/workflows/testing-60.yaml index e2a10c6..3606d00 100644 --- a/.github/workflows/testing-60.yaml +++ b/.github/workflows/testing-60.yaml @@ -99,7 +99,16 @@ jobs: - name: Local Integration Testing run: make test-integration - - name: Upload coverage reports to Codecov + - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + + - name: Upload test results to Codecov + uses: codecov/codecov-action@v5 + if: ${{ !cancelled() }} + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: junit.xml + report_type: test_results diff --git a/.gitignore b/.gitignore index 6d596ba..7ef61f0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ beta_dist/ # Testing .coverage +coverage.xml +junit.xml +junit-integration.xml .cache/ .tox/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c9de1..d7c0727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,12 @@ This release aligns with recent Django releases and Python releases. - Updated `actions/checkout@v2` references to `actions/checkout@v6`. - Renamed GitHub Actions workflows to match supported Django versions (`testing-42.yaml`, `testing-52.yaml`, `testing-60.yaml`). - Updated `tox.ini` to test against Django `4.2`, `5.2`, and `6.0`. + - General `Makefile` refactoring. + - Added `scripts/testpypi_integration.sh` for testing deployed Test PyPI packages. + - Added Test PyPI integration test matrix to `tox.ini` with parallel execution support. + - Migrated unit tests to `pytest` with `pytest-django` and `pytest-cov`. + - Added Codecov test results integration via JUnit XML output. + - Fixed Codecov coverage upload by generating `coverage.xml` during test runs. - General Updates: - Added `Development` section to `README` with `uv` setup instructions. - Updated `pyproject.toml` to include missing classifiers. diff --git a/Makefile b/Makefile index 8bd2a48..abd2652 100644 --- a/Makefile +++ b/Makefile @@ -5,33 +5,32 @@ DIST = ./dist BETA_DIST = ./beta_dist +#--------------------------------------------------------------------------------------------------- # Composite Variables +#--------------------------------------------------------------------------------------------------- -CLEAN_TARGETS = ./.cache ./.tox ./*.egg-info ./.pytest_cache $(BETA_DIST) $(DIST) ./build ./htmlcov .coverage coverage.xml +CLEAN_TARGETS = ./.cache ./.tox ./*.egg-info ./.pytest_cache $(BETA_DIST) $(DIST) ./build ./htmlcov .coverage coverage.xml junit.xml junit-integration.xml ######################################################################################################################## -# `make help` Needs to be first so it is ran when just `make` is called +# Utilities ######################################################################################################################## - +# `help` Needs to be first so it is ran when just `make` is called .PHONY: help help: # Show this help screen @ack '^[a-zA-Z_-]+:.*?# .*$$' $(MAKEFILE_LIST) |\ - sort -k1,1 |\ + LC_ALL=C sort -t: -k1,1 |\ awk 'BEGIN {FS = ":.*?# "}; {printf "\033[1m%-30s\033[0m %s\n", $$1, $$2}' +.PHONY: clean +clean: # Clean up build, test, and other project artifacts + rm -rf $(CLEAN_TARGETS) && \ + find . | grep -E "(__pycache__|\.pyc|\.pyo$$)" | xargs rm -rf && \ + : -######################################################################################################################## -# Testing -######################################################################################################################## - -.PHONY: run-tox -run-tox: # Run tox for the project - @uv run tox - -.PHONY: test -test: version-check test-flake test-unit test-coverage-report # Run the full testing suite - +#--------------------------------------------------------------------------------------------------- +# Versioning +#--------------------------------------------------------------------------------------------------- .PHONY: version-check version-check: # Verify the project version string is correct across the project @@ -41,108 +40,87 @@ version-check: # Verify the project version string is correct across the project version-check-django: # Verify the project's Django version @uv run python -c 'import django; print(django.VERSION)' +######################################################################################################################## +# Testing +######################################################################################################################## + +.PHONY: test +test: version-check test-flake test-unit test-coverage-report # Run the full testing suite + +.PHONY: test-tox +test-tox: # Run tox for the project + @uv run tox -p auto #--------------------------------------------------------------------------------------------------- -# Test Subcommands +# Internal Test Commands #--------------------------------------------------------------------------------------------------- - # Report test coverage after tests are complete .PHONY: test-coverage-report test-coverage-report: @uv run coverage report - # Run flake8 against project files .PHONY: test-flake test-flake: @uv run flake8 -v - # Tox testing requires coverage to "append" results -.PHONY: test-tox -test-tox: COV-ARGS = --append -test-tox: test-unit - +.PHONY: test-tox-entry +test-tox-entry: COV-ARGS = --append +test-tox-entry: test-unit # Run only unit tests .PHONY: test-unit test-unit: - @uv run coverage run \ - ${COV-ARGS} \ - --source="./letsencrypt" \ - --omit="\ - ./letsencrypt/migrations/*,\ - ./letsencrypt/admin.py,\ - ./letsencrypt/apps.py,\ - ./letsencrypt/tests.py,\ - ./letsencrypt/urls.py,\ - " \ - example_project/manage.py test \ - --settings=example_project.settings_test \ - + @uv run pytest letsencrypt/tests.py \ + --cov=letsencrypt \ + --cov-config=pyproject.toml \ + --cov-report=term-missing \ + --cov-report=xml \ + --junitxml=junit.xml \ + ${PYTEST-ARGS} \ ######################################################################################################################## # Integration Testing ######################################################################################################################## - .PHONY: test-integration test-integration: # Run the integration tests for the project @./scripts/local_integration.sh +.PHONY: test-integration-testpypi +test-integration-testpypi: # Test the deployed Test PyPI package (requires VERSION arg) + @./scripts/testpypi_integration.sh $(VERSION) -######################################################################################################################## -# Project Publishing -######################################################################################################################## - - -.PHONY: publish -publish: build # Build and publish the package to PyPi - uv run twine upload --repository pypi $(DIST)/* - - -.PHONY: test-publish -test-publish: build-beta # Build and publish the package to TestPyPi - uv run twine upload --repository testpypi $(BETA_DIST)/* - +.PHONY: test-integration-testpypi-tox +test-integration-testpypi-tox: # Test the deployed Test PyPI package for all versions in parallel with tox (requires VERSION arg) + @TESTPYPI_VERSION=$(VERSION) uv run tox -m testpypi -p all ######################################################################################################################## -# Project Building +# Building ######################################################################################################################## - .PHONY: build build: build-pre build-package # Build the release package - .PHONY: build-beta build-beta: build-pre build-beta-package # Build the beta package - -.PHONY: clean -clean: # Clean up build, test, and other project artifacts - rm -rf $(CLEAN_TARGETS) && \ - find . | grep -E "(__pycache__|\.pyc|\.pyo$$)" | xargs rm -rf && \ - : - #--------------------------------------------------------------------------------------------------- -# Build Subcommands +# Internal Build Commands #--------------------------------------------------------------------------------------------------- - # Perform required pre-build steps for all build types .PHONY: build-pre build-pre: version-check clean test - -# Build 'sdist' and 'bdist_wheel' for this package (PyPi) +# Build 'sdist' and 'bdist_wheel' for this package (PyPI) .PHONY: build-package build-package: uv build --out-dir $(DIST) - -# Build 'sdist' and 'bdist_wheel' for the beta package (Test PyPi) +# Build 'sdist' and 'bdist_wheel' for the beta package (Test PyPI) .PHONY: build-beta-package build-beta-package: uv run ./scripts/version_manager.py set-beta-build && \ @@ -150,3 +128,15 @@ build-beta-package: uv build --out-dir $(BETA_DIST) && \ uv run ./scripts/version_manager.py unset-beta-build && \ : + +######################################################################################################################## +# Publishing +######################################################################################################################## + +.PHONY: publish +publish: build # Build and publish the package to PyPI + uv run twine upload --repository pypi $(DIST)/* + +.PHONY: publish-test +publish-test: build-beta # Build and publish the package to Test PyPI + uv run twine upload --repository testpypi $(BETA_DIST)/* diff --git a/pyproject.toml b/pyproject.toml index 5dc7232..85ae5e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,11 +56,30 @@ dev = [ 'coverage', 'flake8', 'pytest', + 'pytest-cov', + 'pytest-django', 'setuptools', 'tox', 'tox-uv', 'twine', ] +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "example_project.settings_test" +pythonpath = [".", "example_project"] +python_files = ["tests.py", "test_*.py", "*_test.py"] +testpaths = ["letsencrypt", "tests"] +addopts = "-v" + +[tool.coverage.run] +source = ["letsencrypt"] +omit = [ + "letsencrypt/migrations/*", + "letsencrypt/admin.py", + "letsencrypt/apps.py", + "letsencrypt/tests.py", + "letsencrypt/urls.py", +] + [tool.setuptools.packages.find] include = ['letsencrypt*'] diff --git a/scripts/local_integration.sh b/scripts/local_integration.sh index 2061f40..571df8f 100755 --- a/scripts/local_integration.sh +++ b/scripts/local_integration.sh @@ -17,7 +17,7 @@ popd echo "${PREFIX} sleeping as server boots..." && sleep 2 -uv run pytest -v ./tests/integration/ +uv run pytest -v ./tests/integration/ --junitxml=junit-integration.xml kill ${SERVER_PID} echo "${PREFIX} killed server via PID ${SERVER_PID}" diff --git a/scripts/testpypi_integration.sh b/scripts/testpypi_integration.sh new file mode 100755 index 0000000..f5b0088 --- /dev/null +++ b/scripts/testpypi_integration.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# +# -*- bash -*- +# shellcheck shell=bash +# +# Test PyPI Integration Testing Script +# +# This script tests the DEPLOYED package from test-pypi (not local code). +# It creates an isolated environment to ensure we're testing the actual +# published artifact. +# +# Usage: +# ./scripts/testpypi_integration.sh [PYTHON] [DJANGO_VERSION] [PORT] +# +# Examples: +# ./scripts/testpypi_integration.sh 6.0.0.123.20260130.83438 +# ./scripts/testpypi_integration.sh 6.0.0.123.20260130.83438 python3.12 +# ./scripts/testpypi_integration.sh 6.0.0.123.20260130.83438 python3.12 5.2 +# ./scripts/testpypi_integration.sh 6.0.0.123.20260130.83438 python3.12 5.2 8021 +# + +set -euo pipefail + +PREFIX="[Test PyPI Integration]" + +# Parse arguments +VERSION="${1:-}" +PYTHON="${2:-python3}" +DJANGO_VERSION="${3:-}" +DJANGO_TEST_PORT="${4:-${DJANGO_TEST_PORT:-8000}}" + +if [[ -z "${VERSION}" ]]; then + echo "${PREFIX} ERROR: VERSION argument is required" + echo "Usage: ${0} [PYTHON] [DJANGO_VERSION] [PORT]" + exit 1 +fi + +echo "${PREFIX} Testing django-letsencrypt version: ${VERSION}" +echo "${PREFIX} Using Python: ${PYTHON}" +echo "${PREFIX} Using port: ${DJANGO_TEST_PORT}" +[[ -n "${DJANGO_VERSION}" ]] && echo "${PREFIX} Django version: ${DJANGO_VERSION}" + +# Store the original project directory (where tests live) +PROJECT_DIR="$(pwd)" + +# Create a temporary working directory +WORK_DIR=$(mktemp -d) +echo "${PREFIX} Created temp directory: ${WORK_DIR}" + +# Cleanup function +cleanup() { + echo "${PREFIX} Cleaning up..." + + # Kill server if running + if [[ -n "${SERVER_PID:-}" ]]; then + kill "${SERVER_PID}" 2>/dev/null || true + echo "${PREFIX} Killed server PID ${SERVER_PID}" + fi + + # Remove temp directory + rm -rf "${WORK_DIR}" + echo "${PREFIX} Removed temp directory" +} +trap cleanup EXIT + +# Copy example_project to temp directory +cp -r "${PROJECT_DIR}/example_project" "${WORK_DIR}/example_project" +rm -f "${WORK_DIR}/example_project/db.sqlite3" +echo "${PREFIX} Copied example_project to temp directory (fresh database)" + +# Create isolated virtual environment +${PYTHON} -m venv "${WORK_DIR}/.venv" +echo "${PREFIX} Created virtual environment" + +# Activate the virtual environment +# shellcheck disable=SC1091 +source "${WORK_DIR}/.venv/bin/activate" +echo "${PREFIX} Activated virtual environment" + +# Upgrade pip +pip install --upgrade pip --quiet + +# Install Django if specific version requested +if [[ -n "${DJANGO_VERSION}" ]]; then + echo "${PREFIX} Installing Django ${DJANGO_VERSION}..." + pip install "Django>=${DJANGO_VERSION},<$((${DJANGO_VERSION%%.*} + 1))" --quiet +fi + +# Install django-letsencrypt from test-pypi +echo "${PREFIX} Installing django-letsencrypt==${VERSION} from test-pypi..." +pip install \ + --index-url https://test.pypi.org/simple/ \ + --extra-index-url https://pypi.org/simple/ \ + "django-letsencrypt==${VERSION}" + +# Show what was installed +echo "${PREFIX} Installed packages:" +pip list | grep -E "(django|letsencrypt|pytz)" + +# Navigate to the example project +cd "${WORK_DIR}/example_project" + +# Run migrations +echo "${PREFIX} Running migrations..." +python manage.py migrate --settings=example_project.settings + +# Create example data +echo "${PREFIX} Creating example data..." +python ./create_example_data.py + +# Start the server in background +echo "${PREFIX} Starting Django development server on port ${DJANGO_TEST_PORT}..." +python manage.py runserver "127.0.0.1:${DJANGO_TEST_PORT}" --settings=example_project.settings > /dev/null 2>&1 & +SERVER_PID=$! +echo "${PREFIX} Server PID is ${SERVER_PID}" + +# Wait for server to boot with retry logic +echo "${PREFIX} Waiting for server to start..." +MAX_RETRIES=30 +RETRY_COUNT=0 +while ! curl -s "http://127.0.0.1:${DJANGO_TEST_PORT}/" > /dev/null 2>&1; do + RETRY_COUNT=$((RETRY_COUNT + 1)) + if [[ ${RETRY_COUNT} -ge ${MAX_RETRIES} ]]; then + echo "${PREFIX} ERROR: Server failed to start after ${MAX_RETRIES} attempts" + exit 1 + fi + if ! kill -0 "${SERVER_PID}" 2>/dev/null; then + echo "${PREFIX} ERROR: Server process died" + exit 1 + fi + sleep 0.5 +done +echo "${PREFIX} Server is ready (took ${RETRY_COUNT} attempts)" + +# Run integration tests (using pytest from the temp venv) +echo "${PREFIX} Running integration tests..." +cd "${PROJECT_DIR}" + +# Install pytest in the temp venv if needed +pip install pytest --quiet + +# Run the integration tests +DJANGO_TEST_PORT="${DJANGO_TEST_PORT}" pytest -v ./tests/integration/ + +echo "${PREFIX} Integration tests passed!" diff --git a/tests/integration/test_integration_local_server.py b/tests/integration/test_integration_local_server.py index c93f5bd..9af254c 100644 --- a/tests/integration/test_integration_local_server.py +++ b/tests/integration/test_integration_local_server.py @@ -16,6 +16,7 @@ limitations under the License. """ +import os import urllib.parse import urllib.request import urllib.response @@ -28,7 +29,8 @@ def build_url_for_challenge(base_url: str, challenge: str) -> str: def get_base_url() -> str: - return 'http://localhost:8000/.well-known/acme-challenge/' + port = os.environ.get('DJANGO_TEST_PORT', '8000') + return f'http://localhost:{port}/.well-known/acme-challenge/' @pytest.mark.parametrize("expected_challenge,expected_response", [ diff --git a/tox.ini b/tox.ini index 4852fab..0d4a704 100644 --- a/tox.ini +++ b/tox.ini @@ -9,46 +9,96 @@ envlist = py{312,313,314}-django60 coverage-report - [testenv] set_env = UV_PROJECT_ENVIRONMENT={env_dir} - commands = python --version make version-check-django - make test-tox - + make test-tox-entry deps = django42: Django>=4.2.27,<5.0 django52: Django>=5.2.10,<6.0 django60: Django>=6.0.1,<7.0 pytz coverage - allowlist_externals = make - [testenv:flake-and-version-check] basepython = python3.14 - commands = coverage erase make test-flake make version-check - deps = coverage flake8 +[testenv:py{310,311,312}-django42] +depends = flake-and-version-check + +[testenv:py{310,311,312,313,314}-django52] +depends = flake-and-version-check + +[testenv:py{312,313,314}-django60] +depends = flake-and-version-check [testenv:coverage-report] basepython = python3.14 - +depends = + py{310,311,312}-django42 + py{310,311,312,313,314}-django52 + py{312,313,314}-django60 commands = coverage report --omit='.tox/*' coverage html --omit='.tox/*' - deps = coverage + +[testenv:testpypi] +skip_install = true +passenv = + TESTPYPI_VERSION + DJANGO_TEST_PORT +allowlist_externals = {toxinidir}/scripts/testpypi_integration.sh + +[testenv:testpypi-py{310,311,312}-django42] +labels = testpypi +base = testenv:testpypi +set_env = + py310: SCRIPT_PYTHON=python3.10 + py310: DJANGO_TEST_PORT=52010 + py311: SCRIPT_PYTHON=python3.11 + py311: DJANGO_TEST_PORT=52011 + py312: SCRIPT_PYTHON=python3.12 + py312: DJANGO_TEST_PORT=52012 +commands = {toxinidir}/scripts/testpypi_integration.sh {env:TESTPYPI_VERSION} {env:SCRIPT_PYTHON} 4.2 {env:DJANGO_TEST_PORT} + +[testenv:testpypi-py{310,311,312,313,314}-django52] +labels = testpypi +base = testenv:testpypi +set_env = + py310: SCRIPT_PYTHON=python3.10 + py310: DJANGO_TEST_PORT=52020 + py311: SCRIPT_PYTHON=python3.11 + py311: DJANGO_TEST_PORT=52021 + py312: SCRIPT_PYTHON=python3.12 + py312: DJANGO_TEST_PORT=52022 + py313: SCRIPT_PYTHON=python3.13 + py313: DJANGO_TEST_PORT=52023 + py314: SCRIPT_PYTHON=python3.14 + py314: DJANGO_TEST_PORT=52024 +commands = {toxinidir}/scripts/testpypi_integration.sh {env:TESTPYPI_VERSION} {env:SCRIPT_PYTHON} 5.2 {env:DJANGO_TEST_PORT} + +[testenv:testpypi-py{312,313,314}-django60] +labels = testpypi +base = testenv:testpypi +set_env = + py312: SCRIPT_PYTHON=python3.12 + py312: DJANGO_TEST_PORT=52032 + py313: SCRIPT_PYTHON=python3.13 + py313: DJANGO_TEST_PORT=52033 + py314: SCRIPT_PYTHON=python3.14 + py314: DJANGO_TEST_PORT=52034 +commands = {toxinidir}/scripts/testpypi_integration.sh {env:TESTPYPI_VERSION} {env:SCRIPT_PYTHON} 6.0 {env:DJANGO_TEST_PORT} diff --git a/uv.lock b/uv.lock index afc8f5a..5fde221 100644 --- a/uv.lock +++ b/uv.lock @@ -292,6 +292,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "cryptography" version = "46.0.4" @@ -393,6 +398,8 @@ dev = [ { name = "coverage" }, { name = "flake8" }, { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, { name = "setuptools" }, { name = "tox" }, { name = "tox-uv" }, @@ -410,6 +417,8 @@ dev = [ { name = "coverage" }, { name = "flake8" }, { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, { name = "setuptools" }, { name = "tox" }, { name = "tox-uv" }, @@ -731,6 +740,32 @@ 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-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 = "pytz" version = "2025.2"