diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f6deef..19d4653 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,7 @@ jobs: run: echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Ensure Python 3.12 and sync deps (dev) + working-directory: efile_app run: | uv python install 3.12 uv sync --group dev @@ -39,15 +40,18 @@ jobs: # ruff - name: Ruff format (check only) if: matrix.check == 'ruff' + working-directory: efile_app run: uv run ruff format --check . - name: Ruff lint if: matrix.check == 'ruff' + working-directory: efile_app run: uv run ruff check . # type checking - name: Type check (ty) if: matrix.check == 'type' + working-directory: efile_app run: uv run ty check # tests @@ -55,4 +59,5 @@ jobs: if: matrix.check == 'tests' env: DJANGO_SETTINGS_MODULE: efile.settings + working-directory: efile_app run: uv run pytest -q diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml new file mode 100644 index 0000000..f92687b --- /dev/null +++ b/.github/workflows/fly-deploy.yml @@ -0,0 +1,18 @@ +# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ + +name: Fly Deploy +on: + push: + branches: + - main +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + concurrency: deploy-group # optional: ensure only one action runs at a time + steps: + - uses: actions/checkout@v5 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --remote-only + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/.gitignore b/.gitignore index 846c7d5..75f435f 100644 --- a/.gitignore +++ b/.gitignore @@ -111,6 +111,10 @@ celerybeat.pid # Environments .env .venv +.env.staging +.env.prod +.env.production +.env.local env/ venv/ ENV/ @@ -150,3 +154,7 @@ cython_debug/ # Logs *.log + +# Sqlite database files +efile_app/db.sqlite3 +efile_app/db.sqlite3-journal diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70a7fd8..43fae6d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,22 +12,23 @@ repos: hooks: - id: ty name: ty (type check) - entry: uv run ty check + entry: bash -lc language: system types: [python] - stages: [pre-commit] pass_filenames: false + args: + - cd efile_app && uv run ty check # Run tests on pre-push to keep commits snappy - repo: local hooks: - id: pytest name: pytest (pre-push) - entry: uv run pytest + entry: bash -lc language: system stages: [pre-push] pass_filenames: false args: - - -q + - cd efile_app && uv run pytest -q # If needed, override Django settings here instead of pyproject: # - --ds=efile.settings diff --git a/Dockerfile b/Dockerfile index 86eab2a..2920b10 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,21 +13,29 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN curl -LsSf https://astral.sh/uv/install.sh | sh ENV PATH="/root/.local/bin:${PATH}" -WORKDIR /app +WORKDIR /app/efile_app -# Copy project metadata first for better layer caching -COPY pyproject.toml uv.lock* ./ +# Copy project metadata first for better layer caching (project now rooted at efile_app/) +COPY efile_app/pyproject.toml efile_app/uv.lock* ./ # Install dependencies without installing the project code yet (faster rebuilds) RUN uv sync --frozen --no-install-project -# Copy the rest of the source code -COPY . . +# Copy the rest of the source code into /app +COPY . /app # Install the project itself (editable-like install) RUN uv sync --frozen +# Collect static files at build time so WhiteNoise can serve them at runtime +# Use staging settings to ensure DEBUG=False and STATIC_ROOT is set +# Provide a non-insecure SECRET_KEY for build to satisfy settings_staging +RUN DJANGO_SETTINGS_MODULE=efile.settings_staging \ + DJANGO_SECRET_KEY=build-static-collect-key \ + DATABASE_URL=sqlite:////tmp/build-collectstatic.sqlite3 \ + uv run python manage.py collectstatic --noinput + EXPOSE 8000 -# For Django dev server; override in compose for different commands -CMD ["uv", "run", "python", "efile_app/manage.py", "runserver", "0.0.0.0:8000"] +# Use Gunicorn with Uvicorn workers (ASGI) and log to stdout/stderr (debug level) +CMD ["uv", "run", "gunicorn", "efile.asgi:application", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "60", "--access-logfile", "-", "--error-logfile", "-", "--log-level", "debug", "--access-logformat", "%(h)s - %(l)s %(u)s [%(t)s] \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\" in %(L)ss"] diff --git a/efile_app/efile/__pycache__/__init__.cpython-313.pyc b/efile_app/efile/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index ca7ab24..0000000 Binary files a/efile_app/efile/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/efile_app/efile/__pycache__/forms.cpython-313.pyc b/efile_app/efile/__pycache__/forms.cpython-313.pyc deleted file mode 100644 index c03a880..0000000 Binary files a/efile_app/efile/__pycache__/forms.cpython-313.pyc and /dev/null differ diff --git a/efile_app/efile/__pycache__/settings.cpython-313.pyc b/efile_app/efile/__pycache__/settings.cpython-313.pyc deleted file mode 100644 index 74ff20f..0000000 Binary files a/efile_app/efile/__pycache__/settings.cpython-313.pyc and /dev/null differ diff --git a/efile_app/efile/__pycache__/urls.cpython-313.pyc b/efile_app/efile/__pycache__/urls.cpython-313.pyc deleted file mode 100644 index b7eab29..0000000 Binary files a/efile_app/efile/__pycache__/urls.cpython-313.pyc and /dev/null differ diff --git a/efile_app/efile/__pycache__/views.cpython-313.pyc b/efile_app/efile/__pycache__/views.cpython-313.pyc deleted file mode 100644 index b14e7f5..0000000 Binary files a/efile_app/efile/__pycache__/views.cpython-313.pyc and /dev/null differ diff --git a/efile_app/efile/__pycache__/views_login.cpython-313.pyc b/efile_app/efile/__pycache__/views_login.cpython-313.pyc deleted file mode 100644 index e8d4a02..0000000 Binary files a/efile_app/efile/__pycache__/views_login.cpython-313.pyc and /dev/null differ diff --git a/efile_app/efile/__pycache__/views_register.cpython-313.pyc b/efile_app/efile/__pycache__/views_register.cpython-313.pyc deleted file mode 100644 index 716a35f..0000000 Binary files a/efile_app/efile/__pycache__/views_register.cpython-313.pyc and /dev/null differ diff --git a/efile_app/efile/__pycache__/wsgi.cpython-313.pyc b/efile_app/efile/__pycache__/wsgi.cpython-313.pyc deleted file mode 100644 index cfcefd9..0000000 Binary files a/efile_app/efile/__pycache__/wsgi.cpython-313.pyc and /dev/null differ diff --git a/efile_app/efile/api/auth_views.py b/efile_app/efile/api/auth_views.py index 26f32af..9115935 100644 --- a/efile_app/efile/api/auth_views.py +++ b/efile_app/efile/api/auth_views.py @@ -1,60 +1,63 @@ """ API views for authentication and user management """ -from django.http import JsonResponse -from django.views.decorators.http import require_http_methods -from django.views.decorators.csrf import csrf_exempt -from django.contrib.auth import authenticate, login, logout -from django.contrib.auth.models import User -from django.conf import settings -import requests + import json -import os -from .base import APIResponseMixin, validate_request + +import requests +from django.conf import settings +from django.contrib.auth import authenticate, login, logout +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods +from requests.exceptions import RequestException, Timeout + +from .base import APIResponseMixin class AuthAPIViews(APIResponseMixin): """API views for authentication""" - + @staticmethod def get_state_from_request(request): """Extract state from request URL or parameters""" # Try to get state from query parameters first - state = request.GET.get('state') + state = request.GET.get("state") if state: return state.lower() - + # Try to extract from URL path (e.g., /jurisdictions/illinois/) path = request.path - if '/jurisdictions/' in path: - path_parts = path.split('/jurisdictions/') + if "/jurisdictions/" in path: + path_parts = path.split("/jurisdictions/") if len(path_parts) > 1: - state_part = path_parts[1].split('/')[0] + state_part = path_parts[1].split("/")[0] if state_part: return state_part.lower() - + # Default to Illinois if no state found - return 'illinois' + return "illinois" @staticmethod def get_tyler_token(request, state=None): """Helper method to retrieve Tyler token from various sources""" if state is None: state = AuthAPIViews.get_state_from_request(request) - - auth_tokens = request.session.get('auth_tokens', {}) + + auth_tokens = request.session.get("auth_tokens", {}) print(f"Auth tokens in session: {auth_tokens}") - + # Try different Tyler token key formats - tyler_token = (auth_tokens.get(f'TYLER-TOKEN-{state.upper()}') or - auth_tokens.get(f'tyler_token_{state}') or - auth_tokens.get(f'tyler-token-{state}')) - + tyler_token = ( + auth_tokens.get(f"TYLER-TOKEN-{state.upper()}") + or auth_tokens.get(f"tyler_token_{state}") + or auth_tokens.get(f"tyler-token-{state}") + ) + if tyler_token: return tyler_token - + return None - + @staticmethod @require_http_methods(["POST"]) @csrf_exempt @@ -62,30 +65,28 @@ def user_login(request): """Handle user login""" try: data = json.loads(request.body) - - username = data.get('username') - password = data.get('password') - + + username = data.get("username") + password = data.get("password") + if not username or not password: return AuthAPIViews.error_response("Username and password required") - + user = authenticate(request, username=username, password=password) - + if user is not None: login(request, user) - return AuthAPIViews.success_response({ - 'user_id': user.id, - 'username': user.username, - 'email': user.email - }, "Login successful") + return AuthAPIViews.success_response( + {"user_id": user.id, "username": user.username, "email": user.email}, "Login successful" + ) else: return AuthAPIViews.error_response("Invalid credentials", 401) - + except json.JSONDecodeError: return AuthAPIViews.error_response("Invalid JSON data") except Exception as e: return AuthAPIViews.error_response(f"Error: {str(e)}") - + @staticmethod @require_http_methods(["POST"]) @csrf_exempt @@ -96,7 +97,7 @@ def user_logout(request): return AuthAPIViews.success_response({}, "Logout successful") except Exception as e: return AuthAPIViews.error_response(f"Error: {str(e)}") - + @staticmethod @require_http_methods(["GET"]) def user_profile(request): @@ -107,171 +108,171 @@ def user_profile(request): state = AuthAPIViews.get_state_from_request(request) tyler_token = AuthAPIViews.get_tyler_token(request, state) api_key = getattr(settings, "SUFFOLK_EFILE_API_KEY", None) - + headers = { - 'Content-Type': 'application/json', - 'User-Agent': f'{state.title()}-eFile-Client/1.0', - 'X-API-Key': api_key if api_key else '', + "Content-Type": "application/json", + "User-Agent": f"{state.title()}-eFile-Client/1.0", + "X-API-Key": api_key if api_key else "", } - + # Add Tyler token if available if tyler_token: - headers[f'tyler-token-{state}'] = tyler_token + headers[f"tyler-token-{state}"] = tyler_token else: # Log that no token was found for debugging print(f"Warning: No Tyler token found for state '{state}' in Suffolk eFile API request") - + api_response = requests.get( - f'https://efile-test.suffolklitlab.org/jurisdictions/{state}/firmattorneyservice/firm', + f"https://efile-test.suffolklitlab.org/jurisdictions/{state}/firmattorneyservice/firm", headers=headers, - timeout=10 + timeout=10, ) - + if api_response.status_code == 200: external_data = api_response.json() print(f"External data retrieved: {external_data}") - + # Extract address information from external API response - address_info = external_data.get('address', {}) - address_line1 = address_info.get('addressLine1', '') - address_line2 = address_info.get('addressLine2', '') - city = address_info.get('city', '') - state = address_info.get('state', 'IL') - zip_code = address_info.get('zipCode', '60601') - phone_number = external_data.get('phoneNumber', '') - + address_info = external_data.get("address", {}) + address_line1 = address_info.get("addressLine1", "") + address_line2 = address_info.get("addressLine2", "") + city = address_info.get("city", "") + state = address_info.get("state", "IL") + zip_code = address_info.get("zipCode", "60601") + phone_number = external_data.get("phoneNumber", "") + # Build user profile data combining local and external data user_data = { - 'external_firm_data': external_data, + "external_firm_data": external_data, # Local user data (if authenticated) - 'id': request.user.id if request.user.is_authenticated else None, - 'username': request.user.username if request.user.is_authenticated else 'guest', - 'email': request.user.email if request.user.is_authenticated else request.session.get('user_email'), - 'first_name': request.user.first_name if request.user.is_authenticated else 'Demo', - 'last_name': request.user.last_name if request.user.is_authenticated else 'User', - 'date_joined': request.user.date_joined.isoformat() if request.user.is_authenticated else None, - 'last_login': request.user.last_login.isoformat() if (request.user.is_authenticated and request.user.last_login) else None, + "id": request.user.id if request.user.is_authenticated else None, + "username": request.user.username if request.user.is_authenticated else "guest", + "email": request.user.email + if request.user.is_authenticated + else request.session.get("user_email"), + "first_name": request.user.first_name if request.user.is_authenticated else "Demo", + "last_name": request.user.last_name if request.user.is_authenticated else "User", + "date_joined": request.user.date_joined.isoformat() if request.user.is_authenticated else None, + "last_login": request.user.last_login.isoformat() + if (request.user.is_authenticated and request.user.last_login) + else None, # Address information from external API - 'address': address_line1, - 'address_line2': address_line2, - 'city': city, - 'state': state, - 'zip': zip_code, - 'phone': phone_number, + "address": address_line1, + "address_line2": address_line2, + "city": city, + "state": state, + "zip": zip_code, + "phone": phone_number, # Default location information - 'preferred_county': 'cook', - 'zip_code': zip_code, # Use actual zip from API - 'location': { - 'county': 'Cook County', - 'state': 'Illinois', - 'zip_code': zip_code, - 'available_counties': ['cook', 'dupage', 'kane', 'lake', 'mchenry', 'will'] - } + "preferred_county": "cook", + "zip_code": zip_code, # Use actual zip from API + "location": { + "county": "Cook County", + "state": "Illinois", + "zip_code": zip_code, + "available_counties": ["cook", "dupage", "kane", "lake", "mchenry", "will"], + }, } - + return AuthAPIViews.success_response(user_data) elif api_response.status_code == 401: # API requires authentication - return mock data for demo user_data = { - 'external_api_status': 'requires_authentication', - 'note': 'Suffolk eFile API requires authentication. Using demo data.', - 'id': request.user.id if request.user.is_authenticated else None, - 'username': request.user.username if request.user.is_authenticated else 'demo_user', - 'email': request.user.email if request.user.is_authenticated else request.session.get('user_email', 'demo@example.com'), - 'first_name': request.user.first_name if request.user.is_authenticated else 'John', - 'last_name': request.user.last_name if request.user.is_authenticated else 'Doe', + "external_api_status": "requires_authentication", + "note": "Suffolk eFile API requires authentication. Using demo data.", + "id": request.user.id if request.user.is_authenticated else None, + "username": request.user.username if request.user.is_authenticated else "demo_user", + "email": request.user.email + if request.user.is_authenticated + else request.session.get("user_email", "demo@example.com"), + "first_name": request.user.first_name if request.user.is_authenticated else "John", + "last_name": request.user.last_name if request.user.is_authenticated else "Doe", # Default address information for demo - 'address': '123 Main St', - 'address_line2': '', - 'city': 'Chicago', - 'state': 'IL', - 'zip': '60601', - 'phone': '(312) 555-1234', - 'preferred_county': 'cook', - 'zip_code': '60601', # Downtown Chicago zip for demo - 'location': { - 'county': 'Cook County', - 'state': 'Illinois', - 'zip_code': '60601', - 'available_counties': ['cook', 'dupage', 'kane', 'lake', 'mchenry', 'will'] + "address": "123 Main St", + "address_line2": "", + "city": "Chicago", + "state": "IL", + "zip": "60601", + "phone": "(312) 555-1234", + "preferred_county": "cook", + "zip_code": "60601", # Downtown Chicago zip for demo + "location": { + "county": "Cook County", + "state": "Illinois", + "zip_code": "60601", + "available_counties": ["cook", "dupage", "kane", "lake", "mchenry", "will"], }, # Mock firm data based on Suffolk eFile API structure - 'firm_info': { - 'firm_name': 'Demo Law Firm', - 'firm_id': 'DEMO_001', - 'attorneys': [ + "firm_info": { + "firm_name": "Demo Law Firm", + "firm_id": "DEMO_001", + "attorneys": [ { - 'attorney_id': 'ATT_001', - 'first_name': 'John', - 'last_name': 'Doe', - 'bar_number': '123456', - 'email': 'john.doe@demolaw.com' + "attorney_id": "ATT_001", + "first_name": "John", + "last_name": "Doe", + "bar_number": "123456", + "email": "john.doe@demolaw.com", } - ] - } + ], + }, } - + return AuthAPIViews.success_response(user_data) else: # Fall back to local data if external API fails user_data = { - 'external_api_error': f"Suffolk API returned status {api_response.status_code}", - 'response_text': api_response.text[:200] if api_response.text else 'No response body', - 'id': request.user.id if request.user.is_authenticated else None, - 'username': request.user.username if request.user.is_authenticated else 'guest', - 'email': request.user.email if request.user.is_authenticated else request.session.get('user_email'), - 'first_name': request.user.first_name if request.user.is_authenticated else 'Demo', - 'last_name': request.user.last_name if request.user.is_authenticated else 'User', + "external_api_error": f"Suffolk API returned status {api_response.status_code}", + "response_text": api_response.text[:200] if api_response.text else "No response body", + "id": request.user.id if request.user.is_authenticated else None, + "username": request.user.username if request.user.is_authenticated else "guest", + "email": request.user.email + if request.user.is_authenticated + else request.session.get("user_email"), + "first_name": request.user.first_name if request.user.is_authenticated else "Demo", + "last_name": request.user.last_name if request.user.is_authenticated else "User", # Default address information - 'address': '123 Main St', - 'address_line2': '', - 'city': 'Chicago', - 'state': 'IL', - 'zip': '60601', - 'phone': '(312) 555-1234', - 'preferred_county': 'cook', - 'zip_code': '60601', # Downtown Chicago zip for demo - 'location': { - 'county': 'Cook County', - 'state': 'Illinois', - 'zip_code': '60601' - } + "address": "123 Main St", + "address_line2": "", + "city": "Chicago", + "state": "IL", + "zip": "60601", + "phone": "(312) 555-1234", + "preferred_county": "cook", + "zip_code": "60601", # Downtown Chicago zip for demo + "location": {"county": "Cook County", "state": "Illinois", "zip_code": "60601"}, } - + return AuthAPIViews.success_response(user_data) - - except requests.exceptions.Timeout: + + except Timeout: return AuthAPIViews.error_response("External API request timed out", 408) - except requests.exceptions.RequestException as e: + except RequestException as e: # Fall back to local data if external API is unavailable user_data = { - 'external_api_error': f"Could not connect to Suffolk API: {str(e)}", - 'id': request.user.id if request.user.is_authenticated else None, - 'username': request.user.username if request.user.is_authenticated else 'guest', - 'email': request.user.email if request.user.is_authenticated else request.session.get('user_email'), - 'first_name': request.user.first_name if request.user.is_authenticated else 'Demo', - 'last_name': request.user.last_name if request.user.is_authenticated else 'User', + "external_api_error": f"Could not connect to Suffolk API: {str(e)}", + "id": request.user.id if request.user.is_authenticated else None, + "username": request.user.username if request.user.is_authenticated else "guest", + "email": request.user.email if request.user.is_authenticated else request.session.get("user_email"), + "first_name": request.user.first_name if request.user.is_authenticated else "Demo", + "last_name": request.user.last_name if request.user.is_authenticated else "User", # Default address information - 'address': '123 Main St', - 'address_line2': '', - 'city': 'Chicago', - 'state': 'IL', - 'zip': '60601', - 'phone': '(312) 555-1234', - 'preferred_county': 'cook', - 'zip_code': '60601', # Downtown Chicago zip for demo - 'location': { - 'county': 'Cook County', - 'state': 'Illinois', - 'zip_code': '60601' - } + "address": "123 Main St", + "address_line2": "", + "city": "Chicago", + "state": "IL", + "zip": "60601", + "phone": "(312) 555-1234", + "preferred_county": "cook", + "zip_code": "60601", # Downtown Chicago zip for demo + "location": {"county": "Cook County", "state": "Illinois", "zip_code": "60601"}, } - + return AuthAPIViews.success_response(user_data) - + except Exception as e: return AuthAPIViews.error_response(f"Error: {str(e)}") - + @staticmethod @require_http_methods(["POST"]) @csrf_exempt @@ -279,48 +280,45 @@ def external_auth(request): """Handle authentication with external Suffolk eFile API""" try: data = json.loads(request.body) - - username = data.get('username') - password = data.get('password') - + + username = data.get("username") + password = data.get("password") + if not username or not password: return AuthAPIViews.error_response("Username and password required") - + # Authenticate with Suffolk eFile API state = AuthAPIViews.get_state_from_request(request) auth_response = requests.post( - f'https://efile-test.suffolklitlab.org/jurisdictions/{state}/auth/login', - json={ - 'username': username, - 'password': password - }, - headers={ - 'Content-Type': 'application/json', - 'User-Agent': f'{state.title()}-eFile-Client/1.0' - } + f"https://efile-test.suffolklitlab.org/jurisdictions/{state}/auth/login", + json={"username": username, "password": password}, + headers={"Content-Type": "application/json", "User-Agent": f"{state.title()}-eFile-Client/1.0"}, ) - + if auth_response.status_code == 200: auth_data = auth_response.json() - + # Store auth tokens in session including Tyler token - request.session['auth_tokens'] = { - 'access_token': auth_data.get('access_token'), - 'refresh_token': auth_data.get('refresh_token'), - f'tyler_token_{state}': auth_data.get(f'tyler_token_{state}'), - 'expires_in': auth_data.get('expires_in'), - 'state': state # Store the state for future reference + request.session["auth_tokens"] = { + "access_token": auth_data.get("access_token"), + "refresh_token": auth_data.get("refresh_token"), + f"tyler_token_{state}": auth_data.get(f"tyler_token_{state}"), + "expires_in": auth_data.get("expires_in"), + "state": state, # Store the state for future reference } - - return AuthAPIViews.success_response({ - 'authenticated': True, - 'user': auth_data.get('user', {}), - 'state': state, - 'has_tyler_token': f'tyler_token_{state}' in auth_data - }, "External authentication successful") + + return AuthAPIViews.success_response( + { + "authenticated": True, + "user": auth_data.get("user", {}), + "state": state, + "has_tyler_token": f"tyler_token_{state}" in auth_data, + }, + "External authentication successful", + ) else: return AuthAPIViews.error_response("External authentication failed", 401) - + except json.JSONDecodeError: return AuthAPIViews.error_response("Invalid JSON data") except Exception as e: @@ -333,29 +331,26 @@ def external_profile(request): try: if not request.user.is_authenticated: return AuthAPIViews.error_response("Not authenticated", 401) - + # This could fetch additional data from Suffolk eFile API or other sources # For now, return enhanced location data external_data = { - 'location_details': { - 'county_court_info': { - 'cook': {'address': '50 W Washington St, Chicago, IL', 'phone': '(312) 603-5030'}, - 'dupage': {'address': '505 N County Farm Rd, Wheaton, IL', 'phone': '(630) 407-8700'}, - 'kane': {'address': '37W777 Route 38, St Charles, IL', 'phone': '(630) 232-3413'} + "location_details": { + "county_court_info": { + "cook": {"address": "50 W Washington St, Chicago, IL", "phone": "(312) 603-5030"}, + "dupage": {"address": "505 N County Farm Rd, Wheaton, IL", "phone": "(630) 407-8700"}, + "kane": {"address": "37W777 Route 38, St Charles, IL", "phone": "(630) 232-3413"}, } }, - 'preferences': { - 'language': 'en', - 'timezone': 'America/Chicago', - 'notification_settings': { - 'email': True, - 'sms': False - } - } + "preferences": { + "language": "en", + "timezone": "America/Chicago", + "notification_settings": {"email": True, "sms": False}, + }, } - + return AuthAPIViews.success_response(external_data) - + except Exception as e: return AuthAPIViews.error_response(f"Error: {str(e)}") @@ -369,11 +364,7 @@ def tyler_token(request): tyler_token = AuthAPIViews.get_tyler_token(request, state) api_key = getattr(settings, "SUFFOLK_EFILE_API_KEY", None) - return AuthAPIViews.success_response({ - 'tyler_token': tyler_token, - 'api_key': api_key, - 'state': state - }) + return AuthAPIViews.success_response({"tyler_token": tyler_token, "api_key": api_key, "state": state}) except Exception as e: return AuthAPIViews.error_response(f"Error: {str(e)}") @@ -386,26 +377,26 @@ def payment_accounts(request): state = AuthAPIViews.get_state_from_request(request) tyler_token = AuthAPIViews.get_tyler_token(request, state) api_key = getattr(settings, "SUFFOLK_EFILE_API_KEY", None) - + headers = { - 'Content-Type': 'application/json', - 'User-Agent': f'{state.title()}-eFile-Client/1.0', - 'X-API-Key': api_key if api_key else '', + "Content-Type": "application/json", + "User-Agent": f"{state.title()}-eFile-Client/1.0", + "X-API-Key": api_key if api_key else "", } - + # Add Tyler token if available if tyler_token: - headers[f'tyler-token-{state}'] = tyler_token + headers[f"tyler-token-{state}"] = tyler_token else: # Log that no token was found for debugging print(f"Warning: No Tyler token found for state '{state}' in Suffolk eFile payment accounts request") - + api_response = requests.get( - f'https://efile-test.suffolklitlab.org/jurisdictions/{state}/payments/payment-accounts/', + f"https://efile-test.suffolklitlab.org/jurisdictions/{state}/payments/payment-accounts/", headers=headers, - timeout=10 + timeout=10, ) - + if api_response.status_code == 200: payment_accounts = api_response.json() return AuthAPIViews.success_response(payment_accounts) @@ -415,13 +406,13 @@ def payment_accounts(request): else: # Return error with status info return AuthAPIViews.error_response( - f"Payment accounts API returned status {api_response.status_code}: {api_response.text[:200]}", - api_response.status_code + f"Payment accounts API returned status {api_response.status_code}: {api_response.text[:200]}", + api_response.status_code, ) - - except requests.exceptions.Timeout: + + except Timeout: return AuthAPIViews.error_response("Payment accounts API request timed out", 408) - except requests.exceptions.RequestException as e: + except RequestException as e: return AuthAPIViews.error_response(f"Could not connect to payment accounts API: {str(e)}", 503) except Exception as e: return AuthAPIViews.error_response(f"Error: {str(e)}") diff --git a/efile_app/efile/api/base.py b/efile_app/efile/api/base.py index 3814d9a..a6eadaa 100644 --- a/efile_app/efile/api/base.py +++ b/efile_app/efile/api/base.py @@ -1,48 +1,40 @@ """ Base API views and utilities for Illinois eFile system """ + from django.http import JsonResponse -from django.views.decorators.http import require_http_methods -from django.views.decorators.csrf import csrf_exempt -import json class APIResponseMixin: """Mixin for consistent API responses""" - + @staticmethod def success_response(data, message=None): """Return a successful API response""" - response = { - 'success': True, - 'data': data - } + response = {"success": True, "data": data} if message: - response['message'] = message + response["message"] = message return JsonResponse(response) - + @staticmethod def error_response(error_message, status_code=400): """Return an error API response""" - return JsonResponse({ - 'success': False, - 'error': error_message - }, status=status_code) + return JsonResponse({"success": False, "error": error_message}, status=status_code) def get_auth_tokens(request): """Helper function to get auth tokens from session""" - return request.session.get('auth_tokens', None) + return request.session.get("auth_tokens", None) def validate_request(request, required_params=None): """Validate API request and return any missing parameters""" if required_params is None: required_params = [] - + missing_params = [] for param in required_params: if param not in request.GET and param not in request.POST: missing_params.append(param) - + return missing_params diff --git a/efile_app/efile/api/case_form_views.py b/efile_app/efile/api/case_form_views.py index 488787a..c986a7a 100644 --- a/efile_app/efile/api/case_form_views.py +++ b/efile_app/efile/api/case_form_views.py @@ -3,53 +3,54 @@ Provides case-specific form structure based on document type selection Uses case-type-forms.yaml for dynamic form generation """ -from django.http import JsonResponse + +import os + +import yaml from django.views.decorators.http import require_http_methods + from .base import APIResponseMixin -import yaml -import os class CaseFormAPIViews(APIResponseMixin): """API views for dynamic case form configuration""" - + @staticmethod def _load_case_type_forms(): """Load case type forms configuration from YAML""" try: config_path = os.path.join( - os.path.dirname(os.path.dirname(__file__)), - 'static', 'config', 'case-type-forms.yaml' + os.path.dirname(os.path.dirname(__file__)), "static", "config", "case-type-forms.yaml" ) - - with open(config_path, 'r') as file: + + with open(config_path) as file: return yaml.safe_load(file) except Exception as e: print(f"Error loading case-type-forms.yaml: {e}") return None - + @staticmethod def _find_case_type_by_keywords(case_type_name): """Find case type configuration by matching keywords""" forms_config = CaseFormAPIViews._load_case_type_forms() - if not forms_config or 'case_types' not in forms_config: + if not forms_config or "case_types" not in forms_config: return None - + case_type_name_lower = case_type_name.lower() - + # Try exact match first - if case_type_name_lower in forms_config['case_types']: - return forms_config['case_types'][case_type_name_lower] - + if case_type_name_lower in forms_config["case_types"]: + return forms_config["case_types"][case_type_name_lower] + # Try keyword matching - for case_type_key, case_type_config in forms_config['case_types'].items(): - if 'keywords' in case_type_config: - for keyword in case_type_config['keywords']: + for _case_type_key, case_type_config in forms_config["case_types"].items(): + if "keywords" in case_type_config: + for keyword in case_type_config["keywords"]: if keyword.lower() in case_type_name_lower: return case_type_config - + return None - + @staticmethod @require_http_methods(["GET"]) def get_case_form_config(request): @@ -59,51 +60,51 @@ def get_case_form_config(request): """ try: # Get parameters from request - court_code = request.GET.get('court') - category_id = request.GET.get('category') - case_type_id = request.GET.get('case_type') - filing_type_id = request.GET.get('filing_type') - document_type_id = request.GET.get('document_type') - + court_code = request.GET.get("court") + category_id = request.GET.get("category") + case_type_id = request.GET.get("case_type") + filing_type_id = request.GET.get("filing_type") + document_type_id = request.GET.get("document_type") + if not all([court_code, document_type_id]): - return CaseFormAPIViews.error_response( - "Missing required parameters: court, document_type" - ) - + return CaseFormAPIViews.error_response("Missing required parameters: court, document_type") + # Try to find case type configuration using case_type_id as the name case_config = None if case_type_id: case_config = CaseFormAPIViews._find_case_type_by_keywords(case_type_id) - + # If no specific configuration found, return minimal structure if not case_config: - return CaseFormAPIViews.success_response({ - 'case_info': { - 'category': category_id, - 'case_type': case_type_id, - 'filing_type': filing_type_id, - 'document_type': document_type_id - }, - 'sections': {}, - 'show_parties_section': False, - 'show_services_section': False - }) - + return CaseFormAPIViews.success_response( + { + "case_info": { + "category": category_id, + "case_type": case_type_id, + "filing_type": filing_type_id, + "document_type": document_type_id, + }, + "sections": {}, + "show_parties_section": False, + "show_services_section": False, + } + ) + # Structure the response based on case-type-forms.yaml format form_config = { - 'case_info': { - 'category': category_id, - 'case_type': case_type_id, - 'filing_type': filing_type_id, - 'document_type': document_type_id + "case_info": { + "category": category_id, + "case_type": case_type_id, + "filing_type": filing_type_id, + "document_type": document_type_id, }, - 'sections': case_config.get('sections', {}), - 'show_parties_section': len(case_config.get('sections', {})) > 0, - 'show_services_section': False # Services are handled separately via API + "sections": case_config.get("sections", {}), + "show_parties_section": len(case_config.get("sections", {})) > 0, + "show_services_section": False, # Services are handled separately via API } - + return CaseFormAPIViews.success_response(form_config) - + except Exception as e: return CaseFormAPIViews.error_response(f"Error retrieving case form configuration: {str(e)}") diff --git a/efile_app/efile/api/case_type_config.py b/efile_app/efile/api/case_type_config.py index f41be4a..dd7f987 100644 --- a/efile_app/efile/api/case_type_config.py +++ b/efile_app/efile/api/case_type_config.py @@ -1,9 +1,9 @@ +import os + import yaml -import json -from django.http import JsonResponse -from django.views.decorators.cache import cache_page from django.conf import settings -import os +from django.http import JsonResponse + # @cache_page(60 * 15) # Cache for 15 minutes - temporarily disabled for development def get_case_type_config(request): @@ -11,30 +11,18 @@ def get_case_type_config(request): Serves the case type configuration from YAML file as JSON """ try: - config_path = os.path.join(settings.BASE_DIR, 'efile', 'static', 'config', 'case-type-forms.yaml') - - with open(config_path, 'r', encoding='utf-8') as file: + config_path = os.path.join(settings.BASE_DIR, "efile", "static", "config", "case-type-forms.yaml") + + with open(config_path, encoding="utf-8") as file: config_data = yaml.safe_load(file) - - return JsonResponse({ - 'success': True, - 'config': config_data - }) - + + return JsonResponse({"success": True, "config": config_data}) + except FileNotFoundError: - return JsonResponse({ - 'success': False, - 'error': 'Configuration file not found' - }, status=404) - + return JsonResponse({"success": False, "error": "Configuration file not found"}, status=404) + except yaml.YAMLError as e: - return JsonResponse({ - 'success': False, - 'error': f'YAML parsing error: {str(e)}' - }, status=500) - + return JsonResponse({"success": False, "error": f"YAML parsing error: {str(e)}"}, status=500) + except Exception as e: - return JsonResponse({ - 'success': False, - 'error': f'Unexpected error: {str(e)}' - }, status=500) + return JsonResponse({"success": False, "error": f"Unexpected error: {str(e)}"}, status=500) diff --git a/efile_app/efile/api/config_views.py b/efile_app/efile/api/config_views.py index 998bf11..f5a9a31 100644 --- a/efile_app/efile/api/config_views.py +++ b/efile_app/efile/api/config_views.py @@ -1,90 +1,92 @@ """ API views for form configuration based on case-type-forms.yaml """ -from django.http import JsonResponse -from django.views.decorators.http import require_http_methods -from .base import APIResponseMixin, get_auth_tokens, validate_request -import yaml + import os +import yaml +from django.views.decorators.http import require_http_methods + +from .base import APIResponseMixin + class ConfigAPIViews(APIResponseMixin): """API views for form configuration""" - + @staticmethod def _load_case_type_forms(): """Load case type forms configuration from YAML""" try: config_path = os.path.join( - os.path.dirname(os.path.dirname(__file__)), - 'static', 'config', 'case-type-forms.yaml' + os.path.dirname(os.path.dirname(__file__)), "static", "config", "case-type-forms.yaml" ) - - with open(config_path, 'r') as file: + + with open(config_path) as file: return yaml.safe_load(file) except Exception as e: print(f"Error loading case-type-forms.yaml: {e}") return None - + @staticmethod def _find_case_type_by_keywords(case_type_name): """Find case type configuration by matching keywords""" forms_config = ConfigAPIViews._load_case_type_forms() - if not forms_config or 'case_types' not in forms_config: + if not forms_config or "case_types" not in forms_config: return None - + case_type_name_lower = case_type_name.lower() - + # Try exact match first - if case_type_name_lower in forms_config['case_types']: - return forms_config['case_types'][case_type_name_lower] - + if case_type_name_lower in forms_config["case_types"]: + return forms_config["case_types"][case_type_name_lower] + # Try keyword matching - for case_type_key, case_type_config in forms_config['case_types'].items(): - if 'keywords' in case_type_config: - for keyword in case_type_config['keywords']: + for _case_type_key, case_type_config in forms_config["case_types"].items(): + if "keywords" in case_type_config: + for keyword in case_type_config["keywords"]: if keyword.lower() in case_type_name_lower: return case_type_config - + return None - + @staticmethod @require_http_methods(["GET"]) def get_form_config(request): """Get complete form configuration for a specific filing type""" try: # Get required parameters - category_id = request.GET.get('category') - case_type_id = request.GET.get('case_type') - filing_type_id = request.GET.get('filing_type') - + # NOTE: temporarily commenting out the unused variables + # category_id = request.GET.get("category") + case_type_id = request.GET.get("case_type") + # filing_type_id = request.GET.get("filing_type") + if not case_type_id: - return ConfigAPIViews.error_response( - "Missing required parameter: case_type" - ) - + return ConfigAPIViews.error_response("Missing required parameter: case_type") + # Try to find case type configuration using case_type_id as the name case_config = ConfigAPIViews._find_case_type_by_keywords(case_type_id) - + # If no specific configuration found, return minimal structure if not case_config: - return ConfigAPIViews.success_response({ - 'sections': {}, - 'is_name_change': 'name change' in case_type_id.lower(), - 'case_type_name': case_type_id, - 'has_parties': False - }) - + return ConfigAPIViews.success_response( + { + "sections": {}, + "is_name_change": "name change" in case_type_id.lower(), + "case_type_name": case_type_id, + "has_parties": False, + } + ) + # Structure the response based on case-type-forms.yaml format config = { - 'sections': case_config.get('sections', {}), - 'is_name_change': 'name change' in case_type_id.lower(), - 'case_type_name': case_type_id, - 'has_parties': len(case_config.get('sections', {})) > 0 + "sections": case_config.get("sections", {}), + "is_name_change": "name change" in case_type_id.lower(), + "case_type_name": case_type_id, + "has_parties": len(case_config.get("sections", {})) > 0, } - + return ConfigAPIViews.success_response(config) - + except Exception as e: return ConfigAPIViews.error_response(f"Error: {str(e)}") diff --git a/efile_app/efile/api/dropdown_views.py b/efile_app/efile/api/dropdown_views.py index 987459f..0f9bfba 100644 --- a/efile_app/efile/api/dropdown_views.py +++ b/efile_app/efile/api/dropdown_views.py @@ -3,169 +3,167 @@ Handles cascading dropdowns for case categories, types, counties, etc. Uses GET requests to external APIs exclusively. """ -from django.http import JsonResponse -from django.views.decorators.http import require_http_methods + import requests -from .base import APIResponseMixin, get_auth_tokens, validate_request +from django.views.decorators.http import require_http_methods + from ..utils.zip_to_county_il import get_county_by_zip +from .base import APIResponseMixin, get_auth_tokens class DropdownAPIViews(APIResponseMixin): """API views for dropdown data""" - + @staticmethod @require_http_methods(["GET"]) def get_case_categories(request): """Get available case categories from Suffolk LIT Lab API""" try: auth_tokens = get_auth_tokens(request) - + # Get required parameters - court_code = request.GET.get('court') - jurisdiction = request.GET.get('jurisdiction', 'illinois') - + court_code = request.GET.get("court") + jurisdiction = request.GET.get("jurisdiction", "illinois") + if not court_code: return DropdownAPIViews.error_response("Missing required court parameter") - + # Make API call to external categories endpoint api_url = f"https://efile-test.suffolklitlab.org/jurisdictions/{jurisdiction}/codes/courts/{court_code}/categories" - + # Make the API request with auth tokens if available headers = {} - if auth_tokens and 'token' in auth_tokens: - headers['Authorization'] = f"Bearer {auth_tokens['token']}" - + if auth_tokens and "token" in auth_tokens: + headers["Authorization"] = f"Bearer {auth_tokens['token']}" + response = requests.get(api_url, headers=headers, timeout=10) - + if response.status_code == 200: # Parse the API response - expecting list of {name, code} objects api_data = response.json() - + # Transform API data to our dropdown format categories = [] if isinstance(api_data, list): for category in api_data: - if isinstance(category, dict) and 'code' in category and 'name' in category: - categories.append({ - 'value': category['code'], - 'text': f"{category['name']} ({category['code']})" - }) - + if isinstance(category, dict) and "code" in category and "name" in category: + categories.append( + {"value": category["code"], "text": f"{category['name']} ({category['code']})"} + ) + return DropdownAPIViews.success_response(categories) else: return DropdownAPIViews.error_response(f"API request failed with status {response.status_code}") - + except (requests.RequestException, requests.Timeout) as api_error: return DropdownAPIViews.error_response(f"API request failed: {str(api_error)}") except Exception as e: return DropdownAPIViews.error_response(f"Error: {str(e)}") - + @staticmethod @require_http_methods(["GET"]) def get_case_types(request): """Get case types based on selected category from Suffolk LIT Lab API""" try: auth_tokens = get_auth_tokens(request) - + # Get required parameters - court_code = request.GET.get('court') - category_id = request.GET.get('parent') # category_id from case category dropdown - jurisdiction = request.GET.get('jurisdiction', 'illinois') - + court_code = request.GET.get("court") + category_id = request.GET.get("parent") # category_id from case category dropdown + jurisdiction = request.GET.get("jurisdiction", "illinois") + if not court_code: return DropdownAPIViews.error_response("Missing required court parameter") - + if not category_id: return DropdownAPIViews.error_response("Missing required category_id parameter") - + # Make API call to external case types endpoint api_url = f"https://efile-test.suffolklitlab.org/jurisdictions/{jurisdiction}/codes/courts/{court_code}/case_types/?category_id={category_id}" - + # Make the API request with auth tokens if available headers = {} - if auth_tokens and 'token' in auth_tokens: - headers['Authorization'] = f"Bearer {auth_tokens['token']}" - + if auth_tokens and "token" in auth_tokens: + headers["Authorization"] = f"Bearer {auth_tokens['token']}" + response = requests.get(api_url, headers=headers, timeout=10) - + if response.status_code == 200: # Parse the API response - expecting list of {name, code} objects api_data = response.json() - + # Transform API data to our dropdown format case_types = [] if isinstance(api_data, list): for case_type in api_data: - if isinstance(case_type, dict) and 'code' in case_type and 'name' in case_type: - case_types.append({ - 'value': case_type['code'], - 'text': f"{case_type['name']} ({case_type['code']})" - }) - + if isinstance(case_type, dict) and "code" in case_type and "name" in case_type: + case_types.append( + {"value": case_type["code"], "text": f"{case_type['name']} ({case_type['code']})"} + ) + return DropdownAPIViews.success_response(case_types) else: return DropdownAPIViews.error_response(f"API request failed with status {response.status_code}") - + except (requests.RequestException, requests.Timeout) as api_error: return DropdownAPIViews.error_response(f"API request failed: {str(api_error)}") except Exception as e: return DropdownAPIViews.error_response(f"Error: {str(e)}") - + @staticmethod @require_http_methods(["GET"]) def get_filing_types(request): """Get filing types based on selected case type from Suffolk LIT Lab API""" try: auth_tokens = get_auth_tokens(request) - + # Get required parameters - court_code = request.GET.get('court') - casetype_id = request.GET.get('parent') - jurisdiction = request.GET.get('jurisdiction', 'illinois') - current_case_id = request.GET.get('current_case_id') - + court_code = request.GET.get("court") + casetype_id = request.GET.get("parent") + jurisdiction = request.GET.get("jurisdiction", "illinois") + current_case_id = request.GET.get("current_case_id") + # Set initial flag: True if no current case, False if there is a current case - initial = 'True' if not current_case_id else 'False' - + initial = "True" if not current_case_id else "False" + if not court_code: return DropdownAPIViews.error_response("Missing required court parameter") - + if not casetype_id: return DropdownAPIViews.error_response("Missing required case type parameter") - + # Make API call to external filing types endpoint api_url = f"https://efile-test.suffolklitlab.org/jurisdictions/{jurisdiction}/codes/courts/{court_code}/filing_types/?initial={initial}&category_id={casetype_id}" - + # Make the API request with auth tokens if available headers = {} - if auth_tokens and 'token' in auth_tokens: - headers['Authorization'] = f"Bearer {auth_tokens['token']}" - + if auth_tokens and "token" in auth_tokens: + headers["Authorization"] = f"Bearer {auth_tokens['token']}" + response = requests.get(api_url, headers=headers, timeout=10) - + if response.status_code == 200: # Parse the API response - expecting list of {name, code} objects api_data = response.json() - + # Transform API data to our dropdown format filing_types = [] if isinstance(api_data, list): for filing_type in api_data: - if isinstance(filing_type, dict) and 'code' in filing_type and 'name' in filing_type: - filing_types.append({ - 'value': filing_type['code'], - 'text': f"{filing_type['name']} ({filing_type['code']})" - }) - + if isinstance(filing_type, dict) and "code" in filing_type and "name" in filing_type: + filing_types.append( + {"value": filing_type["code"], "text": f"{filing_type['name']} ({filing_type['code']})"} + ) + return DropdownAPIViews.success_response(filing_types) else: return DropdownAPIViews.error_response(f"API request failed with status {response.status_code}") - + except (requests.RequestException, requests.Timeout) as api_error: return DropdownAPIViews.error_response(f"API request failed: {str(api_error)}") except Exception as e: return DropdownAPIViews.error_response(f"Error: {str(e)}") - + @staticmethod @require_http_methods(["GET"]) def get_courts(request): @@ -173,40 +171,37 @@ def get_courts(request): try: auth_tokens = get_auth_tokens(request) - jurisdiction = request.GET.get('jurisdiction', 'illinois') - user_zip = request.GET.get('user_zip') - user_county = request.GET.get('user_county') + jurisdiction = request.GET.get("jurisdiction", "illinois") + user_zip = request.GET.get("user_zip") + user_county = request.GET.get("user_county") # Make API call to external jurisdiction endpoint api_url = f"https://efile.suffolklitlab.org/jurisdictions/{jurisdiction}/codes/courts/?with_names=True" - + try: # Make the API request with auth tokens if available headers = {} - if auth_tokens and 'token' in auth_tokens: - headers['Authorization'] = f"Bearer {auth_tokens['token']}" - + if auth_tokens and "token" in auth_tokens: + headers["Authorization"] = f"Bearer {auth_tokens['token']}" + response = requests.get(api_url, headers=headers, timeout=10) - + if response.status_code == 200: # Parse the API response - expecting list of {name, code} objects api_data = response.json() - + # Transform API data to our dropdown format courts = [] if isinstance(api_data, list): for court in api_data: - if isinstance(court, dict) and 'code' in court and 'name' in court: + if isinstance(court, dict) and "code" in court and "name" in court: # Filter out courts with unwanted patterns in the name - court_name = court['name'] - if any(pattern in court_name for pattern in ['(zOdyssey)', 'z -', 'zz']): + court_name = court["name"] + if any(pattern in court_name for pattern in ["(zOdyssey)", "z -", "zz"]): continue # Skip this court - - courts.append({ - 'value': court['code'], - 'text': court['name'] - }) - + + courts.append({"value": court["code"], "text": court["name"]}) + # If we got courts from the API, return them with user location priority if courts: return DropdownAPIViews.success_response( @@ -215,136 +210,136 @@ def get_courts(request): else: # If no courts found in API response, fall back to hardcoded data raise requests.RequestException("No courts found in API response") - + else: # API call failed, fall back to hardcoded Illinois courts raise requests.RequestException(f"API returned status {response.status_code}") - - except (requests.RequestException, requests.Timeout) as api_error: + + except (requests.RequestException, requests.Timeout): # Fallback to comprehensive Illinois courts if API fails fallback_courts = [ - {'value': 'PSUPCRT', 'text': 'Supreme Court of Illinois'}, - {'value': 'PAC1', 'text': 'Appellate Court – 1st District'}, - {'value': 'PAC2', 'text': 'Appellate Court – 2nd District'}, - {'value': 'PAC3', 'text': 'Appellate Court – 3rd District'}, - {'value': 'PAC4', 'text': 'Appellate Court – 4th District'}, - {'value': 'PAC5', 'text': 'Appellate Court – 5th District'}, - {'value': 'ardc', 'text': 'ARDC Clerk\'s Office'}, - {'value': 'adams', 'text': 'Adams County'}, - {'value': 'alexander', 'text': 'Alexander County'}, - {'value': 'bond', 'text': 'Bond County'}, - {'value': 'boone', 'text': 'Boone County'}, - {'value': 'brown', 'text': 'Brown County'}, - {'value': 'bureau', 'text': 'Bureau County'}, - {'value': 'calhoun', 'text': 'Calhoun County'}, - {'value': 'carroll', 'text': 'Carroll County'}, - {'value': 'cass', 'text': 'Cass County'}, - {'value': 'champaign', 'text': 'Champaign County'}, - {'value': 'christian', 'text': 'Christian County'}, - {'value': 'clark', 'text': 'Clark County'}, - {'value': 'clay', 'text': 'Clay County'}, - {'value': 'clinton', 'text': 'Clinton County'}, - {'value': 'coles', 'text': 'Coles County'}, - {'value': 'cook:chd1', 'text': 'Cook County - Chancery - District 1 - Chicago'}, - {'value': 'cook:cd1', 'text': 'Cook County - County Division - District 1 - Chicago'}, - {'value': 'cook:crd1', 'text': 'Cook County - Criminal - District 1 - Chicago'}, - {'value': 'cook:dr1', 'text': 'Cook County - Domestic Relations - District 1 - Chicago'}, - {'value': 'cook:law1', 'text': 'Cook County - Law - District 1 - Chicago'}, - {'value': 'cook:cvd1', 'text': 'Cook County - Municipal Civil - District 1 - Chicago'}, - {'value': 'cook:pr1', 'text': 'Cook County - Probate - District 1 - Chicago'}, - {'value': 'crawford', 'text': 'Crawford County'}, - {'value': 'cumberland', 'text': 'Cumberland County'}, - {'value': 'dewitt', 'text': 'De Witt County'}, - {'value': 'dekalb', 'text': 'DeKalb County'}, - {'value': 'douglas', 'text': 'Douglas County'}, - {'value': 'dupage', 'text': 'DuPage County'}, - {'value': 'edgar', 'text': 'Edgar County'}, - {'value': 'edwards', 'text': 'Edwards County'}, - {'value': 'effingham', 'text': 'Effingham County'}, - {'value': 'fayette', 'text': 'Fayette County'}, - {'value': 'ford', 'text': 'Ford County'}, - {'value': 'franklin', 'text': 'Franklin County'}, - {'value': 'fulton', 'text': 'Fulton County'}, - {'value': 'gallatin', 'text': 'Gallatin County'}, - {'value': 'greene', 'text': 'Greene County'}, - {'value': 'grundy', 'text': 'Grundy County'}, - {'value': 'hamilton', 'text': 'Hamilton County'}, - {'value': 'hancock', 'text': 'Hancock County'}, - {'value': 'hardin', 'text': 'Hardin County'}, - {'value': 'henderson', 'text': 'Henderson County'}, - {'value': 'henry', 'text': 'Henry County'}, - {'value': 'iroquois', 'text': 'Iroquois County'}, - {'value': 'jackson', 'text': 'Jackson County'}, - {'value': 'jasper', 'text': 'Jasper County'}, - {'value': 'jefferson', 'text': 'Jefferson County'}, - {'value': 'jersey', 'text': 'Jersey County'}, - {'value': 'jodaviess', 'text': 'Jo Daviess County'}, - {'value': 'johnson', 'text': 'Johnson County'}, - {'value': 'kane', 'text': 'Kane County'}, - {'value': 'KankakeeCV', 'text': 'Kankakee - Civil'}, - {'value': 'KankakeeCR', 'text': 'Kankakee - Criminal'}, - {'value': 'KankakeeFAM', 'text': 'Kankakee - Family and Juvenile'}, - {'value': 'KankakeeTR', 'text': 'Kankakee - Traffic'}, - {'value': 'kendall', 'text': 'Kendall County'}, - {'value': 'knox', 'text': 'Knox County'}, - {'value': 'lasalle', 'text': 'LaSalle County'}, - {'value': 'lake', 'text': 'Lake County'}, - {'value': 'lawrence', 'text': 'Lawrence County'}, - {'value': 'lee', 'text': 'Lee County'}, - {'value': 'livingston', 'text': 'Livingston County'}, - {'value': 'logan', 'text': 'Logan County'}, - {'value': 'macon', 'text': 'Macon County'}, - {'value': 'macoupin', 'text': 'Macoupin County'}, - {'value': 'madison', 'text': 'Madison County'}, - {'value': 'marion', 'text': 'Marion County'}, - {'value': 'marshall', 'text': 'Marshall County'}, - {'value': 'mason', 'text': 'Mason County'}, - {'value': 'massac', 'text': 'Massac County'}, - {'value': 'mcdonough', 'text': 'McDonough County'}, - {'value': 'mchenry', 'text': 'McHenry County'}, - {'value': 'mclean', 'text': 'McLean County'}, - {'value': 'menard', 'text': 'Menard County'}, - {'value': 'mercer', 'text': 'Mercer County'}, - {'value': 'monroe', 'text': 'Monroe County'}, - {'value': 'montgomery', 'text': 'Montgomery County'}, - {'value': 'morgan', 'text': 'Morgan County'}, - {'value': 'moultrie', 'text': 'Moultrie County'}, - {'value': 'ogle', 'text': 'Ogle County'}, - {'value': 'peoria', 'text': 'Peoria County'}, - {'value': 'peoriacr', 'text': 'Peoria CR'}, - {'value': 'peoriacs', 'text': 'Peoria CS'}, - {'value': 'peoriatr', 'text': 'Peoria TR'}, - {'value': 'perry', 'text': 'Perry County'}, - {'value': 'piatt', 'text': 'Piatt County'}, - {'value': 'pike', 'text': 'Pike County'}, - {'value': 'pope', 'text': 'Pope County'}, - {'value': 'pulaski', 'text': 'Pulaski County'}, - {'value': 'putnam', 'text': 'Putnam County'}, - {'value': 'randolph', 'text': 'Randolph County'}, - {'value': 'richland', 'text': 'Richland County'}, - {'value': 'rockisland', 'text': 'Rock Island County'}, - {'value': 'saline', 'text': 'Saline County'}, - {'value': 'sangamon', 'text': 'Sangamon County'}, - {'value': 'schuyler', 'text': 'Schuyler County'}, - {'value': 'scott', 'text': 'Scott County'}, - {'value': 'shelby', 'text': 'Shelby County'}, - {'value': 'stclair', 'text': 'St. Clair County'}, - {'value': 'stark', 'text': 'Stark County'}, - {'value': 'stephenson', 'text': 'Stephenson County'}, - {'value': 'tazewell', 'text': 'Tazewell County'}, - {'value': 'tazewell:tr', 'text': 'Tazewell County - Traffic'}, - {'value': 'union', 'text': 'Union County'}, - {'value': 'vermilion', 'text': 'Vermilion County'}, - {'value': 'wabash', 'text': 'Wabash County'}, - {'value': 'warren', 'text': 'Warren County'}, - {'value': 'washington', 'text': 'Washington County'}, - {'value': 'wayne', 'text': 'Wayne County'}, - {'value': 'white', 'text': 'White County'}, - {'value': 'whiteside', 'text': 'Whiteside County'}, - {'value': 'will', 'text': 'Will County'}, - {'value': 'williamson', 'text': 'Williamson County'}, - {'value': 'winnebago', 'text': 'Winnebago County'}, - {'value': 'woodford', 'text': 'Woodford County'} + {"value": "PSUPCRT", "text": "Supreme Court of Illinois"}, + {"value": "PAC1", "text": "Appellate Court – 1st District"}, + {"value": "PAC2", "text": "Appellate Court – 2nd District"}, + {"value": "PAC3", "text": "Appellate Court – 3rd District"}, + {"value": "PAC4", "text": "Appellate Court – 4th District"}, + {"value": "PAC5", "text": "Appellate Court – 5th District"}, + {"value": "ardc", "text": "ARDC Clerk's Office"}, + {"value": "adams", "text": "Adams County"}, + {"value": "alexander", "text": "Alexander County"}, + {"value": "bond", "text": "Bond County"}, + {"value": "boone", "text": "Boone County"}, + {"value": "brown", "text": "Brown County"}, + {"value": "bureau", "text": "Bureau County"}, + {"value": "calhoun", "text": "Calhoun County"}, + {"value": "carroll", "text": "Carroll County"}, + {"value": "cass", "text": "Cass County"}, + {"value": "champaign", "text": "Champaign County"}, + {"value": "christian", "text": "Christian County"}, + {"value": "clark", "text": "Clark County"}, + {"value": "clay", "text": "Clay County"}, + {"value": "clinton", "text": "Clinton County"}, + {"value": "coles", "text": "Coles County"}, + {"value": "cook:chd1", "text": "Cook County - Chancery - District 1 - Chicago"}, + {"value": "cook:cd1", "text": "Cook County - County Division - District 1 - Chicago"}, + {"value": "cook:crd1", "text": "Cook County - Criminal - District 1 - Chicago"}, + {"value": "cook:dr1", "text": "Cook County - Domestic Relations - District 1 - Chicago"}, + {"value": "cook:law1", "text": "Cook County - Law - District 1 - Chicago"}, + {"value": "cook:cvd1", "text": "Cook County - Municipal Civil - District 1 - Chicago"}, + {"value": "cook:pr1", "text": "Cook County - Probate - District 1 - Chicago"}, + {"value": "crawford", "text": "Crawford County"}, + {"value": "cumberland", "text": "Cumberland County"}, + {"value": "dewitt", "text": "De Witt County"}, + {"value": "dekalb", "text": "DeKalb County"}, + {"value": "douglas", "text": "Douglas County"}, + {"value": "dupage", "text": "DuPage County"}, + {"value": "edgar", "text": "Edgar County"}, + {"value": "edwards", "text": "Edwards County"}, + {"value": "effingham", "text": "Effingham County"}, + {"value": "fayette", "text": "Fayette County"}, + {"value": "ford", "text": "Ford County"}, + {"value": "franklin", "text": "Franklin County"}, + {"value": "fulton", "text": "Fulton County"}, + {"value": "gallatin", "text": "Gallatin County"}, + {"value": "greene", "text": "Greene County"}, + {"value": "grundy", "text": "Grundy County"}, + {"value": "hamilton", "text": "Hamilton County"}, + {"value": "hancock", "text": "Hancock County"}, + {"value": "hardin", "text": "Hardin County"}, + {"value": "henderson", "text": "Henderson County"}, + {"value": "henry", "text": "Henry County"}, + {"value": "iroquois", "text": "Iroquois County"}, + {"value": "jackson", "text": "Jackson County"}, + {"value": "jasper", "text": "Jasper County"}, + {"value": "jefferson", "text": "Jefferson County"}, + {"value": "jersey", "text": "Jersey County"}, + {"value": "jodaviess", "text": "Jo Daviess County"}, + {"value": "johnson", "text": "Johnson County"}, + {"value": "kane", "text": "Kane County"}, + {"value": "KankakeeCV", "text": "Kankakee - Civil"}, + {"value": "KankakeeCR", "text": "Kankakee - Criminal"}, + {"value": "KankakeeFAM", "text": "Kankakee - Family and Juvenile"}, + {"value": "KankakeeTR", "text": "Kankakee - Traffic"}, + {"value": "kendall", "text": "Kendall County"}, + {"value": "knox", "text": "Knox County"}, + {"value": "lasalle", "text": "LaSalle County"}, + {"value": "lake", "text": "Lake County"}, + {"value": "lawrence", "text": "Lawrence County"}, + {"value": "lee", "text": "Lee County"}, + {"value": "livingston", "text": "Livingston County"}, + {"value": "logan", "text": "Logan County"}, + {"value": "macon", "text": "Macon County"}, + {"value": "macoupin", "text": "Macoupin County"}, + {"value": "madison", "text": "Madison County"}, + {"value": "marion", "text": "Marion County"}, + {"value": "marshall", "text": "Marshall County"}, + {"value": "mason", "text": "Mason County"}, + {"value": "massac", "text": "Massac County"}, + {"value": "mcdonough", "text": "McDonough County"}, + {"value": "mchenry", "text": "McHenry County"}, + {"value": "mclean", "text": "McLean County"}, + {"value": "menard", "text": "Menard County"}, + {"value": "mercer", "text": "Mercer County"}, + {"value": "monroe", "text": "Monroe County"}, + {"value": "montgomery", "text": "Montgomery County"}, + {"value": "morgan", "text": "Morgan County"}, + {"value": "moultrie", "text": "Moultrie County"}, + {"value": "ogle", "text": "Ogle County"}, + {"value": "peoria", "text": "Peoria County"}, + {"value": "peoriacr", "text": "Peoria CR"}, + {"value": "peoriacs", "text": "Peoria CS"}, + {"value": "peoriatr", "text": "Peoria TR"}, + {"value": "perry", "text": "Perry County"}, + {"value": "piatt", "text": "Piatt County"}, + {"value": "pike", "text": "Pike County"}, + {"value": "pope", "text": "Pope County"}, + {"value": "pulaski", "text": "Pulaski County"}, + {"value": "putnam", "text": "Putnam County"}, + {"value": "randolph", "text": "Randolph County"}, + {"value": "richland", "text": "Richland County"}, + {"value": "rockisland", "text": "Rock Island County"}, + {"value": "saline", "text": "Saline County"}, + {"value": "sangamon", "text": "Sangamon County"}, + {"value": "schuyler", "text": "Schuyler County"}, + {"value": "scott", "text": "Scott County"}, + {"value": "shelby", "text": "Shelby County"}, + {"value": "stclair", "text": "St. Clair County"}, + {"value": "stark", "text": "Stark County"}, + {"value": "stephenson", "text": "Stephenson County"}, + {"value": "tazewell", "text": "Tazewell County"}, + {"value": "tazewell:tr", "text": "Tazewell County - Traffic"}, + {"value": "union", "text": "Union County"}, + {"value": "vermilion", "text": "Vermilion County"}, + {"value": "wabash", "text": "Wabash County"}, + {"value": "warren", "text": "Warren County"}, + {"value": "washington", "text": "Washington County"}, + {"value": "wayne", "text": "Wayne County"}, + {"value": "white", "text": "White County"}, + {"value": "whiteside", "text": "Whiteside County"}, + {"value": "will", "text": "Will County"}, + {"value": "williamson", "text": "Williamson County"}, + {"value": "winnebago", "text": "Winnebago County"}, + {"value": "woodford", "text": "Woodford County"}, ] return DropdownAPIViews.success_response( DropdownAPIViews._prioritize_courts_by_location(fallback_courts, user_zip, user_county) @@ -352,7 +347,7 @@ def get_courts(request): except Exception as e: return DropdownAPIViews.error_response(f"Error: {str(e)}") - + @staticmethod def _prioritize_courts_by_location(courts, user_zip=None, user_county=None): """ @@ -361,103 +356,105 @@ def _prioritize_courts_by_location(courts, user_zip=None, user_county=None): """ if not courts: return courts - + # Determine user county from zip code if provided target_county = user_county if user_zip and not target_county: target_county = get_county_by_zip(user_zip) - + if not target_county: return courts - + # Normalize county name for matching (lowercase, no spaces) - target_county_normalized = target_county.lower().replace(' ', '').replace('county', '') - + target_county_normalized = target_county.lower().replace(" ", "").replace("county", "") + # Create prioritized list prioritized_courts = [] other_courts = [] - + for court in courts: - court_value = court.get('value', '').lower() - court_text = court.get('text', '').lower() - + court_value = court.get("value", "").lower() + court_text = court.get("text", "").lower() + # Check if this court matches the user's county is_match = False - + # Direct value match (e.g., 'cook' matches 'cook') if court_value == target_county_normalized: is_match = True - + # Text match (e.g., 'Cook County' matches 'cook') elif target_county_normalized in court_text: is_match = True - + # Special handling for Cook County divisions - elif target_county_normalized == 'cook' and 'cook:' in court_value: + elif target_county_normalized == "cook" and "cook:" in court_value: is_match = True - + if is_match: # Mark as default/recommended court with recommended text court_copy = court.copy() - court_copy['text'] = f"{court['text']} (Recommended)" + court_copy["text"] = f"{court['text']} (Recommended)" prioritized_courts.append(court_copy) else: other_courts.append(court) - + # Mark only the first prioritized court as selected/default final_courts = prioritized_courts + other_courts if prioritized_courts: # Mark the first recommended court as selected using multiple flag approaches - final_courts[0]['selected'] = True - final_courts[0]['default'] = True - final_courts[0]['recommended'] = True - final_courts[0]['isSelected'] = True # Alternative property name - + final_courts[0]["selected"] = True + final_courts[0]["default"] = True + final_courts[0]["recommended"] = True + final_courts[0]["isSelected"] = True # Alternative property name + return final_courts - + @staticmethod @require_http_methods(["GET"]) def get_document_types(request): """Get document types based on selected filing type from Suffolk LIT Lab API""" try: auth_tokens = get_auth_tokens(request) - + # Get required parameters - court_code = request.GET.get('court') - filing_type_id = request.GET.get('parent') # filing type ID from filing type dropdown - jurisdiction = request.GET.get('jurisdiction', 'illinois') - + court_code = request.GET.get("court") + filing_type_id = request.GET.get("parent") # filing type ID from filing type dropdown + jurisdiction = request.GET.get("jurisdiction", "illinois") + if not court_code: return DropdownAPIViews.error_response("Missing required court parameter") - + if not filing_type_id: return DropdownAPIViews.error_response("Missing required filing type parameter") - + # Make API call to external document types endpoint api_url = f"https://efile-test.suffolklitlab.org/jurisdictions/{jurisdiction}/codes/courts/{court_code}/filing_types/{filing_type_id}/document_types" print(f"API URL: {api_url}") # Debugging line - + # Make the API request with auth tokens if available headers = {} - if auth_tokens and 'token' in auth_tokens: - headers['Authorization'] = f"Bearer {auth_tokens['token']}" - + if auth_tokens and "token" in auth_tokens: + headers["Authorization"] = f"Bearer {auth_tokens['token']}" + response = requests.get(api_url, headers=headers, timeout=10) - + if response.status_code == 200: # Parse the API response - expecting list of {name, code} objects api_data = response.json() - + # Transform API data to our dropdown format document_types = [] if isinstance(api_data, list): for document_type in api_data: - if isinstance(document_type, dict) and 'code' in document_type and 'name' in document_type: - document_types.append({ - 'value': document_type['code'], - 'text': f"{document_type['name']} ({document_type['code']})" - }) - + if isinstance(document_type, dict) and "code" in document_type and "name" in document_type: + document_types.append( + { + "value": document_type["code"], + "text": f"{document_type['name']} ({document_type['code']})", + } + ) + return DropdownAPIViews.success_response(document_types) else: return DropdownAPIViews.error_response(f"API request failed with status {response.status_code}") @@ -473,125 +470,138 @@ def get_optional_services(request): """Get optional services for a filing type from Suffolk LIT Lab API""" try: auth_tokens = get_auth_tokens(request) - + # Get required parameters - court_code = request.GET.get('court') - filing_type_id = request.GET.get('filing_type_id') - jurisdiction = request.GET.get('jurisdiction', 'illinois') - + court_code = request.GET.get("court") + filing_type_id = request.GET.get("filing_type_id") + jurisdiction = request.GET.get("jurisdiction", "illinois") + if not court_code: return DropdownAPIViews.error_response("Missing required court parameter") - + if not filing_type_id: return DropdownAPIViews.error_response("Missing required filing_type_id parameter") - + # Make API call to Suffolk optional services endpoint api_url = f"https://efile-test.suffolklitlab.org/jurisdictions/{jurisdiction}/codes/courts/{court_code}/filing_types/{filing_type_id}/optional_services" - + try: # Make the API request with auth tokens if available headers = {} - if auth_tokens and 'token' in auth_tokens: - headers['Authorization'] = f"Bearer {auth_tokens['token']}" - + if auth_tokens and "token" in auth_tokens: + headers["Authorization"] = f"Bearer {auth_tokens['token']}" + response = requests.get(api_url, headers=headers, timeout=10) - + if response.status_code == 200: # Parse the API response - expecting list of service objects api_data = response.json() - + # Transform API data to our format optional_services = [] if isinstance(api_data, list): for service in api_data: if isinstance(service, dict): - optional_services.append({ - 'code': service.get('code') or service.get('id'), - 'name': service.get('name') or service.get('label') or service.get('text'), - 'fee': service.get('fee') or service.get('cost') or 0, - 'description': service.get('description') or service.get('desc'), - 'required': service.get('required', False) - }) - + optional_services.append( + { + "code": service.get("code") or service.get("id"), + "name": service.get("name") or service.get("label") or service.get("text"), + "fee": service.get("fee") or service.get("cost") or 0, + "description": service.get("description") or service.get("desc"), + "required": service.get("required", False), + } + ) + return DropdownAPIViews.success_response(optional_services) - + else: # API call failed, return empty list with info message - print(f"Optional services API returned status {response.status_code} for court {court_code}, filing type {filing_type_id}") + msg = ( + "Optional services API returned status " + f"{response.status_code} for court {court_code}, " + f"filing type {filing_type_id}" + ) + print(msg) return DropdownAPIViews.success_response([]) - + except (requests.RequestException, requests.Timeout) as api_error: # Return empty list if API fails - print(f"Optional services API request failed for court {court_code}, filing type {filing_type_id}: {api_error}") + msg = ( + "Optional services API request failed for court " + f"{court_code}, filing type {filing_type_id}: {api_error}" + ) + print(msg) return DropdownAPIViews.success_response([]) - + except Exception as e: return DropdownAPIViews.error_response(f"Error: {str(e)}") - + @staticmethod @require_http_methods(["GET"]) def get_party_types(request): """Get available party types from Suffolk LIT Lab API based on case type""" try: auth_tokens = get_auth_tokens(request) - + # Get required parameters - court_code = request.GET.get('court') - case_type_code = request.GET.get('case_type') - jurisdiction = request.GET.get('jurisdiction', 'illinois') - + court_code = request.GET.get("court") + case_type_code = request.GET.get("case_type") + jurisdiction = request.GET.get("jurisdiction", "illinois") + if not court_code or not case_type_code: return DropdownAPIViews.error_response("Missing required court or case_type parameters") - + # Make API call to external party types endpoint api_url = f"https://efile-test.suffolklitlab.org/jurisdictions/{jurisdiction}/codes/courts/{court_code}/case_types/{case_type_code}/party_types" - + # Make the API request with auth tokens if available headers = {} - if auth_tokens and 'token' in auth_tokens: - headers['Authorization'] = f"Bearer {auth_tokens['token']}" - + if auth_tokens and "token" in auth_tokens: + headers["Authorization"] = f"Bearer {auth_tokens['token']}" + response = requests.get(api_url, headers=headers, timeout=10) - + if response.status_code == 200: # Parse the API response - expecting list of party type objects api_data = response.json() - + # Transform API data to our dropdown format party_types = [] if isinstance(api_data, list): for party_type in api_data: - if (isinstance(party_type, dict) and - 'code' in party_type and - 'name' in party_type): + if isinstance(party_type, dict) and "code" in party_type and "name" in party_type: # Include all party types for now (no filtering) - - party_types.append({ - 'value': party_type['code'], - 'text': party_type['name'], - 'code': party_type['code'], - 'name': party_type['name'], - 'isRequired': party_type.get('isrequired', False), - 'isAvailableForNewParties': party_type.get('isAvailableForNewParties', True) - }) - + + party_types.append( + { + "value": party_type["code"], + "text": party_type["name"], + "code": party_type["code"], + "name": party_type["name"], + "isRequired": party_type.get("isrequired", False), + "isAvailableForNewParties": party_type.get("isAvailableForNewParties", True), + } + ) + # Sort alphabetically by name - party_types.sort(key=lambda x: x['name']) - - return DropdownAPIViews.success_response({ - 'party_types': party_types, - 'count': len(party_types), - 'source': 'suffolk_api', - 'court': court_code, - 'case_type': case_type_code - }) + party_types.sort(key=lambda x: x["name"]) + + return DropdownAPIViews.success_response( + { + "party_types": party_types, + "count": len(party_types), + "source": "suffolk_api", + "court": court_code, + "case_type": case_type_code, + } + ) else: # Log the error but don't expose sensitive details error_msg = f"External API returned status {response.status_code}" return DropdownAPIViews.error_response(error_msg) - - except requests.RequestException as e: - return DropdownAPIViews.error_response(f"Network error: Failed to fetch party types") + + except requests.RequestException: + return DropdownAPIViews.error_response("Network error: Failed to fetch party types") except Exception as e: return DropdownAPIViews.error_response(f"Unexpected error: {str(e)}") diff --git a/efile_app/efile/api/filing_views.py b/efile_app/efile/api/filing_views.py index 2b3beaa..c6c4419 100644 --- a/efile_app/efile/api/filing_views.py +++ b/efile_app/efile/api/filing_views.py @@ -1,17 +1,19 @@ """ API views for filing operations and document management """ -from django.http import JsonResponse -from django.views.decorators.http import require_http_methods -from django.views.decorators.csrf import csrf_exempt -import requests + import json -from .base import APIResponseMixin, get_auth_tokens, validate_request + +import requests +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods + +from .base import APIResponseMixin, get_auth_tokens class FilingAPIViews(APIResponseMixin): """API views for filing operations""" - + @staticmethod @require_http_methods(["GET"]) def get_filings(request): @@ -20,22 +22,22 @@ def get_filings(request): auth_tokens = get_auth_tokens(request) if not auth_tokens: return FilingAPIViews.error_response("Not authenticated", 401) - + # API call to get user's filings response = requests.get( - 'https://suffolkefile.com/api/filings', - headers={'Authorization': f'Bearer {auth_tokens["access_token"]}'} + "https://suffolkefile.com/api/filings", + headers={"Authorization": f"Bearer {auth_tokens['access_token']}"}, ) - + if response.status_code == 200: data = response.json() - return FilingAPIViews.success_response(data.get('filings', [])) + return FilingAPIViews.success_response(data.get("filings", [])) else: return FilingAPIViews.error_response("Failed to fetch filings") - + except Exception as e: return FilingAPIViews.error_response(f"Error: {str(e)}") - + @staticmethod @require_http_methods(["POST"]) @csrf_exempt @@ -45,36 +47,34 @@ def create_filing(request): auth_tokens = get_auth_tokens(request) if not auth_tokens: return FilingAPIViews.error_response("Not authenticated", 401) - + data = json.loads(request.body) - + # Validate required fields - required_fields = ['case_category', 'case_type', 'filing_type', 'county'] + required_fields = ["case_category", "case_type", "filing_type", "county"] missing_fields = [field for field in required_fields if not data.get(field)] - + if missing_fields: - return FilingAPIViews.error_response( - f"Missing required fields: {', '.join(missing_fields)}" - ) - + return FilingAPIViews.error_response(f"Missing required fields: {', '.join(missing_fields)}") + # API call to create filing response = requests.post( - 'https://suffolkefile.com/api/filings', + "https://suffolkefile.com/api/filings", json=data, - headers={'Authorization': f'Bearer {auth_tokens["access_token"]}'} + headers={"Authorization": f"Bearer {auth_tokens['access_token']}"}, ) - + if response.status_code == 201: filing_data = response.json() return FilingAPIViews.success_response(filing_data, "Filing created successfully") else: return FilingAPIViews.error_response("Failed to create filing") - + except json.JSONDecodeError: return FilingAPIViews.error_response("Invalid JSON data") except Exception as e: return FilingAPIViews.error_response(f"Error: {str(e)}") - + @staticmethod @require_http_methods(["GET"]) def get_filing_detail(request, filing_id): @@ -83,13 +83,13 @@ def get_filing_detail(request, filing_id): auth_tokens = get_auth_tokens(request) if not auth_tokens: return FilingAPIViews.error_response("Not authenticated", 401) - + # API call to get filing details response = requests.get( - f'https://suffolkefile.com/api/filings/{filing_id}', - headers={'Authorization': f'Bearer {auth_tokens["access_token"]}'} + f"https://suffolkefile.com/api/filings/{filing_id}", + headers={"Authorization": f"Bearer {auth_tokens['access_token']}"}, ) - + if response.status_code == 200: filing_data = response.json() return FilingAPIViews.success_response(filing_data) @@ -97,10 +97,10 @@ def get_filing_detail(request, filing_id): return FilingAPIViews.error_response("Filing not found", 404) else: return FilingAPIViews.error_response("Failed to fetch filing details") - + except Exception as e: return FilingAPIViews.error_response(f"Error: {str(e)}") - + @staticmethod @require_http_methods(["PUT"]) @csrf_exempt @@ -110,16 +110,16 @@ def update_filing(request, filing_id): auth_tokens = get_auth_tokens(request) if not auth_tokens: return FilingAPIViews.error_response("Not authenticated", 401) - + data = json.loads(request.body) - + # API call to update filing response = requests.put( - f'https://suffolkefile.com/api/filings/{filing_id}', + f"https://suffolkefile.com/api/filings/{filing_id}", json=data, - headers={'Authorization': f'Bearer {auth_tokens["access_token"]}'} + headers={"Authorization": f"Bearer {auth_tokens['access_token']}"}, ) - + if response.status_code == 200: filing_data = response.json() return FilingAPIViews.success_response(filing_data, "Filing updated successfully") @@ -127,12 +127,12 @@ def update_filing(request, filing_id): return FilingAPIViews.error_response("Filing not found", 404) else: return FilingAPIViews.error_response("Failed to update filing") - + except json.JSONDecodeError: return FilingAPIViews.error_response("Invalid JSON data") except Exception as e: return FilingAPIViews.error_response(f"Error: {str(e)}") - + @staticmethod @require_http_methods(["DELETE"]) @csrf_exempt @@ -142,20 +142,20 @@ def delete_filing(request, filing_id): auth_tokens = get_auth_tokens(request) if not auth_tokens: return FilingAPIViews.error_response("Not authenticated", 401) - + # API call to delete filing response = requests.delete( - f'https://suffolkefile.com/api/filings/{filing_id}', - headers={'Authorization': f'Bearer {auth_tokens["access_token"]}'} + f"https://suffolkefile.com/api/filings/{filing_id}", + headers={"Authorization": f"Bearer {auth_tokens['access_token']}"}, ) - + if response.status_code == 204: return FilingAPIViews.success_response({}, "Filing deleted successfully") elif response.status_code == 404: return FilingAPIViews.error_response("Filing not found", 404) else: return FilingAPIViews.error_response("Failed to delete filing") - + except Exception as e: return FilingAPIViews.error_response(f"Error: {str(e)}") diff --git a/efile_app/efile/api/urls.py b/efile_app/efile/api/urls.py index 24e8c54..622e8f1 100644 --- a/efile_app/efile/api/urls.py +++ b/efile_app/efile/api/urls.py @@ -1,68 +1,58 @@ """ URL patterns for the API module """ + from django.urls import path + +from .auth_views import ( + external_auth, + external_profile, + payment_accounts, + tyler_token, + user_login, + user_logout, + user_profile, +) +from .case_type_config import get_case_type_config +from .config_views import get_form_config from .dropdown_views import ( get_case_categories, get_case_types, - get_filing_types, get_courts, get_document_types, + get_filing_types, get_optional_services, - get_party_types -) -from .auth_views import ( - user_login, - user_logout, - user_profile, - external_auth, - external_profile, - payment_accounts, - tyler_token + get_party_types, ) -from .filing_views import ( - get_filings, - create_filing, - get_filing_detail, - update_filing, - delete_filing -) -from .config_views import ( - get_form_config -) -from .case_type_config import get_case_type_config +from .filing_views import create_filing, delete_filing, get_filing_detail, get_filings, update_filing -app_name = 'api' +app_name = "api" urlpatterns = [ # Dropdown API endpoints - path('dropdowns/case-categories/', get_case_categories, name='case_categories'), - path('dropdowns/case-types/', get_case_types, name='case_types'), - path('dropdowns/filing-types/', get_filing_types, name='filing_types'), - path('dropdowns/courts/', get_courts, name='courts'), - path('dropdowns/document-types/', get_document_types, name='document_types'), - path('dropdowns/optional-services/', get_optional_services, name='optional_services'), - path('dropdowns/party-types/', get_party_types, name='party_types'), - + path("dropdowns/case-categories/", get_case_categories, name="case_categories"), + path("dropdowns/case-types/", get_case_types, name="case_types"), + path("dropdowns/filing-types/", get_filing_types, name="filing_types"), + path("dropdowns/courts/", get_courts, name="courts"), + path("dropdowns/document-types/", get_document_types, name="document_types"), + path("dropdowns/optional-services/", get_optional_services, name="optional_services"), + path("dropdowns/party-types/", get_party_types, name="party_types"), # Form configuration endpoints - path('form-config/', get_form_config, name='form_config'), - path('case-type-config/', get_case_type_config, name='case_type_config'), - + path("form-config/", get_form_config, name="form_config"), + path("case-type-config/", get_case_type_config, name="case_type_config"), # Authentication API endpoints - path('auth/login/', user_login, name='login'), - path('auth/logout/', user_logout, name='logout'), - path('auth/profile/', user_profile, name='profile'), - path('auth/external/', external_auth, name='external_auth'), - path('auth/external-profile/', external_profile, name='external_profile'), - path('auth/tyler-token/', tyler_token, name='tyler_token'), - + path("auth/login/", user_login, name="login"), + path("auth/logout/", user_logout, name="logout"), + path("auth/profile/", user_profile, name="profile"), + path("auth/external/", external_auth, name="external_auth"), + path("auth/external-profile/", external_profile, name="external_profile"), + path("auth/tyler-token/", tyler_token, name="tyler_token"), # Payment API endpoints - path('payment-accounts/', payment_accounts, name='payment_accounts'), - + path("payment-accounts/", payment_accounts, name="payment_accounts"), # Filing API endpoints - path('filings/', get_filings, name='filings_list'), - path('filings/create/', create_filing, name='create_filing'), - path('filings//', get_filing_detail, name='filing_detail'), - path('filings//update/', update_filing, name='update_filing'), - path('filings//delete/', delete_filing, name='delete_filing'), + path("filings/", get_filings, name="filings_list"), + path("filings/create/", create_filing, name="create_filing"), + path("filings//", get_filing_detail, name="filing_detail"), + path("filings//update/", update_filing, name="update_filing"), + path("filings//delete/", delete_filing, name="delete_filing"), ] diff --git a/efile_app/efile/forms.py b/efile_app/efile/forms.py index d1bc851..70e2c47 100644 --- a/efile_app/efile/forms.py +++ b/efile_app/efile/forms.py @@ -14,7 +14,7 @@ class EFileRegistrationForm(forms.Form): first_name = forms.CharField( max_length=100, label="First or Given Name", - widget=forms.TextInput(attrs={'class': 'form-control', 'id': 'firstName', 'required': 'required'}) + widget=forms.TextInput(attrs={"class": "form-control", "id": "firstName", "required": "required"}), ) middle_name = forms.CharField( max_length=100, required=False, widget=forms.TextInput(attrs={"class": "form-control", "id": "middleName"}) @@ -22,7 +22,7 @@ class EFileRegistrationForm(forms.Form): last_name = forms.CharField( max_length=100, label="Last or Family Name", - widget=forms.TextInput(attrs={'class': 'form-control', 'id': 'lastName', 'required': 'required'}) + widget=forms.TextInput(attrs={"class": "form-control", "id": "lastName", "required": "required"}), ) # Physical Address street_address = forms.CharField( @@ -98,20 +98,20 @@ class EFileRegistrationForm(forms.Form): ) # Contact Information email = forms.EmailField( - widget=forms.EmailInput(attrs={ - 'class': 'form-control', - 'id': 'email', - 'label':'Email address for court communications', - 'required': 'required'}) + widget=forms.EmailInput( + attrs={ + "class": "form-control", + "id": "email", + "label": "Email address for court communications", + "required": "required", + } + ) ) phone = forms.CharField( max_length=20, - widget=forms.TextInput(attrs={ - 'class': 'form-control', - 'label': 'Primary Phone Number', - 'id': 'phone', - 'required': 'required' - }) + widget=forms.TextInput( + attrs={"class": "form-control", "label": "Primary Phone Number", "id": "phone", "required": "required"} + ), ) # Password password = forms.CharField( @@ -157,126 +157,84 @@ def save(self): ) return user + class EFileExpertForm(forms.Form): # Case Classification Fields court = forms.CharField( max_length=100, - widget=forms.Select(attrs={ - 'class': 'form-select dropdown-field', - 'id': 'court', - 'required': True - }) + widget=forms.Select(attrs={"class": "form-select dropdown-field", "id": "court", "required": True}), ) case_category = forms.CharField( max_length=100, - widget=forms.Select(attrs={ - 'class': 'form-select dropdown-field', - 'id': 'case_category', - 'required': True - }) + widget=forms.Select(attrs={"class": "form-select dropdown-field", "id": "case_category", "required": True}), ) case_type = forms.CharField( max_length=100, - widget=forms.Select(attrs={ - 'class': 'form-select dropdown-field', - 'id': 'case_type', - 'required': True - }) + widget=forms.Select(attrs={"class": "form-select dropdown-field", "id": "case_type", "required": True}), ) filing_type = forms.CharField( max_length=100, - widget=forms.Select(attrs={ - 'class': 'form-select dropdown-field', - 'id': 'filing_type', - 'required': True - }) + widget=forms.Select(attrs={"class": "form-select dropdown-field", "id": "filing_type", "required": True}), ) document_type = forms.CharField( max_length=100, - widget=forms.Select(attrs={ - 'class': 'form-select dropdown-field', - 'id': 'document_type', - 'required': True - }) + widget=forms.Select(attrs={"class": "form-select dropdown-field", "id": "document_type", "required": True}), ) - + # Petitioner Information (for name change cases) petitioner_first_name = forms.CharField( max_length=100, required=False, - widget=forms.TextInput(attrs={ - 'class': 'form-control', - 'id': 'petitioner_first_name' - }) + widget=forms.TextInput(attrs={"class": "form-control", "id": "petitioner_first_name"}), ) petitioner_last_name = forms.CharField( max_length=100, required=False, - widget=forms.TextInput(attrs={ - 'class': 'form-control', - 'id': 'petitioner_last_name' - }) + widget=forms.TextInput(attrs={"class": "form-control", "id": "petitioner_last_name"}), ) petitioner_address = forms.CharField( - required=False, - widget=forms.Textarea(attrs={ - 'class': 'form-control', - 'id': 'petitioner_address', - 'rows': 3 - }) + required=False, widget=forms.Textarea(attrs={"class": "form-control", "id": "petitioner_address", "rows": 3}) ) - + # Name Sought Information (for name change cases) new_first_name = forms.CharField( - max_length=100, - required=False, - widget=forms.TextInput(attrs={ - 'class': 'form-control', - 'id': 'new_first_name' - }) + max_length=100, required=False, widget=forms.TextInput(attrs={"class": "form-control", "id": "new_first_name"}) ) new_last_name = forms.CharField( - max_length=100, - required=False, - widget=forms.TextInput(attrs={ - 'class': 'form-control', - 'id': 'new_last_name' - }) + max_length=100, required=False, widget=forms.TextInput(attrs={"class": "form-control", "id": "new_last_name"}) ) - + # Optional Services (MultipleChoiceField for checkboxes) optional_services = forms.MultipleChoiceField( choices=[ - ('certified_mailing', 'Certified Mailing Fee (Each Mailing)'), - ('record_search', 'Record Search'), - ('copies_first', 'Copies - 1st Page'), - ('mailing_fees', 'Mailing Fees (when clerk required to mail)'), - ('copies_docket', 'Copies - Docket'), - ('copies_2_19', 'Copies - Pages 2-19'), - ('copies_20_beyond', 'Copies - Pages 20 and beyond'), - ('certification_seal', 'Certification or Authentication with Seal'), + ("certified_mailing", "Certified Mailing Fee (Each Mailing)"), + ("record_search", "Record Search"), + ("copies_first", "Copies - 1st Page"), + ("mailing_fees", "Mailing Fees (when clerk required to mail)"), + ("copies_docket", "Copies - Docket"), + ("copies_2_19", "Copies - Pages 2-19"), + ("copies_20_beyond", "Copies - Pages 20 and beyond"), + ("certification_seal", "Certification or Authentication with Seal"), ], required=False, - widget=forms.CheckboxSelectMultiple(attrs={ - 'class': 'form-check-input' - }) + widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}), ) def clean(self): cleaned_data = super().clean() - case_type = cleaned_data.get('case_type', '') - + case_type = cleaned_data.get("case_type", "") + # If this is a name change case, make certain fields required - if 'name change' in case_type.lower(): - if not cleaned_data.get('petitioner_first_name'): - self.add_error('petitioner_first_name', 'This field is required for name change cases.') - if not cleaned_data.get('petitioner_last_name'): - self.add_error('petitioner_last_name', 'This field is required for name change cases.') - if not cleaned_data.get('petitioner_address'): - self.add_error('petitioner_address', 'This field is required for name change cases.') - if not cleaned_data.get('new_first_name'): - self.add_error('new_first_name', 'This field is required for name change cases.') - if not cleaned_data.get('new_last_name'): - self.add_error('new_last_name', 'This field is required for name change cases.') - + if "name change" in case_type.lower(): + if not cleaned_data.get("petitioner_first_name"): + self.add_error("petitioner_first_name", "This field is required for name change cases.") + if not cleaned_data.get("petitioner_last_name"): + self.add_error("petitioner_last_name", "This field is required for name change cases.") + if not cleaned_data.get("petitioner_address"): + self.add_error("petitioner_address", "This field is required for name change cases.") + if not cleaned_data.get("new_first_name"): + self.add_error("new_first_name", "This field is required for name change cases.") + if not cleaned_data.get("new_last_name"): + self.add_error("new_last_name", "This field is required for name change cases.") + return cleaned_data diff --git a/efile_app/efile/settings.py b/efile_app/efile/settings.py index feaaf64..573f377 100644 --- a/efile_app/efile/settings.py +++ b/efile_app/efile/settings.py @@ -1,102 +1 @@ -from pathlib import Path - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-r1o&rohs_$%i1)u-8amhsu)o!z4ply+x^*_ht&-jav0h6rnpdo" - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = ['.localhost', '127.0.0.1', '[::1]', 'testserver'] - -# Application definition -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", -] - -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -ROOT_URLCONF = "efile.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [BASE_DIR / "efile/templates"], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -WSGI_APPLICATION = "efile.wsgi.application" - -# Database -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } -} - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] - -LANGUAGE_CODE = "en-us" -TIME_ZONE = "UTC" -USE_I18N = True -USE_TZ = True - -STATIC_URL = "/static/" -STATICFILES_DIRS = [ - BASE_DIR / "efile/static", -] - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -LOGIN_REDIRECT_URL = "/dashboard/" -LOGOUT_REDIRECT_URL = "/login/" -SUFFOLK_EFILE_API_KEY = "bF9PWmOaJv2CUgFB9ypP2" - -# AWS S3 Configuration -AWS_ACCESS_KEY_ID = "AKIA3C6FLW7XTYISIW72" -AWS_SECRET_ACCESS_KEY = r"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" # Raw string to handle special chars -AWS_S3_BUCKET_NAME = "forms-mvp-xf6361" -AWS_S3_REGION_NAME = "us-east-1" - -# File Upload Settings -MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB -ALLOWED_FILE_TYPES = ['.pdf', '.doc', '.docx'] +from efile.settings_dev import * # noqa: F401,F403 diff --git a/efile_app/efile/settings_base.py b/efile_app/efile/settings_base.py new file mode 100644 index 0000000..c47efd4 --- /dev/null +++ b/efile_app/efile/settings_base.py @@ -0,0 +1,131 @@ +import os +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +# Dev fallback; staging/prod must override via env in their settings modules. +SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "django-insecure-r1o&rohs_$%i1)u-8amhsu)o!z4ply+x^*_ht&-jav0h6rnpdo") + +# SECURITY WARNING: don't run with debug turned on in production! +# Override in env-specific settings +DEBUG = True + +# Override in env-specific settings +ALLOWED_HOSTS: list[str] = [] + +# Override in env-specific settings +CSRF_TRUSTED_ORIGINS: list[str] = [] + +# Application definition +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "efile.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "efile/templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "efile.wsgi.application" + +# Database +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + +AUTH_PASSWORD_VALIDATORS = [ + {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, +] + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True + +STATIC_URL = "/static/" +STATICFILES_DIRS = [BASE_DIR / "efile/static"] + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +LOGIN_REDIRECT_URL = "/dashboard/" +LOGOUT_REDIRECT_URL = "/login/" +SUFFOLK_EFILE_API_KEY = os.getenv("SUFFOLK_EFILE_API_KEY", "") + +# AWS S3 Configuration +AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID", "") +AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY", "") +AWS_S3_BUCKET_NAME = os.getenv("AWS_S3_BUCKET_NAME", "") +AWS_S3_REGION_NAME = os.getenv("AWS_S3_REGION_NAME", "us-east-1") + +# File Upload Settings +MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB +ALLOWED_FILE_TYPES = [".pdf", ".doc", ".docx"] + +# Logging configuration +# Uses standard Python logging. Control the app logger level via DJANGO_LOG_LEVEL env var. +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": { + "format": "[%(levelname)s] %(asctime)s %(name)s: %(message)s", + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "simple", + } + }, + "loggers": { + # Django's own logs + "django": { + "handlers": ["console"], + "level": os.getenv("DJANGO_DJANGO_LOG_LEVEL", "INFO"), + "propagate": True, + }, + # App-specific logger + "efile": { + "handlers": ["console"], + "level": os.getenv("DJANGO_LOG_LEVEL", "DEBUG"), + "propagate": False, + }, + }, +} diff --git a/efile_app/efile/settings_dev.py b/efile_app/efile/settings_dev.py new file mode 100644 index 0000000..f97d173 --- /dev/null +++ b/efile_app/efile/settings_dev.py @@ -0,0 +1,35 @@ +import os +from pathlib import Path + +import dj_database_url +from dotenv import load_dotenv + +# Determine BASE_DIR without importing base to load .env first +_BASE_DIR = Path(__file__).resolve().parent.parent + +# Load environment variables from .env at the project base directory BEFORE base import +load_dotenv(dotenv_path=_BASE_DIR / ".env", override=False) + +from efile.settings_base import * # noqa: E402,F401,F403 +from efile.settings_base import DATABASES as BASE_DATABASES # noqa: E402 + +# Bind DATABASES explicitly to avoid F405 and make linter aware of the symbol +DATABASES = BASE_DATABASES + +DEBUG = True +ALLOWED_HOSTS = ["localhost", "127.0.0.1", ".localhost", "[::1]", "testserver"] +CSRF_TRUSTED_ORIGINS = [ + "http://localhost", + "http://127.0.0.1", + "http://::1", + "http://testserver", +] + +# If a DATABASE_URL is provided locally, prefer Postgres over SQLite +_DEV_DATABASE_URL = os.getenv("DATABASE_URL", "").strip() +if _DEV_DATABASE_URL: + DATABASES["default"] = dj_database_url.config( + default=_DEV_DATABASE_URL, + conn_max_age=0, # no pooling for dev; immediate close on request end + ssl_require=False, + ) diff --git a/efile_app/efile/settings_prod.py b/efile_app/efile/settings_prod.py new file mode 100644 index 0000000..145c7fe --- /dev/null +++ b/efile_app/efile/settings_prod.py @@ -0,0 +1,37 @@ +import os + +import dj_database_url + +from efile.settings_base import * # noqa: F401,F403 +from efile.settings_base import DATABASES as BASE_DATABASES + +# Bind DATABASES explicitly to avoid F405 and make linter aware of the symbol +DATABASES = BASE_DATABASES + +DEBUG = False +ALLOWED_HOSTS = ["forms-mvp-prod.fly.dev"] +CSRF_TRUSTED_ORIGINS = ["https://forms-mvp-prod.fly.dev"] + +# Security hardening +SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY") +if not SECRET_KEY or SECRET_KEY.startswith("django-insecure-"): + raise RuntimeError("DJANGO_SECRET_KEY must be set to a strong value in production") + +SECURE_SSL_REDIRECT = True +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_SECURE = True +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +SECURE_HSTS_SECONDS = 31536000 +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True + +# Database: require DATABASE_URL and configure Postgres with pooling and SSL +DATABASE_URL = os.getenv("DATABASE_URL", "").strip() +if not DATABASE_URL: + raise RuntimeError("DATABASE_URL must be set for production environment") + +DATABASES["default"] = dj_database_url.config( + default=DATABASE_URL, + conn_max_age=600, + ssl_require=True, +) diff --git a/efile_app/efile/settings_staging.py b/efile_app/efile/settings_staging.py new file mode 100644 index 0000000..28f3736 --- /dev/null +++ b/efile_app/efile/settings_staging.py @@ -0,0 +1,85 @@ +import os + +import dj_database_url + +from efile.settings_base import * # noqa: F401,F403 + +# Import specific names with aliases to avoid F405 from star import usage +from efile.settings_base import BASE_DIR as BASE_SETTINGS_DIR # noqa: F401 +from efile.settings_base import DATABASES as BASE_DATABASES +from efile.settings_base import MIDDLEWARE as BASE_MIDDLEWARE + +# Bind DATABASES explicitly to avoid F405 and make linter aware of the symbol +DATABASES = BASE_DATABASES + +DEBUG = False +ALLOWED_HOSTS = ["forms-mvp-staging.fly.dev"] +CSRF_TRUSTED_ORIGINS = ["https://forms-mvp-staging.fly.dev"] + +# Security hardening suitable for staging +SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY") +if not SECRET_KEY or SECRET_KEY.startswith("django-insecure-"): + raise RuntimeError("DJANGO_SECRET_KEY must be set to a strong value in staging") + +SECURE_SSL_REDIRECT = True +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_SECURE = True +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +# HSTS with short window for staging +SECURE_HSTS_SECONDS = 86400 # 1 day +SECURE_HSTS_INCLUDE_SUBDOMAINS = False +SECURE_HSTS_PRELOAD = False + +# Static files (WhiteNoise) +# Collect static into a dedicated directory and serve with WhiteNoise +STATIC_ROOT = BASE_SETTINGS_DIR / "staticfiles" +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" + +# Build MIDDLEWARE from base and insert WhiteNoise right after SecurityMiddleware +MIDDLEWARE = list(BASE_MIDDLEWARE) +try: + security_index = MIDDLEWARE.index("django.middleware.security.SecurityMiddleware") +except ValueError: + security_index = -1 +insert_at = security_index + 1 if security_index >= 0 else 0 +MIDDLEWARE.insert(insert_at, "whitenoise.middleware.WhiteNoiseMiddleware") + +# Verbose logging to console for staging +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + }, + "root": { + "handlers": ["console"], + "level": "DEBUG", + }, + "loggers": { + "django": {"handlers": ["console"], "level": "DEBUG", "propagate": False}, + "django.request": {"handlers": ["console"], "level": "DEBUG", "propagate": False}, + "uvicorn": {"handlers": ["console"], "level": "DEBUG", "propagate": False}, + "gunicorn": {"handlers": ["console"], "level": "DEBUG", "propagate": False}, + "whitenoise": {"handlers": ["console"], "level": "DEBUG", "propagate": False}, + }, +} + +# Database: require DATABASE_URL and configure Postgres with pooling and SSL +DATABASE_URL = os.getenv("DATABASE_URL", "").strip() +if not DATABASE_URL: + raise RuntimeError("DATABASE_URL must be set for staging environment") + +DATABASES["default"] = dj_database_url.config( + default=DATABASE_URL, + conn_max_age=600, + ssl_require=True, +) diff --git a/efile_app/efile/test_integration.py b/efile_app/efile/test_integration.py index 49e25cb..56d21b8 100644 --- a/efile_app/efile/test_integration.py +++ b/efile_app/efile/test_integration.py @@ -2,177 +2,177 @@ Simple integration tests for the efile application Tests basic functionality without mocking external services """ -import pytest + import json -from django.test import Client + +import pytest from django.contrib.auth.models import User +from django.test import Client class TestBasicFunctionality: """Test basic functionality that doesn't depend on external services.""" - + def test_zip_to_county_mapping_works(self): """Test that zip code to county mapping works correctly.""" from efile.utils.zip_to_county_il import get_county_by_zip, get_zips_by_county - + # Test known mappings - assert get_county_by_zip('60601') == 'Cook' - assert get_county_by_zip('60614') == 'Cook' - + assert get_county_by_zip("60601") == "Cook" + assert get_county_by_zip("60614") == "Cook" + # Test reverse mapping - cook_zips = get_zips_by_county('Cook') - assert '60601' in cook_zips - assert '60614' in cook_zips + cook_zips = get_zips_by_county("Cook") + assert "60601" in cook_zips + assert "60614" in cook_zips assert len(cook_zips) > 50 # Cook County has many zip codes - + # Test invalid inputs - assert get_county_by_zip('00000') is None - assert get_zips_by_county('NonexistentCounty') == [] + assert get_county_by_zip("00000") is None + assert get_zips_by_county("NonexistentCounty") == [] @pytest.mark.django_db def test_profile_api_basic_functionality(self): """Test that profile API returns some response.""" client = Client() - + # Create a test user user = User.objects.create_user( - username='testuser', - password='testpass123', - first_name='Test', - last_name='User', - email='test@example.com' + username="testuser", password="testpass123", first_name="Test", last_name="User", email="test@example.com" ) - + # Login the user client.force_login(user) - + # Test authenticated request - response = client.get('/api/auth/profile/', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + response = client.get("/api/auth/profile/", HTTP_X_REQUESTED_WITH="XMLHttpRequest") + assert response.status_code == 200 data = json.loads(response.content) - + # Should have basic structure - assert 'success' in data - - if data['success']: - assert 'data' in data - assert data['data']['username'] == 'testuser' - assert data['data']['first_name'] == 'Test' - - @pytest.mark.django_db + assert "success" in data + + if data["success"]: + assert "data" in data + assert data["data"]["username"] == "testuser" + assert data["data"]["first_name"] == "Test" + + @pytest.mark.django_db def test_case_categories_api_basic_functionality(self): """Test that case categories API returns some response.""" client = Client() - + # Case categories API requires a court parameter - response = client.get('/api/dropdowns/case-categories/', - {'court': 'cook:law1'}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + response = client.get( + "/api/dropdowns/case-categories/", {"court": "cook:law1"}, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + ) + assert response.status_code in [200, 400] # Accept both success and error data = json.loads(response.content) - + # Should have basic structure - assert 'success' in data - - if data['success']: - assert 'data' in data - assert isinstance(data['data'], list) + assert "success" in data + + if data["success"]: + assert "data" in data + assert isinstance(data["data"], list) def test_form_page_loads(self): """Test that the expert form page loads without errors.""" client = Client() - + # Test the form page (might need authentication) try: - response = client.get('/expert-form/') + response = client.get("/expert-form/") # Page should load (200) or redirect to login (302) assert response.status_code in [200, 302, 404] # 404 if route doesn't exist - except Exception as e: + except Exception: # If the route doesn't exist, that's okay for this test assert True def test_login_page_functionality(self): """Test that login functionality works.""" client = Client() - - response = client.get('/login/') + + response = client.get("/login/") assert response.status_code == 200 - assert b'login' in response.content.lower() + assert b"login" in response.content.lower() class TestUtilityFunctions: """Test utility functions in isolation.""" - + def test_case_form_config_exists(self): """Test that case form configuration can be loaded.""" try: from efile.api.case_form_views import CaseFormAPIViews + # Should be able to load case type forms without error forms_config = CaseFormAPIViews._load_case_type_forms() assert forms_config is not None - assert 'case_types' in forms_config + assert "case_types" in forms_config except Exception: # If case-type-forms.yaml doesn't exist, skip this test pytest.skip("case-type-forms.yaml not found") - + def test_dropdown_api_views_can_be_imported(self): """Test that dropdown API views can be imported.""" from efile.api.dropdown_views import DropdownAPIViews - + # Should be able to create instance view = DropdownAPIViews() assert view is not None - + # Should have required methods - assert hasattr(DropdownAPIViews, '_prioritize_courts_by_location') - assert hasattr(DropdownAPIViews, 'get_case_categories') - assert hasattr(DropdownAPIViews, 'get_courts') + assert hasattr(DropdownAPIViews, "_prioritize_courts_by_location") + assert hasattr(DropdownAPIViews, "get_case_categories") + assert hasattr(DropdownAPIViews, "get_courts") class TestJavaScriptFileStructure: """Test that our refactored JavaScript files exist.""" - + def test_javascript_files_exist(self): """Test that all required JavaScript files exist.""" import os + from django.conf import settings - + # Get the static files directory - static_root = os.path.join(settings.BASE_DIR, 'efile', 'static', 'js') - + static_root = os.path.join(settings.BASE_DIR, "efile", "static", "js") + required_files = [ - 'api-utils.js', - 'cascading-dropdowns.js', - 'form-validation.js', - 'expert-form-main.js', - 'README.md' + "api-utils.js", + "cascading-dropdowns.js", + "form-validation.js", + "expert-form-main.js", + "README.md", ] - + for filename in required_files: file_path = os.path.join(static_root, filename) assert os.path.exists(file_path), f"Required file {filename} not found at {file_path}" - + def test_javascript_files_have_content(self): """Test that JavaScript files contain expected content.""" import os + from django.conf import settings - - static_root = os.path.join(settings.BASE_DIR, 'efile', 'static', 'js') - + + static_root = os.path.join(settings.BASE_DIR, "efile", "static", "js") + # Test that files contain expected classes/functions tests = [ - ('api-utils.js', 'class ApiUtils'), - ('cascading-dropdowns.js', 'class CascadingDropdowns'), - ('form-validation.js', 'class FormValidation'), - ('expert-form-main.js', 'class ExpertForm'), + ("api-utils.js", "class ApiUtils"), + ("cascading-dropdowns.js", "class CascadingDropdowns"), + ("form-validation.js", "class FormValidation"), + ("expert-form-main.js", "class ExpertForm"), ] - + for filename, expected_content in tests: file_path = os.path.join(static_root, filename) if os.path.exists(file_path): - with open(file_path, 'r') as f: + with open(file_path) as f: content = f.read() assert expected_content in content, f"{filename} should contain '{expected_content}'" diff --git a/efile_app/efile/test_utils.py b/efile_app/efile/test_utils.py index 67a4e60..a703748 100644 --- a/efile_app/efile/test_utils.py +++ b/efile_app/efile/test_utils.py @@ -1,23 +1,24 @@ # Test utilities and fixtures for the efile application +import json +from unittest.mock import patch + import pytest from django.contrib.auth.models import User from django.test import Client -from unittest.mock import Mock, patch -import json class MockResponse: """Mock response object for testing API calls.""" - + def __init__(self, json_data, status_code=200, ok=True): self.json_data = json_data self.status_code = status_code self.ok = ok - + def json(self): return self.json_data - + def raise_for_status(self): if not self.ok: raise Exception(f"HTTP {self.status_code}") @@ -26,72 +27,74 @@ def raise_for_status(self): @pytest.fixture def mock_court_api_response(): """Mock response for Suffolk LIT Lab courts API.""" - return MockResponse({ - 'courts': [ - { - 'id': 'cook:law1', - 'name': 'Cook County Circuit Court - Law Division', - 'jurisdiction': 'illinois', - 'address': '50 W Washington St, Chicago, IL 60602' - }, - { - 'id': 'cook:dr1', - 'name': 'Cook County Circuit Court - Domestic Relations', - 'jurisdiction': 'illinois', - 'address': '50 W Washington St, Chicago, IL 60602' - }, - { - 'id': 'cook:pr1', - 'name': 'Cook County Circuit Court - Probate', - 'jurisdiction': 'illinois', - 'address': '50 W Washington St, Chicago, IL 60602' - }, - { - 'id': 'will:law1', - 'name': 'Will County Circuit Court - Law Division', - 'jurisdiction': 'illinois', - 'address': '14 W Jefferson St, Joliet, IL 60432' - } - ] - }) + return MockResponse( + { + "courts": [ + { + "id": "cook:law1", + "name": "Cook County Circuit Court - Law Division", + "jurisdiction": "illinois", + "address": "50 W Washington St, Chicago, IL 60602", + }, + { + "id": "cook:dr1", + "name": "Cook County Circuit Court - Domestic Relations", + "jurisdiction": "illinois", + "address": "50 W Washington St, Chicago, IL 60602", + }, + { + "id": "cook:pr1", + "name": "Cook County Circuit Court - Probate", + "jurisdiction": "illinois", + "address": "50 W Washington St, Chicago, IL 60602", + }, + { + "id": "will:law1", + "name": "Will County Circuit Court - Law Division", + "jurisdiction": "illinois", + "address": "14 W Jefferson St, Joliet, IL 60432", + }, + ] + } + ) @pytest.fixture def mock_case_categories_response(): """Mock response for case categories API.""" - return MockResponse({ - 'case_categories': [ - {'id': 'civil', 'text': 'Civil Law'}, - {'id': 'family', 'text': 'Family Law'}, - {'id': 'probate', 'text': 'Probate'}, - {'id': 'misc', 'text': 'Miscellaneous'}, - ] - }) + return MockResponse( + { + "case_categories": [ + {"id": "civil", "text": "Civil Law"}, + {"id": "family", "text": "Family Law"}, + {"id": "probate", "text": "Probate"}, + {"id": "misc", "text": "Miscellaneous"}, + ] + } + ) @pytest.fixture def mock_case_types_response(): """Mock response for case types API.""" - return MockResponse({ - 'case_types': [ - {'id': 'divorce', 'name': 'Divorce'}, - {'id': 'custody', 'name': 'Child Custody'}, - {'id': 'support', 'name': 'Child Support'}, - {'id': 'contract', 'name': 'Contract Dispute'}, - {'id': 'tort', 'name': 'Personal Injury'}, - ] - }) + return MockResponse( + { + "case_types": [ + {"id": "divorce", "name": "Divorce"}, + {"id": "custody", "name": "Child Custody"}, + {"id": "support", "name": "Child Support"}, + {"id": "contract", "name": "Contract Dispute"}, + {"id": "tort", "name": "Personal Injury"}, + ] + } + ) @pytest.fixture def authenticated_user(db): """Create and return an authenticated user.""" user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpass123', - first_name='Test', - last_name='User' + username="testuser", email="test@example.com", password="testpass123", first_name="Test", last_name="User" ) return user @@ -100,8 +103,8 @@ def authenticated_user(db): def api_client(): """Create a Django test client with proper headers for API requests.""" client = Client() - client.defaults['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - client.defaults['HTTP_CONTENT_TYPE'] = 'application/json' + client.defaults["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" + client.defaults["HTTP_CONTENT_TYPE"] = "application/json" return client @@ -114,34 +117,34 @@ def authenticated_api_client(api_client, authenticated_user): class APITestMixin: """Mixin class providing common API testing utilities.""" - + def assert_api_success(self, response, expected_data_length=None): """Assert that an API response indicates success.""" assert response.status_code == 200 data = json.loads(response.content) - assert data['success'] is True - assert 'data' in data - + assert data["success"] is True + assert "data" in data + if expected_data_length is not None: - assert len(data['data']) == expected_data_length - + assert len(data["data"]) == expected_data_length + return data - + def assert_api_error(self, response, expected_error_message=None): """Assert that an API response indicates an error.""" assert response.status_code == 200 # Our APIs return 200 with error flags data = json.loads(response.content) - assert data['success'] is False - assert 'error' in data - + assert data["success"] is False + assert "error" in data + if expected_error_message: - assert expected_error_message.lower() in data['error'].lower() - + assert expected_error_message.lower() in data["error"].lower() + return data - + def mock_external_api(self, api_response): """Context manager for mocking external API calls.""" - return patch('requests.get', return_value=api_response) + return patch("requests.get", return_value=api_response) @pytest.fixture @@ -152,38 +155,38 @@ def api_test_mixin(): # Sample test data for consistent testing SAMPLE_ZIP_CODES = { - 'cook_county': ['60601', '60614', '60611', '60612'], - 'will_county': ['60432', '60403', '60435'], - 'dupage_county': ['60515', '60516', '60521'], + "cook_county": ["60601", "60614", "60611", "60612"], + "will_county": ["60432", "60403", "60435"], + "dupage_county": ["60515", "60516", "60521"], } SAMPLE_COURT_CODES = { - 'cook_law': 'cook:law1', - 'cook_family': 'cook:dr1', - 'cook_probate': 'cook:pr1', - 'will_law': 'will:law1', + "cook_law": "cook:law1", + "cook_family": "cook:dr1", + "cook_probate": "cook:pr1", + "will_law": "will:law1", } SAMPLE_CASE_CATEGORIES = [ - {'id': 'civil', 'text': 'Civil Law'}, - {'id': 'family', 'text': 'Family Law'}, - {'id': 'probate', 'text': 'Probate'}, - {'id': 'criminal', 'text': 'Criminal'}, + {"id": "civil", "text": "Civil Law"}, + {"id": "family", "text": "Family Law"}, + {"id": "probate", "text": "Probate"}, + {"id": "criminal", "text": "Criminal"}, ] def create_mock_form_data(**kwargs): """Create mock form data for testing form submissions.""" default_data = { - 'court': 'cook:law1', - 'case_category': 'civil', - 'case_type': 'contract', - 'filing_type': 'complaint', - 'document_type': 'motion', - 'petitioner_first_name': 'John', - 'petitioner_last_name': 'Doe', - 'new_first_name': 'Jane', - 'new_last_name': 'Smith', + "court": "cook:law1", + "case_category": "civil", + "case_type": "contract", + "filing_type": "complaint", + "document_type": "motion", + "petitioner_first_name": "John", + "petitioner_last_name": "Doe", + "new_first_name": "Jane", + "new_last_name": "Smith", } default_data.update(kwargs) return default_data @@ -192,13 +195,13 @@ def create_mock_form_data(**kwargs): def create_mock_user_profile(**kwargs): """Create mock user profile data for testing.""" default_profile = { - 'username': 'testuser', - 'first_name': 'Test', - 'last_name': 'User', - 'email': 'test@example.com', - 'zip_code': '60601', - 'preferred_county': 'Cook', - 'state': 'IL', + "username": "testuser", + "first_name": "Test", + "last_name": "User", + "email": "test@example.com", + "zip_code": "60601", + "preferred_county": "Cook", + "state": "IL", } default_profile.update(kwargs) return default_profile diff --git a/efile_app/efile/tests.py b/efile_app/efile/tests.py index 16d470d..8cf5b92 100644 --- a/efile_app/efile/tests.py +++ b/efile_app/efile/tests.py @@ -1,10 +1,10 @@ -import pytest import json from unittest.mock import Mock, patch -from django.urls import reverse + +import pytest from django.contrib.auth.models import User from django.test import Client -from django.http import JsonResponse +from django.urls import reverse @pytest.mark.django_db(False) @@ -26,274 +26,268 @@ def test_login_page_renders(client): # DROPDOWN API TESTS # ============================================================================ + class TestDropdownAPIs: """Test suite for dropdown API endpoints.""" - + @pytest.fixture def api_client(self): """Create a client with AJAX headers.""" client = Client() return client - + @pytest.fixture def user(self, db): """Create a test user.""" - return User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpass123' - ) - + return User.objects.create_user(username="testuser", email="test@example.com", password="testpass123") + def test_courts_api_without_params(self, api_client): """Test courts API returns data without parameters.""" - with patch('efile.api.dropdown_views.requests.get') as mock_get: + with patch("efile.api.dropdown_views.requests.get") as mock_get: mock_response = Mock() mock_response.json.return_value = { - 'courts': [ - {'id': 'cook:law1', 'name': 'Cook County Law Division', 'jurisdiction': 'illinois'}, - {'id': 'cook:dr1', 'name': 'Cook County Domestic Relations', 'jurisdiction': 'illinois'}, + "courts": [ + {"id": "cook:law1", "name": "Cook County Law Division", "jurisdiction": "illinois"}, + {"id": "cook:dr1", "name": "Cook County Domestic Relations", "jurisdiction": "illinois"}, ] } mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response - - response = api_client.get('/api/dropdowns/courts/', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + + response = api_client.get("/api/dropdowns/courts/", HTTP_X_REQUESTED_WITH="XMLHttpRequest") + assert response.status_code == 200 data = json.loads(response.content) - assert data['success'] is True - assert len(data['data']) >= 2 - assert any(court['value'] == 'cook:law1' for court in data['data']) - + assert data["success"] is True + assert len(data["data"]) >= 2 + assert any(court["value"] == "cook:law1" for court in data["data"]) + def test_courts_api_with_user_location(self, api_client): """Test courts API prioritizes based on user location.""" - with patch('efile.api.dropdown_views.requests.get') as mock_get: + with patch("efile.api.dropdown_views.requests.get") as mock_get: mock_response = Mock() mock_response.json.return_value = { - 'courts': [ - {'id': 'cook:law1', 'name': 'Cook County Law Division', 'jurisdiction': 'illinois'}, - {'id': 'will:law1', 'name': 'Will County Law Division', 'jurisdiction': 'illinois'}, + "courts": [ + {"id": "cook:law1", "name": "Cook County Law Division", "jurisdiction": "illinois"}, + {"id": "will:law1", "name": "Will County Law Division", "jurisdiction": "illinois"}, ] } mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response - - response = api_client.get('/api/dropdowns/courts/', { - 'user_county': 'Cook', - 'user_zip': '60601' - }, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + + response = api_client.get( + "/api/dropdowns/courts/", + {"user_county": "Cook", "user_zip": "60601"}, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + assert response.status_code == 200 data = json.loads(response.content) - assert data['success'] is True - + assert data["success"] is True + # Cook County courts should be marked as recommended - cook_courts = [court for court in data['data'] if 'cook' in court['value'].lower()] + cook_courts = [court for court in data["data"] if "cook" in court["value"].lower()] assert len(cook_courts) > 0 - assert any(court.get('recommended') for court in cook_courts) - + assert any(court.get("recommended") for court in cook_courts) + def test_case_categories_api_with_court(self, api_client): """Test case categories API filters by court.""" - with patch('efile.api.dropdown_views.requests.get') as mock_get: + with patch("efile.api.dropdown_views.requests.get") as mock_get: mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = [ - {'code': 'civil', 'name': 'Civil Law'}, - {'code': 'family', 'name': 'Family Law'}, - {'code': 'probate', 'name': 'Probate'}, + {"code": "civil", "name": "Civil Law"}, + {"code": "family", "name": "Family Law"}, + {"code": "probate", "name": "Probate"}, ] mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response - - response = api_client.get('/api/dropdowns/case-categories/', { - 'court': 'cook:law1' - }, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + + response = api_client.get( + "/api/dropdowns/case-categories/", {"court": "cook:law1"}, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + ) + if response.status_code != 200: print(f"Error response content: {response.content.decode()}") - + assert response.status_code == 200 data = json.loads(response.content) - assert data['success'] is True - assert len(data['data']) > 0 - + assert data["success"] is True + assert len(data["data"]) > 0 + # Should contain civil and family for law court - category_texts = [cat['text'].lower() for cat in data['data']] - assert any('civil' in text for text in category_texts) - + category_texts = [cat["text"].lower() for cat in data["data"]] + assert any("civil" in text for text in category_texts) + def test_case_categories_api_without_court_returns_error(self, api_client): """Test case categories API requires court parameter.""" - response = api_client.get('/api/dropdowns/case-categories/', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + response = api_client.get("/api/dropdowns/case-categories/", HTTP_X_REQUESTED_WITH="XMLHttpRequest") + assert response.status_code == 400 data = json.loads(response.content) - assert data['success'] is False - assert 'court parameter' in data['error'].lower() or 'missing' in data['error'].lower() - + assert data["success"] is False + assert "court parameter" in data["error"].lower() or "missing" in data["error"].lower() + def test_case_types_api(self, api_client): """Test case types API returns filtered data.""" - with patch('efile.api.dropdown_views.requests.get') as mock_get: + with patch("efile.api.dropdown_views.requests.get") as mock_get: mock_response = Mock() # The API returns list format, not dict with 'case_types' key mock_response.json.return_value = [ - {'code': 'divorce', 'name': 'Divorce'}, - {'code': 'custody', 'name': 'Child Custody'}, + {"code": "divorce", "name": "Divorce"}, + {"code": "custody", "name": "Child Custody"}, ] mock_response.status_code = 200 # Set status code properly mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response - response = api_client.get('/api/dropdowns/case-types/', { - 'parent': 'family', # Use 'parent' not 'category' - 'court': 'cook:dr1' - }, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = api_client.get( + "/api/dropdowns/case-types/", + { + "parent": "family", # Use 'parent' not 'category' + "court": "cook:dr1", + }, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) assert response.status_code == 200 data = json.loads(response.content) - assert data['success'] is True - assert len(data['data']) > 0 - + assert data["success"] is True + assert len(data["data"]) > 0 + def test_api_handles_external_service_failure(self, api_client): """Test API gracefully handles external service failures.""" - with patch('efile.api.dropdown_views.requests.get') as mock_get: + with patch("efile.api.dropdown_views.requests.get") as mock_get: mock_get.side_effect = Exception("Network error") - response = api_client.get('/api/dropdowns/courts/', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = api_client.get("/api/dropdowns/courts/", HTTP_X_REQUESTED_WITH="XMLHttpRequest") # The API should return a 400 error status for external service failure assert response.status_code == 400 data = json.loads(response.content) - assert data['success'] is False - assert 'error' in data + assert data["success"] is False + assert "error" in data # Check that the actual error message is present - assert 'network error' in data['error'].lower() + assert "network error" in data["error"].lower() # ============================================================================ -# AUTH API TESTS +# AUTH API TESTS # ============================================================================ + class TestAuthAPIs: """Test suite for authentication API endpoints.""" - + @pytest.fixture def user(self, db): """Create a test user with profile data.""" return User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpass123', - first_name='John', - last_name='Doe' + username="testuser", email="test@example.com", password="testpass123", first_name="John", last_name="Doe" ) - + def test_profile_api_authenticated_user(self, client, user): """Test profile API returns user data when authenticated.""" client.force_login(user) - - response = client.get('/api/auth/profile/', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + + response = client.get("/api/auth/profile/", HTTP_X_REQUESTED_WITH="XMLHttpRequest") + assert response.status_code == 200 data = json.loads(response.content) - assert data['success'] is True - assert data['data']['username'] == 'testuser' - assert data['data']['first_name'] == 'John' - assert data['data']['email'] == 'test@example.com' - + assert data["success"] is True + assert data["data"]["username"] == "testuser" + assert data["data"]["first_name"] == "John" + assert data["data"]["email"] == "test@example.com" + def test_profile_api_includes_location_data(self, client, user): """Test profile API includes location and county mapping.""" client.force_login(user) - - response = client.get('/api/auth/profile/', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + + response = client.get("/api/auth/profile/", HTTP_X_REQUESTED_WITH="XMLHttpRequest") + assert response.status_code == 200 data = json.loads(response.content) - assert data['success'] is True - + assert data["success"] is True + # Should include location data (either from API or default demo data) - assert 'zip_code' in data['data'] - assert 'preferred_county' in data['data'] - + assert "zip_code" in data["data"] + assert "preferred_county" in data["data"] + # Should have Cook County as demo data - assert data['data']['zip_code'] == '60601' - assert data['data']['preferred_county'].lower() == 'cook' - + assert data["data"]["zip_code"] == "60601" + assert data["data"]["preferred_county"].lower() == "cook" + def test_profile_api_unauthenticated_user(self, client): """Test profile API handles unauthenticated users with demo data.""" - response = client.get('/api/auth/profile/', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = client.get("/api/auth/profile/", HTTP_X_REQUESTED_WITH="XMLHttpRequest") assert response.status_code == 200 data = json.loads(response.content) - assert data['success'] is True + assert data["success"] is True # Should provide demo data for unauthenticated users - assert data['data']['username'] == 'demo_user' - assert data['data']['first_name'] == 'John' # Auth API returns 'John' not 'Demo' - assert data['data']['last_name'] == 'Doe' # Auth API returns 'Doe' not 'User' - assert data['data']['zip_code'] == '60601' + assert data["data"]["username"] == "demo_user" + assert data["data"]["first_name"] == "John" # Auth API returns 'John' not 'Demo' + assert data["data"]["last_name"] == "Doe" # Auth API returns 'Doe' not 'User' + assert data["data"]["zip_code"] == "60601" # ============================================================================ # UTILITY FUNCTION TESTS # ============================================================================ + class TestZipToCountyMapping: """Test suite for Illinois zip-to-county mapping utility.""" - + def test_get_county_by_zip_cook_county(self): """Test mapping for Cook County zip codes.""" from efile.utils.zip_to_county_il import get_county_by_zip - + # Test major Chicago zip codes - assert get_county_by_zip('60601') == 'Cook' # Downtown Chicago - assert get_county_by_zip('60614') == 'Cook' # Lincoln Park - assert get_county_by_zip('60611') == 'Cook' # Near North Side - + assert get_county_by_zip("60601") == "Cook" # Downtown Chicago + assert get_county_by_zip("60614") == "Cook" # Lincoln Park + assert get_county_by_zip("60611") == "Cook" # Near North Side + def test_get_county_by_zip_other_counties(self): """Test mapping for other Illinois counties.""" from efile.utils.zip_to_county_il import get_county_by_zip - + # Test some other counties with correct zip codes - assert get_county_by_zip('61820') == 'Champaign' # Champaign County - assert get_county_by_zip('62701') == 'Sangamon' # Springfield - assert get_county_by_zip('61108') == 'Winnebago' # Rockford - + assert get_county_by_zip("61820") == "Champaign" # Champaign County + assert get_county_by_zip("62701") == "Sangamon" # Springfield + assert get_county_by_zip("61108") == "Winnebago" # Rockford + def test_get_county_by_zip_invalid_zip(self): """Test handling of invalid zip codes.""" from efile.utils.zip_to_county_il import get_county_by_zip - - assert get_county_by_zip('00000') is None - assert get_county_by_zip('99999') is None - assert get_county_by_zip('invalid') is None - assert get_county_by_zip('') is None + + assert get_county_by_zip("00000") is None + assert get_county_by_zip("99999") is None + assert get_county_by_zip("invalid") is None + assert get_county_by_zip("") is None assert get_county_by_zip(None) is None - + def test_get_zip_codes_by_county(self): """Test reverse mapping from county to zip codes.""" from efile.utils.zip_to_county_il import get_zips_by_county - - cook_zips = get_zips_by_county('Cook') + + cook_zips = get_zips_by_county("Cook") assert len(cook_zips) > 50 # Cook County has many zip codes - assert '60601' in cook_zips - assert '60614' in cook_zips - + assert "60601" in cook_zips + assert "60614" in cook_zips + # Test another county - champaign_zips = get_zips_by_county('Champaign') + champaign_zips = get_zips_by_county("Champaign") assert len(champaign_zips) > 0 - assert '61820' in champaign_zips - + assert "61820" in champaign_zips + def test_get_zip_codes_by_county_invalid(self): """Test handling of invalid county names.""" from efile.utils.zip_to_county_il import get_zips_by_county - - assert get_zips_by_county('InvalidCounty') == [] - assert get_zips_by_county('') == [] + + assert get_zips_by_county("InvalidCounty") == [] + assert get_zips_by_county("") == [] assert get_zips_by_county(None) == [] @@ -301,66 +295,67 @@ def test_get_zip_codes_by_county_invalid(self): # DROPDOWN VIEW HELPER TESTS # ============================================================================ + class TestDropdownViewHelpers: """Test suite for dropdown view helper functions.""" - + def test_prioritize_courts_by_location(self): """Test court prioritization based on user location.""" from efile.api.dropdown_views import DropdownAPIViews - + courts = [ - {'value': 'cook:law1', 'text': 'Cook County Law Division'}, - {'value': 'will:law1', 'text': 'Will County Law Division'}, - {'value': 'dupage:law1', 'text': 'DuPage County Law Division'}, + {"value": "cook:law1", "text": "Cook County Law Division"}, + {"value": "will:law1", "text": "Will County Law Division"}, + {"value": "dupage:law1", "text": "DuPage County Law Division"}, ] - + # Test Cook County user - prioritized = DropdownAPIViews._prioritize_courts_by_location(courts, user_zip='60601', user_county='Cook') - cook_courts = [c for c in prioritized if 'cook' in c['value'].lower()] + prioritized = DropdownAPIViews._prioritize_courts_by_location(courts, user_zip="60601", user_county="Cook") + cook_courts = [c for c in prioritized if "cook" in c["value"].lower()] assert len(cook_courts) > 0 - + # Check that Cook County courts have recommended flag - has_recommended = any(c.get('recommended') or c.get('default') for c in cook_courts) + has_recommended = any(c.get("recommended") or c.get("default") for c in cook_courts) assert has_recommended - + # Cook County courts should be first first_court = prioritized[0] - assert 'cook' in first_court['value'].lower() - + assert "cook" in first_court["value"].lower() + def test_dropdown_view_class_can_be_imported(self): """Test that DropdownAPIViews class can be imported.""" from efile.api.dropdown_views import DropdownAPIViews - + # Should be able to create instance (even though methods are static) view = DropdownAPIViews() assert view is not None - + # Should have required static methods - assert hasattr(DropdownAPIViews, '_prioritize_courts_by_location') - assert hasattr(DropdownAPIViews, 'get_case_categories') - assert hasattr(DropdownAPIViews, 'get_courts') - + assert hasattr(DropdownAPIViews, "_prioritize_courts_by_location") + assert hasattr(DropdownAPIViews, "get_case_categories") + assert hasattr(DropdownAPIViews, "get_courts") + def test_prioritize_courts_by_location_no_data(self): """Test court prioritization with no courts data.""" from efile.api.dropdown_views import DropdownAPIViews - + # Test with empty list result = DropdownAPIViews._prioritize_courts_by_location([]) assert result == [] - + # Test with None result = DropdownAPIViews._prioritize_courts_by_location(None) assert result is None - + def test_prioritize_courts_by_location_no_user_data(self): """Test court prioritization with no user location data.""" from efile.api.dropdown_views import DropdownAPIViews - + courts = [ - {'value': 'cook:law1', 'text': 'Cook County Law Division'}, - {'value': 'will:law1', 'text': 'Will County Law Division'}, + {"value": "cook:law1", "text": "Cook County Law Division"}, + {"value": "will:law1", "text": "Will County Law Division"}, ] - + # Without user location, should return courts unchanged result = DropdownAPIViews._prioritize_courts_by_location(courts) assert result == courts @@ -370,31 +365,29 @@ def test_prioritize_courts_by_location_no_user_data(self): # INTEGRATION TESTS # ============================================================================ + class TestExpertFormIntegration: """Integration tests for the complete expert form flow.""" - + @pytest.fixture def authenticated_client(self, client, db): """Create an authenticated client.""" - user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpass123' - ) + user = User.objects.create_user(username="testuser", email="test@example.com", password="testpass123") client.force_login(user) return client - + def test_expert_form_page_loads(self, authenticated_client): """Test that the expert form page loads correctly.""" - response = authenticated_client.get('/expert_form/') - + response = authenticated_client.get("/expert_form/") + assert response.status_code == 200 - assert b'Case Details & Parties' in response.content or b'Expert Form' in response.content - assert b'cascading-dropdowns.js' in response.content or b'dynamic-form-sections.js' in response.content - - @patch('efile.api.dropdown_views.requests.get') + assert b"Case Details & Parties" in response.content or b"Expert Form" in response.content + assert b"cascading-dropdowns.js" in response.content or b"dynamic-form-sections.js" in response.content + + @patch("efile.api.dropdown_views.requests.get") def test_complete_dropdown_flow(self, mock_get, authenticated_client): """Test the complete dropdown cascade flow.""" + # Mock external API responses def mock_api_response(*args, **kwargs): url = args[0] @@ -402,55 +395,53 @@ def mock_api_response(*args, **kwargs): mock_response.status_code = 200 # Set status code properly mock_response.raise_for_status.return_value = None - if 'courts' in url: + if "courts" in url: # Return list format as the API actually does - mock_response.json.return_value = [ - {'code': 'cook:law1', 'name': 'Cook County Law'} - ] - elif 'case_categories' in url or 'categories' in url: + mock_response.json.return_value = [{"code": "cook:law1", "name": "Cook County Law"}] + elif "case_categories" in url or "categories" in url: # Return list format as the API actually does - mock_response.json.return_value = [ - {'code': 'civil', 'name': 'Civil Law'} - ] - elif 'case_types' in url: + mock_response.json.return_value = [{"code": "civil", "name": "Civil Law"}] + elif "case_types" in url: # Return list format as the API actually does - mock_response.json.return_value = [ - {'code': 'contract', 'name': 'Contract Dispute'} - ] + mock_response.json.return_value = [{"code": "contract", "name": "Contract Dispute"}] return mock_response mock_get.side_effect = mock_api_response # Test courts API - response = authenticated_client.get('/api/dropdowns/courts/', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = authenticated_client.get("/api/dropdowns/courts/", HTTP_X_REQUESTED_WITH="XMLHttpRequest") assert response.status_code == 200 # Test case categories API with court - response = authenticated_client.get('/api/dropdowns/case-categories/', - {'court': 'cook:law1'}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - assert response.status_code == 200 # Test case types API with category - response = authenticated_client.get('/api/dropdowns/case-types/', - {'parent': 'civil', 'court': 'cook:law1'}, # Use 'parent' not 'category' - HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = authenticated_client.get( + "/api/dropdowns/case-categories/", {"court": "cook:law1"}, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + ) + assert response.status_code == 200 # Test case types API with category + response = authenticated_client.get( + "/api/dropdowns/case-types/", + {"parent": "civil", "court": "cook:law1"}, # Use 'parent' not 'category' + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) assert response.status_code == 200 - + def test_form_submission_validation(self, authenticated_client): """Test form submission with validation.""" - response = authenticated_client.post('/expert_form/', { - 'court': 'cook:law1', - 'case_category': 'civil', - 'case_type': 'contract', - 'filing_type': 'complaint', - 'document_type': 'motion', - 'petitioner_first_name': 'John', - 'petitioner_last_name': 'Doe', - 'new_first_name': 'Jane', - 'new_last_name': 'Smith', - }) - + response = authenticated_client.post( + "/expert_form/", + { + "court": "cook:law1", + "case_category": "civil", + "case_type": "contract", + "filing_type": "complaint", + "document_type": "motion", + "petitioner_first_name": "John", + "petitioner_last_name": "Doe", + "new_first_name": "Jane", + "new_last_name": "Smith", + }, + ) + # Should redirect or show success (depending on implementation) assert response.status_code in [200, 302] @@ -459,46 +450,48 @@ def test_form_submission_validation(self, authenticated_client): # ERROR HANDLING TESTS # ============================================================================ + class TestErrorHandling: """Test suite for error handling scenarios.""" - + def test_api_without_ajax_header(self, client): """Test API endpoints work without AJAX headers (currently allowed).""" - response = client.get('/api/dropdowns/courts/') - + response = client.get("/api/dropdowns/courts/") + # Currently the API allows non-AJAX requests assert response.status_code == 200 data = json.loads(response.content) - + # Should still return API response structure - assert 'success' in data - - @patch('efile.api.dropdown_views.requests.get') + assert "success" in data + + @patch("efile.api.dropdown_views.requests.get") def test_external_api_timeout(self, mock_get, client): """Test handling of external API timeouts.""" mock_get.side_effect = Exception("Connection timeout") - response = client.get('/api/dropdowns/courts/', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = client.get("/api/dropdowns/courts/", HTTP_X_REQUESTED_WITH="XMLHttpRequest") # The API should return a 400 error status for timeout assert response.status_code == 400 data = json.loads(response.content) - assert data['success'] is False - assert 'error' in data + assert data["success"] is False + assert "error" in data data = json.loads(response.content) - assert data['success'] is False - assert 'error' in data - + assert data["success"] is False + assert "error" in data + def test_malformed_requests(self, client): """Test handling of malformed API requests.""" # Test with invalid parameters - response = client.get('/api/dropdowns/case-categories/', - {'court': ''}, # Empty court parameter - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + response = client.get( + "/api/dropdowns/case-categories/", + {"court": ""}, # Empty court parameter + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + assert response.status_code == 400 data = json.loads(response.content) - assert data['success'] is False - assert 'error' in data - assert data['success'] is False + assert data["success"] is False + assert "error" in data + assert data["success"] is False diff --git a/efile_app/efile/urls.py b/efile_app/efile/urls.py index e1a14f4..3c06f8b 100644 --- a/efile_app/efile/urls.py +++ b/efile_app/efile/urls.py @@ -1,52 +1,60 @@ -from django.urls import path, include +from django.urls import include, path -from .views.register import efile_register +from .views.api_views import get_case_data +from .views.confirmation import filing_confirmation +from .views.expert_form import efile_expert_form from .views.login import efile_login, efile_logout from .views.options import efile_options -from .views.expert_form import efile_expert_form -from .views.upload import efile_upload, create_filing, upload_documents, test_s3_connection, simple_s3_upload, mock_s3_upload +from .views.register import efile_register from .views.review import case_review -from .views.confirmation import filing_confirmation -from .views.session_api import save_form_data_to_session, save_upload_data_to_session, get_upload_data_from_session, submit_final_filing, clear_session_data, debug_session_data -from .views.api_views import get_case_data -from . import views +from .views.session_api import ( + clear_session_data, + debug_session_data, + get_upload_data_from_session, + save_form_data_to_session, + save_upload_data_to_session, + submit_final_filing, +) +from .views.upload import ( + create_filing, + efile_upload, + mock_s3_upload, + simple_s3_upload, + test_s3_connection, + upload_documents, +) urlpatterns = [ - path('login/', efile_login, name='efile_login'), - path('logout/', efile_logout, name='efile_logout'), - path('register/', efile_register, name='efile_register'), - path('options/', efile_options, name='efile_options'), - path('expert_form/', efile_expert_form, name='expert_form'), - path('upload/', efile_upload, name='upload'), - path('review/', case_review, name='case_review'), - path('filing-confirmation/', filing_confirmation, name='filing_confirmation'), - + path("login/", efile_login, name="efile_login"), + path("logout/", efile_logout, name="efile_logout"), + path("register/", efile_register, name="efile_register"), + path("options/", efile_options, name="efile_options"), + path("expert_form/", efile_expert_form, name="expert_form"), + path("upload/", efile_upload, name="upload"), + path("review/", case_review, name="case_review"), + path("filing-confirmation/", filing_confirmation, name="filing_confirmation"), # Session API endpoints - path('api/get-case-data/', get_case_data, name='get_case_data_from_session'), - path('api/save-case-data/', save_form_data_to_session, name='save_form_data_to_session'), - path('api/save-upload-data/', save_upload_data_to_session, name='save_upload_data_to_session'), - path('api/get-upload-data/', get_upload_data_from_session, name='get_upload_data_from_session'), - path('api/submit-final-filing/', submit_final_filing, name='submit_final_filing'), - path('api/clear-session/', clear_session_data, name='clear_session_data'), - path('api/debug-session/', debug_session_data, name='debug_session_data'), - path('api/debug-session-data/', debug_session_data, name='debug_session_data'), - + path("api/get-case-data/", get_case_data, name="get_case_data_from_session"), + path("api/save-case-data/", save_form_data_to_session, name="save_form_data_to_session"), + path("api/save-upload-data/", save_upload_data_to_session, name="save_upload_data_to_session"), + path("api/get-upload-data/", get_upload_data_from_session, name="get_upload_data_from_session"), + path("api/submit-final-filing/", submit_final_filing, name="submit_final_filing"), + path("api/clear-session/", clear_session_data, name="clear_session_data"), + path("api/debug-session/", debug_session_data, name="debug_session_data"), + path("api/debug-session-data/", debug_session_data, name="debug_session_data"), # API endpoints for upload functionality - path('api/create-filing/', create_filing, name='create_filing'), - path('api/upload-documents/', upload_documents, name='upload_documents'), - path('api/simple-s3-upload/', simple_s3_upload, name='simple_s3_upload'), - path('api/mock-s3-upload/', mock_s3_upload, name='mock_s3_upload'), - path('api/test-s3-connection/', test_s3_connection, name='test_s3_connection'), - + path("api/create-filing/", create_filing, name="create_filing"), + path("api/upload-documents/", upload_documents, name="upload_documents"), + path("api/simple-s3-upload/", simple_s3_upload, name="simple_s3_upload"), + path("api/mock-s3-upload/", mock_s3_upload, name="mock_s3_upload"), + path("api/test-s3-connection/", test_s3_connection, name="test_s3_connection"), # API endpoints for dropdowns - path('api/', include('efile.api.urls')), - + path("api/", include("efile.api.urls")), # Legacy endpoints for backward compatibility (can be removed later) # path('api/get-case-categories/', views.efile_logout, name='get_case_categories_legacy'), # path('api/get-case-types/', views.efile_logout, name='get_case_types_legacy'), # path('api/get-filing-types/', views.efile_logout, name='get_filing_types_legacy'), # path('api/get-counties/', views.efile_logout, name='get_counties_legacy'), # path('api/get-document-types/', views.efile_logout, name='get_document_types_legacy'), - # path('dashboard/', views.dashboard, name='dashboard'), ] diff --git a/efile_app/efile/utils/__init__.py b/efile_app/efile/utils/__init__.py new file mode 100644 index 0000000..c85d6f0 --- /dev/null +++ b/efile_app/efile/utils/__init__.py @@ -0,0 +1 @@ +# Marks efile.utils as a package for imports and type checking. diff --git a/efile_app/efile/utils/case_data_utils.py b/efile_app/efile/utils/case_data_utils.py index bb3ac96..b47cc9b 100644 --- a/efile_app/efile/utils/case_data_utils.py +++ b/efile_app/efile/utils/case_data_utils.py @@ -2,11 +2,13 @@ Utility functions for handling case data throughout the application """ + def get_case_data(request): """ Get case data from session with safe defaults """ - return request.session.get('case_data', {}) + return request.session.get("case_data", {}) + def update_case_data(request, updates): """ @@ -14,58 +16,64 @@ def update_case_data(request, updates): """ case_data = get_case_data(request) case_data.update(updates) - request.session['case_data'] = case_data + request.session["case_data"] = case_data request.session.modified = True return case_data + def clear_case_data(request): """ Clear all case data from session """ - if 'case_data' in request.session: - del request.session['case_data'] + if "case_data" in request.session: + del request.session["case_data"] request.session.modified = True + def get_petitioner_info(request): """ Get petitioner information specifically """ case_data = get_case_data(request) + full_name = f"{case_data.get('petitioner_first_name', '')} {case_data.get('petitioner_last_name', '')}".strip() return { - 'first_name': case_data.get('petitioner_first_name', ''), - 'last_name': case_data.get('petitioner_last_name', ''), - 'address': case_data.get('petitioner_address', ''), - 'full_name': f"{case_data.get('petitioner_first_name', '')} {case_data.get('petitioner_last_name', '')}".strip() + "first_name": case_data.get("petitioner_first_name", ""), + "last_name": case_data.get("petitioner_last_name", ""), + "address": case_data.get("petitioner_address", ""), + "full_name": full_name, } + def get_name_sought_info(request): """ Get name sought information specifically """ case_data = get_case_data(request) return { - 'first_name': case_data.get('new_first_name', ''), - 'last_name': case_data.get('new_last_name', ''), - 'full_name': f"{case_data.get('new_first_name', '')} {case_data.get('new_last_name', '')}".strip() + "first_name": case_data.get("new_first_name", ""), + "last_name": case_data.get("new_last_name", ""), + "full_name": f"{case_data.get('new_first_name', '')} {case_data.get('new_last_name', '')}".strip(), } + def get_case_classification(request): """ Get case classification information """ case_data = get_case_data(request) return { - 'court': case_data.get('court', ''), - 'case_category': case_data.get('case_category', ''), - 'case_type': case_data.get('case_type', ''), - 'filing_type': case_data.get('filing_type', ''), - 'document_type': case_data.get('document_type', ''), - 'is_name_change': 'name change' in case_data.get('case_type', '').lower() + "court": case_data.get("court", ""), + "case_category": case_data.get("case_category", ""), + "case_type": case_data.get("case_type", ""), + "filing_type": case_data.get("filing_type", ""), + "document_type": case_data.get("document_type", ""), + "is_name_change": "name change" in case_data.get("case_type", "").lower(), } + def get_selected_services(request): """ Get list of selected optional services """ case_data = get_case_data(request) - return case_data.get('optional_services', []) + return case_data.get("optional_services", []) diff --git a/efile_app/efile/utils/s3_upload.py b/efile_app/efile/utils/s3_upload.py index 21a93b1..8762af3 100644 --- a/efile_app/efile/utils/s3_upload.py +++ b/efile_app/efile/utils/s3_upload.py @@ -1,36 +1,38 @@ """ S3 Upload utilities for handling document uploads to AWS S3 """ -import boto3 -from botocore.exceptions import ClientError, NoCredentialsError -from django.conf import settings -from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile + +import logging +import mimetypes import uuid from urllib.parse import quote -import mimetypes -import logging + +import boto3 +from botocore.exceptions import ClientError +from django.conf import settings logger = logging.getLogger(__name__) + class S3UploadHandler: """Handles file uploads to AWS S3 and generates secure URLs""" - + def __init__(self): """Initialize S3 upload handler with AWS configuration.""" self.s3_client = None self.credentials_checked = False - + def _ensure_initialized(self): """Ensure S3 client is initialized - check credentials dynamically.""" if self.s3_client is not None: return True - + # Get credentials from Django settings - self.access_key_id = getattr(settings, 'AWS_ACCESS_KEY_ID', '') - self.secret_access_key = getattr(settings, 'AWS_SECRET_ACCESS_KEY', '') - self.bucket_name = getattr(settings, 'AWS_S3_BUCKET_NAME', '') - self.region_name = getattr(settings, 'AWS_S3_REGION_NAME', 'us-east-1') - + self.access_key_id = getattr(settings, "AWS_ACCESS_KEY_ID", "") + self.secret_access_key = getattr(settings, "AWS_SECRET_ACCESS_KEY", "") + self.bucket_name = getattr(settings, "AWS_S3_BUCKET_NAME", "") + self.region_name = getattr(settings, "AWS_S3_REGION_NAME", "us-east-1") + # Check for required credentials if not all([self.access_key_id, self.secret_access_key, self.bucket_name]): if not self.credentials_checked: @@ -43,48 +45,45 @@ def _ensure_initialized(self): else: self._initialize_s3_client() return self.s3_client is not None - + def _initialize_s3_client(self): """Initialize the S3 client and verify bucket access.""" try: self.s3_client = boto3.client( - 's3', + "s3", aws_access_key_id=self.access_key_id, aws_secret_access_key=self.secret_access_key, - region_name=self.region_name + region_name=self.region_name, ) - + # Test connection by attempting to list objects (limited test) try: - self.s3_client.list_objects_v2( - Bucket=self.bucket_name, - MaxKeys=1 - ) + self.s3_client.list_objects_v2(Bucket=self.bucket_name, MaxKeys=1) print(f"S3 connection successful to bucket: {self.bucket_name}") except ClientError as e: - error_code = e.response['Error']['Code'] - if error_code == '403': + error_code = e.response["Error"]["Code"] + if error_code == "403": print(f"Warning: Access denied to S3 bucket {self.bucket_name}. Check credentials and permissions.") - elif error_code == '404': + elif error_code == "404": print(f"Warning: S3 bucket {self.bucket_name} not found.") else: print(f"Warning: S3 bucket access test failed: {e}") # Don't raise exception here, allow the client to be initialized # so we can test credentials later with proper error handling - + except Exception as e: print(f"Failed to initialize S3 client: {e}") self.s3_client = None - - def upload_file(self, file_obj, file_type='document', metadata=None): + + def upload_file(self, file_obj, file_type="document", metadata=None): """ Upload a file to S3 and return the URL - + Args: file_obj: Django UploadedFile object file_type: Type of file (e.g., 'lead_document', 'supporting_document') metadata: Optional metadata dictionary - + Returns: dict: { 'success': bool, @@ -94,83 +93,74 @@ def upload_file(self, file_obj, file_type='document', metadata=None): } """ if not self._ensure_initialized(): - return { - 'success': False, - 'error': 'S3 client not initialized - check AWS credentials' - } - + return {"success": False, "error": "S3 client not initialized - check AWS credentials"} + try: # Generate unique filename file_extension = self._get_file_extension(file_obj.name) unique_filename = f"{uuid.uuid4().hex}{file_extension}" - + # Create S3 key with folder structure s3_key = f"efile-documents/{file_type}/{unique_filename}" - + # Prepare metadata s3_metadata = { - 'original-filename': quote(file_obj.name), - 'file-type': file_type, - 'upload-timestamp': str(int(uuid.uuid1().time)) + "original-filename": quote(file_obj.name), + "file-type": file_type, + "upload-timestamp": str(int(uuid.uuid1().time)), } - + if metadata: s3_metadata.update(metadata) - + # Determine content type - content_type = file_obj.content_type or mimetypes.guess_type(file_obj.name)[0] or 'application/octet-stream' - + content_type = file_obj.content_type or mimetypes.guess_type(file_obj.name)[0] or "application/octet-stream" + # Upload file self.s3_client.upload_fileobj( file_obj, self.bucket_name, s3_key, ExtraArgs={ - 'ContentType': content_type, - 'Metadata': s3_metadata, + "ContentType": content_type, + "Metadata": s3_metadata, # Set appropriate permissions for efile access - 'ACL': 'private' # Keep private, we'll generate signed URLs - } + "ACL": "private", # Keep private, we'll generate signed URLs + }, ) - + # Generate the URL that can be used for efile submission file_url = self._generate_file_url(s3_key) - + logger.info(f"Successfully uploaded file {file_obj.name} to S3: {s3_key}") - + return { - 'success': True, - 'url': file_url, - 'key': s3_key, - 'bucket': self.bucket_name, - 'filename': file_obj.name, - 'size': file_obj.size, - 'content_type': content_type + "success": True, + "url": file_url, + "key": s3_key, + "bucket": self.bucket_name, + "filename": file_obj.name, + "size": file_obj.size, + "content_type": content_type, } - + except ClientError as e: error_msg = f"Failed to upload file to S3: {e}" logger.error(error_msg) - return { - 'success': False, - 'error': error_msg - } + return {"success": False, "error": error_msg} except Exception as e: error_msg = f"Unexpected error during S3 upload: {e}" logger.error(error_msg) - return { - 'success': False, - 'error': error_msg - } - + return {"success": False, "error": error_msg} + def _generate_file_url(self, s3_key, expiration=3600): """ Generate a presigned URL for the uploaded file - + Args: s3_key: S3 object key expiration: URL expiration time in seconds (default: 1 hour) - + Returns: str: Presigned URL """ @@ -178,87 +168,83 @@ def _generate_file_url(self, s3_key, expiration=3600): # For efile submission, we might need a longer-lived URL # or use a different approach depending on Suffolk's requirements presigned_url = self.s3_client.generate_presigned_url( - 'get_object', - Params={'Bucket': self.bucket_name, 'Key': s3_key}, - ExpiresIn=expiration + "get_object", Params={"Bucket": self.bucket_name, "Key": s3_key}, ExpiresIn=expiration ) - + return presigned_url - + except ClientError as e: logger.error(f"Failed to generate presigned URL: {e}") # Fallback to public URL if needed return f"https://{self.bucket_name}.s3.{self.region_name}.amazonaws.com/{s3_key}" - + def get_public_url(self, s3_key): """ Get the public URL for an S3 object (for efile submission) """ return f"https://{self.bucket_name}.s3.{self.region_name}.amazonaws.com/{s3_key}" - + def delete_file(self, s3_key): """ Delete a file from S3 - + Args: s3_key: S3 object key - + Returns: dict: {'success': bool, 'error': str (if failed)} """ try: self.s3_client.delete_object(Bucket=self.bucket_name, Key=s3_key) logger.info(f"Successfully deleted file from S3: {s3_key}") - return {'success': True} - + return {"success": True} + except ClientError as e: error_msg = f"Failed to delete file from S3: {e}" logger.error(error_msg) - return {'success': False, 'error': error_msg} - + return {"success": False, "error": error_msg} + def _get_file_extension(self, filename): """Extract file extension from filename""" - if '.' in filename: - return '.' + filename.rsplit('.', 1)[1].lower() - return '' - + if "." in filename: + return "." + filename.rsplit(".", 1)[1].lower() + return "" + def validate_file(self, file_obj, max_size_mb=10, allowed_types=None): """ Validate uploaded file - + Args: file_obj: Django UploadedFile object max_size_mb: Maximum file size in MB allowed_types: List of allowed file extensions - + Returns: dict: {'valid': bool, 'error': str (if invalid)} """ if allowed_types is None: - allowed_types = ['.pdf', '.doc', '.docx', '.txt'] - + allowed_types = [".pdf", ".doc", ".docx", ".txt"] + # Check file size max_size_bytes = max_size_mb * 1024 * 1024 if file_obj.size > max_size_bytes: - return { - 'valid': False, - 'error': f'File size exceeds {max_size_mb}MB limit' - } - + return {"valid": False, "error": f"File size exceeds {max_size_mb}MB limit"} + # Check file type file_extension = self._get_file_extension(file_obj.name) if file_extension not in allowed_types: return { - 'valid': False, - 'error': f'File type {file_extension} not allowed. Allowed types: {", ".join(allowed_types)}' + "valid": False, + "error": f"File type {file_extension} not allowed. Allowed types: {', '.join(allowed_types)}", } - + # Additional validation for PDF files - if file_extension == '.pdf': + if file_extension == ".pdf": # You could add PDF-specific validation here pass - - return {'valid': True} + + return {"valid": True} + # Global instance s3_handler = S3UploadHandler() diff --git a/efile_app/efile/utils/zip_to_county_il.py b/efile_app/efile/utils/zip_to_county_il.py index f318f41..918dd85 100644 --- a/efile_app/efile/utils/zip_to_county_il.py +++ b/efile_app/efile/utils/zip_to_county_il.py @@ -1,4 +1,3 @@ - """ Illinois Zip Code to County Mapping Utility @@ -6,314 +5,1760 @@ Source: https://www.zip-codes.com/state/il.asp#zipcodes """ +# ruff: noqa: E501 # Grouped mapping: county -> list of zip codes COUNTY_TO_ZIPS_IL = { # Each county is listed with all its zip codes, formatted for readability - 'Adams': [ - '62301', '62305', '62306', '62311', '62312', '62313', '62314', '62316', '62319', '62320', '62321', '62323', '62324', '62325', '62329', '62330', '62334', '62336', '62338', '62339', '62340', '62341', '62343', '62344', '62345', '62346', '62347', '62348', '62349', '62351', '62352', '62353', '62354', '62355', '62356', '62357', '62358', '62359', '62360', '62362', '62363', '62365', '62366', '62367', '62370', '62373', '62374', '62375', '62376', '62378', '62379', '62380', - ], - 'Alexander': [ - '62914', '62957', '62962', '62969', '62988', '62990', '62993', '62994', - ], - 'Bond': [ - '62246', '62262', '62265', '62275', '62284', - ], - 'Boone': [ - '61008', '61011', '61012', '61038', '61065', - ], - 'Brown': [ - '62353', '62375', '62378', - ], - 'Bureau': [ - '61312', '61314', '61315', '61316', '61317', '61318', '61320', '61322', '61323', '61324', '61325', '61326', '61327', '61328', '61329', '61330', '61331', '61332', '61333', '61334', '61335', '61336', '61337', '61338', '61339', '61340', '61341', '61342', '61344', '61345', '61346', '61347', '61348', '61349', '61350', '61353', '61354', '61356', '61358', '61359', '61360', '61361', '61362', '61363', '61364', '61367', '61368', '61369', '61370', '61371', '61372', '61373', '61374', '61375', '61376', '61377', '61378', '61379', - ], - 'Calhoun': [ - '62036', '62045', '62047', '62053', '62065', '62070', - ], - 'Carroll': [ - '61014', '61046', '61051', '61053', '61074', '61078', '61285', - ], - 'Cass': [ - '62611', '62612', '62618', '62622', '62627', '62691', - ], - 'Champaign': [ - '61815', '61816', '61820', '61821', '61822', '61824', '61825', '61826', '61840', '61843', '61845', '61847', '61849', '61851', '61852', '61853', '61859', '61862', '61863', '61864', '61871', '61872', '61873', '61874', '61875', '61877', '61878', '61880', '61949', - ], - 'Christian': [ - '62510', '62517', '62531', '62540', '62546', '62547', '62550', '62555', '62556', '62557', '62567', '62568', '62570', '62083', - ], - 'Clark': [ - '62420', '62423', '62441', '62442', '62474', '62477', - ], - 'Clay': [ - '62434', '62824', '62839', '62858', '62879', '62899', - ], - 'Clinton': [ - '62215', '62216', '62218', '62219', '62230', '62231', '62245', '62250', '62252', '62253', '62265', '62266', '62293', - ], - 'Coles': [ - '61912', '61920', '61931', '61938', '61943', '62440', '62469', - ], - 'Cook': [ - '60004', '60005', '60006', '60007', '60008', '60009', '60016', '60017', '60018', '60019', '60022', '60025', '60026', '60029', '60038', '60043', '60053', '60055', '60056', '60062', '60065', '60067', '60068', '60070', '60074', '60076', '60077', '60078', '60082', '60090', '60091', '60093', '60094', '60095', '60104', '60107', '60130', '60131', '60133', '60141', '60153', '60154', '60155', '60159', '60160', '60161', '60162', '60163', '60164', '60165', '60169', '60171', '60173', '60176', '60179', '60192', '60193', '60194', '60195', '60196', '60201', '60202', '60203', '60204', '60208', '60301', '60302', '60303', '60304', '60305', '60399', '60402', '60403', '60406', '60409', '60411', '60412', '60415', '60419', '60422', '60423', '60425', '60426', '60428', '60429', '60430', '60431', '60432', '60433', '60434', '60435', '60436', '60438', '60439', '60440', '60441', '60442', '60443', '60445', '60446', '60447', '60448', '60449', '60450', '60451', '60452', '60453', '60455', '60456', '60457', '60458', '60459', '60460', '60461', '60462', '60463', '60464', '60465', '60466', '60467', '60468', '60469', '60471', '60472', '60473', '60474', '60475', '60476', '60477', '60478', '60480', '60481', '60482', '60484', '60487', '60490', '60491', '60499', '60501', '60513', '60521', '60522', '60523', '60525', '60526', '60534', '60546', '60558', '60601', '60602', '60603', '60604', '60605', '60606', '60607', '60608', '60610', '60611', '60612', '60613', '60614', '60615', '60616', '60617', '60618', '60619', '60620', '60621', '60622', '60623', '60624', '60625', '60626', '60628', '60629', '60630', '60631', '60632', '60633', '60634', '60636', '60637', '60638', '60639', '60640', '60641', '60642', '60643', '60644', '60645', '60646', '60647', '60649', '60651', '60652', '60653', '60654', '60655', '60656', '60657', '60659', '60660', '60661', '60696', '60697', '60699', '60701', '60706', '60707', '60712', '60714', '60803', '60804', '60805', '60827', - ], - 'Crawford': [ - '62413', '62427', '62433', '62449', '62451', '62454', '62464', '62478', - ], - 'Cumberland': [ - '62428', '62436', '62447', '62468', - ], - 'DeKalb': [ - '60111', '60112', '60115', '60129', '60135', '60145', '60146', '60150', '60151', '60178', '60520', '60548', '60550', '60552', '60556', - ], - 'DeWitt': [ - '61727', '61735', '61749', '61750', '61777', '61842', '61882', - ], - 'Douglas': [ - '61910', '61911', '61913', '61919', '61930', '61941', '61942', '61953', '61956', - ], - 'DuPage': [ - '60101', '60103', '60105', '60106', '60108', '60116', '60117', '60122', '60126', '60128', '60132', '60137', '60138', '60139', '60143', '60147', '60148', '60157', '60168', '60172', '60181', '60183', '60184', '60185', '60186', '60187', '60188', '60189', '60190', '60191', '60399', '60502', '60504', '60514', '60515', '60516', '60517', '60519', '60521', '60523', '60527', '60532', '60540', '60555', '60559', '60561', '60563', '60565', '60566', '60567', '60569', '60572', '60598', '60599', - ], - 'Edgar': [ - '61917', '61924', '61932', '61933', '61940', '61944', '61949', '61955', - ], - 'Edwards': [ - '62476', '62806', '62815', '62818', '62833', - ], - 'Effingham': [ - '62401', '62411', '62414', '62424', '62426', '62443', '62445', '62461', '62467', '62473', - ], - 'Fayette': [ - '62080', '62418', '62458', '62471', '62838', '62880', - ], - 'Ford': [ - '60919', '60933', '60936', '60952', '60957', '60959', '60962', '61773', - ], - 'Franklin': [ - '62812', '62819', '62825', '62836', '62840', '62865', '62874', '62890', '62896', '62897', '62983', '62999', - ], - 'Fulton': [ - '61415', '61427', '61431', '61432', '61433', '61441', '61459', '61477', '61484', '61501', '61519', '61520', '61524', '61531', '61542', '61543', '61544', '61553', '61563', - ], - 'Gallatin': [ - '62867', '62871', '62934', '62954', '62979', '62984', - ], - 'Greene': [ - '62016', '62027', '62044', '62050', '62054', '62078', '62081', '62082', '62092', '62098', - ], - 'Grundy': [ - '60407', '60444', '60447', '60450', - ], - 'Hamilton': [ - '62817', '62828', '62829', '62859', '62860', - ], - 'Hancock': [ - '62311', '62313', '62316', '62321', '62329', '62330', '62334', '62336', '62341', '62354', '62358', '62373', '62379', '62380', '62450', - ], - 'Hardin': [ - '62919', '62931', '62982', - ], - 'Henderson': [ - '61418', '61425', '61437', '61454', '61460', '61469', '61480', - ], - 'Henry': [ - '61233', '61234', '61235', '61238', '61241', '61254', '61258', '61262', '61273', '61274', '61413', '61419', '61434', '61443', '61468', '61490', - ], - 'Iroquois': [ - '60918', '60922', '60924', '60926', '60927', '60928', '60930', '60931', '60938', '60939', '60951', '60953', '60955', '60956', '60966', '60967', '60970', '60973', '60974', - ], - 'Jackson': [ - '62901', '62902', '62903', '62907', '62916', '62924', '62932', '62940', '62942', '62950', '62958', '62966', '62975', '62994', - ], - 'Jasper': [ - '62432', '62448', '62459', '62475', '62479', '62480', '62481', - ], - 'Jefferson': [ - '62810', '62814', '62816', '62830', '62846', '62864', '62866', '62872', '62894', '62898', - ], - 'Jersey': [ - '62022', '62028', '62030', '62031', '62037', '62052', '62063', - ], - 'Jo Daviess': [ - '61001', '61025', '61028', '61036', '61041', '61059', '61062', '61075', '61085', '61087', - ], - 'Johnson': [ - '62909', '62912', '62923', '62939', '62943', '62967', '62972', '62985', '62995', - ], - 'Kane': [ - '60109', '60110', '60118', '60119', '60120', '60121', '60123', '60124', '60134', '60136', '60140', '60144', '60147', '60151', '60170', '60174', '60175', '60177', '60183', '60505', '60506', '60507', '60510', '60511', '60512', '60539', '60542', '60554', '60568', - ], - 'Kankakee': [ - '60901', '60935', '60940', '60950', '60954', '60958', '60961', '60964', '60969', - ], - 'Kendall': [ - '60512', '60536', '60537', '60538', '60541', '60543', '60545', '60560', - ], - 'Knox': [ - '61401', '61402', '61410', '61414', '61428', '61436', '61439', '61448', '61458', '61467', '61485', '61488', '61489', - ], - 'Lake': [ - '60002', '60010', '60011', '60015', '60020', '60030', '60031', '60035', '60036', '60037', '60039', '60040', '60041', '60042', '60044', '60045', '60046', '60047', '60048', '60060', '60061', '60064', '60069', '60073', '60075', '60079', '60083', '60084', '60085', '60086', '60087', '60088', '60089', '60096', '60099', - ], - 'LaSalle': [ - '61301', '61316', '61321', '61325', '61332', '61334', '61341', '61342', '61348', '61350', '61354', '61358', '61360', '61364', '61370', '61371', '61372', '61373', '60518', '60531', '60549', '60551', '60557', - ], - 'Lawrence': [ - '62417', '62439', '62460', '62466', - ], - 'Lee': [ - '61006', '61021', '61031', '61042', '61057', '61310', '61318', '61324', '61331', '61353', '61367', '61378', '60530', '60553', - ], - 'Livingston': [ - '60460', '60311', '60319', '60921', '60929', '60934', '61739', '61740', '61741', '61743', '61764', '61769', '61775', - ], - 'Logan': [ - '61723', '62512', '62518', '62519', '62541', '62543', '62548', '62634', '62635', '62643', '62656', '62666', '62671', '61751', - ], - 'McDonough': [ - '61411', '61416', '61420', '61422', '61438', '61440', '61455', '61326', '62374', - ], - 'McHenry': [ - '60001', '60012', '60013', '60014', '60021', '60033', '60034', '60039', '60050', '60051', '60071', '60072', '60081', '60097', '60098', '60102', '60140', '60142', '60152', '60156', '60180', - ], - 'McLean': [ - '61701', '61702', '61704', '61705', '61709', '61710', '61720', '61722', '61724', '61725', '61728', '61730', '61731', '61732', '61736', '61737', '61744', '61745', '61748', '61752', '61753', '61754', '61758', '61761', '61770', '61772', '61774', '61776', - ], - 'Macon': [ - '62501', '62513', '62514', '62521', '62522', '62523', '62524', '62525', '62526', '62532', '62535', '62537', '62544', '62549', '62551', '62554', '62573', '62756', - ], - 'Macoupin': [ - '62023', '62033', '62630', '62626', '62640', '62649', '62667', '62672', '62674', '62079', '62690', '62093', - ], - 'Madison': [ - '62001', '62002', '62018', '62021', '62024', '62025', '62026', '62034', '62035', '62040', '62046', '62048', '62060', '62061', '62062', '62067', '62074', '62081', '62094', '62095', '62097', '62234', '62249', '62281', '62294', - ], - 'Marion': [ - '62801', '62807', '62849', '62853', '62854', '62870', '62875', '62881', '62882', '62892', '62893', - ], - 'Marshall': [ - '61369', '61375', '61377', '61424', '61537', '61540', '61541', '61565', - ], - 'Mason': [ - '61532', '61546', '61567', '61617', '61644', '61655', '61664', '61682', - ], - 'Massac': [ - '62908', '62910', '62953', '62960', - ], - 'Menard': [ - '62613', '62642', '62659', '62673', '62675', '62688', - ], - 'Mercer': [ - '61231', '61260', '61263', '61272', '61276', '61281', '61412', '61442', '61465', '61466', '61486', - ], - 'Monroe': [ - '62236', '62244', '62248', '62256', '62279', '62295', '62298', - ], - 'Montgomery': [ - '62015', '62017', '62019', '62032', '62049', '62051', '62056', '62075', '62076', '62077', '62091', '62094', '62533', '62538', '62572', - ], - 'Morgan': [ - '62601', '62628', '62631', '62638', '62650', '62651', '62660', '62665', '62668', '62692', '62695', - ], - 'Moultrie': [ - '61914', '61925', '61928', '61937', '61951', - ], - 'Ogle': [ - '60113', '61007', '61015', '61020', '61030', '61043', '61047', '61049', '61052', '61054', '61061', '61084', '61091', - ], - 'Peoria': [ - '61451', '61517', '61525', '61526', '61528', '61529', '61533', '61536', '61547', '61559', '61562', '61601', '61602', '61603', '61604', '61605', '61606', '61607', '61614', '61615', '61625', '61629', '61630', '61633', '61634', '61635', '61636', '61637', '61638', '61639', '61641', '61650', '61651', '61652', '61653', '61654', '61655', '61656', - ], - 'Perry': [ - '62238', '62274', '62997', - ], - 'Piatt': [ - '61813', '61818', '61830', '61839', '61854', '61855', '61856', '61884', '61929', '61936', - ], - 'Pike': [ - '62312', '62314', '62323', '62340', '62343', '62345', '62352', '62355', '62356', '62357', '62361', '62362', '62363', '62366', '62370', - ], - 'Pope': [ - '62928', '62938', '62947', - ], - 'Pulaski': [ - '62941', '62956', '62963', '62964', '62970', '62973', '62976', '62992', '62996', - ], - 'Putnam': [ - '61326', '61327', '61335', '61336', '61340', '61363', '61560', - ], - 'Randolph': [ - '62217', '62233', '62237', '62241', '62242', '62259', '62261', '62272', '62277', '62278', '62280', '62286', '62288', '62292', '62297', - ], - 'Richland': [ - '62419', '62421', '62425', '62450', '62452', '62868', - ], - 'RockIsland': [ - '61201', '61204', '61232', '61236', '61237', '61239', '61240', '61242', '61244', '61256', '61257', '61259', '61264', '61265', '61266', '61275', '61278', '61279', '61282', '61284', '61299', - ], - 'Saline': [ - '62917', '62930', '62935', '62946', '62965', '62977', '62987', - ], - 'Sangamon': [ - '62515', '62520', '62530', '62536', '62539', '62545', '62558', '62561', '62563', '62615', '62625', '62629', '62661', '62662', '62670', '62677', '62684', '62689', '62693', '62701', '62702', '62703', '62704', '62705', '62706', '62707', '62708', '62711', '62712', '62713', '62716', '62719', '62722', '62723', '62726', '62736', '62739', '62746', '62756', '62757', '62761', '62762', '62763', '62764', '62765', '62766', '62767', '62769', '62776', '62777', '62781', '62786', '62791', '62794', '62796', - ], - 'Schuyler': [ - '61452', '62319', '62344', '62367', '62624', '62639', '62681', - ], - 'Scott': [ - '62610', '62621', '62663', '62694', - ], - 'Shelby': [ - '61957', '62422', '62431', '62438', '62444', '62462', '62463', '62465', '62534', '62553', '62565', '62571', - ], - 'StClair': [ - '62059', '62071', '62201', '62202', '62203', '62204', '62205', '62206', '62207', '62208', '62220', '62221', '62222', '62223', '62225', '62226', '62232', '62239', '62240', '62243', '62254', '62255', '62257', '62258', '62260', '62264', '62269', '62282', '62285', '62289', - ], - 'Stark': [ - '61421', '61426', '61449', '61479', '61483', '61491', - ], - 'Stephenson': [ - '61013', '61018', '61019', '61027', '61032', '61039', '61044', '61048', '61050', '61060', '61062', '61089', - ], - 'Tazewell': [ - '61534', '61535', '61550', '61554', '61555', '61558', '61564', '61721', '61733', '61734', '61747', '61755', '61759', - ], - 'Union': [ - '62905', '62906', '62920', '62926', '62952', '62961', '62998', - ], - 'Vermilion': [ - '61810', '61811', '61812', '61814', '61817', '61832', '61833', '61834', '61841', '61844', '61846', '61848', '61850', '61857', '61858', '61865', '61870', '61876', '61883', '60932', '60960', '60963', '60968', '60973', - ], - 'Wabash': [ - '62410', '62811', '62852', '62863', - ], - 'Warren': [ - '61417', '61423', '61435', '61447', '61453', '61462', '61478', - ], - 'Washington': [ - '62214', '62263', '62268', '62271', '62276', '62803', '62808', '62831', '62848', '62876', '62877', - ], - 'Wayne': [ - '62446', '62809', '62823', '62837', '62842', '62850', '62851', '62878', '62895', - ], - 'White': [ - '62820', '62821', '62827', '62834', '62835', '62844', '62861', '62862', '62869', - ], - 'Whiteside': [ - '61037', '61081', '61230', '61243', '61250', '61251', '61252', '61261', '61270', '61277', '61283', - ], - 'Will': [ - '60401', '60403', '60404', '60408', '60410', '60440', '60441', '60442', '60446', '60448', '60449', '60451', '60481', '60484', '60490', '60491', '60503', '60544', '60564', '60585', '60586', - ], - 'Williamson': [ - '62841', '62915', '62918', '62921', '62922', '62933', '62948', '62949', '62951', '62959', '62974', - ], - 'Winnebago': [ - '61016', '61024', '61063', '61073', '61077', '61079', '61080', '61088', '61101', '61102', '61103', '61104', '61105', '61106', '61107', '61108', '61109', '61110', '61111', '61112', '61114', '61115', '61125', '61126', '61130', '61131', '61132', - ], - 'Woodford': [ - '61516', '61530', '61545', '61548', '61560', '61561', '61571', '61729', '61738', '61742', '61760', '61771', + "Adams": [ + "62301", + "62305", + "62306", + "62311", + "62312", + "62313", + "62314", + "62316", + "62319", + "62320", + "62321", + "62323", + "62324", + "62325", + "62329", + "62330", + "62334", + "62336", + "62338", + "62339", + "62340", + "62341", + "62343", + "62344", + "62345", + "62346", + "62347", + "62348", + "62349", + "62351", + "62352", + "62353", + "62354", + "62355", + "62356", + "62357", + "62358", + "62359", + "62360", + "62362", + "62363", + "62365", + "62366", + "62367", + "62370", + "62373", + "62374", + "62375", + "62376", + "62378", + "62379", + "62380", + ], + "Alexander": [ + "62914", + "62957", + "62962", + "62969", + "62988", + "62990", + "62993", + "62994", + ], + "Bond": [ + "62246", + "62262", + "62265", + "62275", + "62284", + ], + "Boone": [ + "61008", + "61011", + "61012", + "61038", + "61065", + ], + "Brown": [ + "62353", + "62375", + "62378", + ], + "Bureau": [ + "61312", + "61314", + "61315", + "61316", + "61317", + "61318", + "61320", + "61322", + "61323", + "61324", + "61325", + "61326", + "61327", + "61328", + "61329", + "61330", + "61331", + "61332", + "61333", + "61334", + "61335", + "61336", + "61337", + "61338", + "61339", + "61340", + "61341", + "61342", + "61344", + "61345", + "61346", + "61347", + "61348", + "61349", + "61350", + "61353", + "61354", + "61356", + "61358", + "61359", + "61360", + "61361", + "61362", + "61363", + "61364", + "61367", + "61368", + "61369", + "61370", + "61371", + "61372", + "61373", + "61374", + "61375", + "61376", + "61377", + "61378", + "61379", + ], + "Calhoun": [ + "62036", + "62045", + "62047", + "62053", + "62065", + "62070", + ], + "Carroll": [ + "61014", + "61046", + "61051", + "61053", + "61074", + "61078", + "61285", + ], + "Cass": [ + "62611", + "62612", + "62618", + "62622", + "62627", + "62691", + ], + "Champaign": [ + "61815", + "61816", + "61820", + "61821", + "61822", + "61824", + "61825", + "61826", + "61840", + "61843", + "61845", + "61847", + "61849", + "61851", + "61852", + "61853", + "61859", + "61862", + "61863", + "61864", + "61871", + "61872", + "61873", + "61874", + "61875", + "61877", + "61878", + "61880", + "61949", + ], + "Christian": [ + "62510", + "62517", + "62531", + "62540", + "62546", + "62547", + "62550", + "62555", + "62556", + "62557", + "62567", + "62568", + "62570", + "62083", + ], + "Clark": [ + "62420", + "62423", + "62441", + "62442", + "62474", + "62477", + ], + "Clay": [ + "62434", + "62824", + "62839", + "62858", + "62879", + "62899", + ], + "Clinton": [ + "62215", + "62216", + "62218", + "62219", + "62230", + "62231", + "62245", + "62250", + "62252", + "62253", + "62265", + "62266", + "62293", + ], + "Coles": [ + "61912", + "61920", + "61931", + "61938", + "61943", + "62440", + "62469", + ], + "Cook": [ + "60004", + "60005", + "60006", + "60007", + "60008", + "60009", + "60016", + "60017", + "60018", + "60019", + "60022", + "60025", + "60026", + "60029", + "60038", + "60043", + "60053", + "60055", + "60056", + "60062", + "60065", + "60067", + "60068", + "60070", + "60074", + "60076", + "60077", + "60078", + "60082", + "60090", + "60091", + "60093", + "60094", + "60095", + "60104", + "60107", + "60130", + "60131", + "60133", + "60141", + "60153", + "60154", + "60155", + "60159", + "60160", + "60161", + "60162", + "60163", + "60164", + "60165", + "60169", + "60171", + "60173", + "60176", + "60179", + "60192", + "60193", + "60194", + "60195", + "60196", + "60201", + "60202", + "60203", + "60204", + "60208", + "60301", + "60302", + "60303", + "60304", + "60305", + "60399", + "60402", + "60403", + "60406", + "60409", + "60411", + "60412", + "60415", + "60419", + "60422", + "60423", + "60425", + "60426", + "60428", + "60429", + "60430", + "60431", + "60432", + "60433", + "60434", + "60435", + "60436", + "60438", + "60439", + "60440", + "60441", + "60442", + "60443", + "60445", + "60446", + "60447", + "60448", + "60449", + "60450", + "60451", + "60452", + "60453", + "60455", + "60456", + "60457", + "60458", + "60459", + "60460", + "60461", + "60462", + "60463", + "60464", + "60465", + "60466", + "60467", + "60468", + "60469", + "60471", + "60472", + "60473", + "60474", + "60475", + "60476", + "60477", + "60478", + "60480", + "60481", + "60482", + "60484", + "60487", + "60490", + "60491", + "60499", + "60501", + "60513", + "60521", + "60522", + "60523", + "60525", + "60526", + "60534", + "60546", + "60558", + "60601", + "60602", + "60603", + "60604", + "60605", + "60606", + "60607", + "60608", + "60610", + "60611", + "60612", + "60613", + "60614", + "60615", + "60616", + "60617", + "60618", + "60619", + "60620", + "60621", + "60622", + "60623", + "60624", + "60625", + "60626", + "60628", + "60629", + "60630", + "60631", + "60632", + "60633", + "60634", + "60636", + "60637", + "60638", + "60639", + "60640", + "60641", + "60642", + "60643", + "60644", + "60645", + "60646", + "60647", + "60649", + "60651", + "60652", + "60653", + "60654", + "60655", + "60656", + "60657", + "60659", + "60660", + "60661", + "60696", + "60697", + "60699", + "60701", + "60706", + "60707", + "60712", + "60714", + "60803", + "60804", + "60805", + "60827", + ], + "Crawford": [ + "62413", + "62427", + "62433", + "62449", + "62451", + "62454", + "62464", + "62478", + ], + "Cumberland": [ + "62428", + "62436", + "62447", + "62468", + ], + "DeKalb": [ + "60111", + "60112", + "60115", + "60129", + "60135", + "60145", + "60146", + "60150", + "60151", + "60178", + "60520", + "60548", + "60550", + "60552", + "60556", + ], + "DeWitt": [ + "61727", + "61735", + "61749", + "61750", + "61777", + "61842", + "61882", + ], + "Douglas": [ + "61910", + "61911", + "61913", + "61919", + "61930", + "61941", + "61942", + "61953", + "61956", + ], + "DuPage": [ + "60101", + "60103", + "60105", + "60106", + "60108", + "60116", + "60117", + "60122", + "60126", + "60128", + "60132", + "60137", + "60138", + "60139", + "60143", + "60147", + "60148", + "60157", + "60168", + "60172", + "60181", + "60183", + "60184", + "60185", + "60186", + "60187", + "60188", + "60189", + "60190", + "60191", + "60399", + "60502", + "60504", + "60514", + "60515", + "60516", + "60517", + "60519", + "60521", + "60523", + "60527", + "60532", + "60540", + "60555", + "60559", + "60561", + "60563", + "60565", + "60566", + "60567", + "60569", + "60572", + "60598", + "60599", + ], + "Edgar": [ + "61917", + "61924", + "61932", + "61933", + "61940", + "61944", + "61949", + "61955", + ], + "Edwards": [ + "62476", + "62806", + "62815", + "62818", + "62833", + ], + "Effingham": [ + "62401", + "62411", + "62414", + "62424", + "62426", + "62443", + "62445", + "62461", + "62467", + "62473", + ], + "Fayette": [ + "62080", + "62418", + "62458", + "62471", + "62838", + "62880", + ], + "Ford": [ + "60919", + "60933", + "60936", + "60952", + "60957", + "60959", + "60962", + "61773", + ], + "Franklin": [ + "62812", + "62819", + "62825", + "62836", + "62840", + "62865", + "62874", + "62890", + "62896", + "62897", + "62983", + "62999", + ], + "Fulton": [ + "61415", + "61427", + "61431", + "61432", + "61433", + "61441", + "61459", + "61477", + "61484", + "61501", + "61519", + "61520", + "61524", + "61531", + "61542", + "61543", + "61544", + "61553", + "61563", + ], + "Gallatin": [ + "62867", + "62871", + "62934", + "62954", + "62979", + "62984", + ], + "Greene": [ + "62016", + "62027", + "62044", + "62050", + "62054", + "62078", + "62081", + "62082", + "62092", + "62098", + ], + "Grundy": [ + "60407", + "60444", + "60447", + "60450", + ], + "Hamilton": [ + "62817", + "62828", + "62829", + "62859", + "62860", + ], + "Hancock": [ + "62311", + "62313", + "62316", + "62321", + "62329", + "62330", + "62334", + "62336", + "62341", + "62354", + "62358", + "62373", + "62379", + "62380", + "62450", + ], + "Hardin": [ + "62919", + "62931", + "62982", + ], + "Henderson": [ + "61418", + "61425", + "61437", + "61454", + "61460", + "61469", + "61480", + ], + "Henry": [ + "61233", + "61234", + "61235", + "61238", + "61241", + "61254", + "61258", + "61262", + "61273", + "61274", + "61413", + "61419", + "61434", + "61443", + "61468", + "61490", + ], + "Iroquois": [ + "60918", + "60922", + "60924", + "60926", + "60927", + "60928", + "60930", + "60931", + "60938", + "60939", + "60951", + "60953", + "60955", + "60956", + "60966", + "60967", + "60970", + "60973", + "60974", + ], + "Jackson": [ + "62901", + "62902", + "62903", + "62907", + "62916", + "62924", + "62932", + "62940", + "62942", + "62950", + "62958", + "62966", + "62975", + "62994", + ], + "Jasper": [ + "62432", + "62448", + "62459", + "62475", + "62479", + "62480", + "62481", + ], + "Jefferson": [ + "62810", + "62814", + "62816", + "62830", + "62846", + "62864", + "62866", + "62872", + "62894", + "62898", + ], + "Jersey": [ + "62022", + "62028", + "62030", + "62031", + "62037", + "62052", + "62063", + ], + "Jo Daviess": [ + "61001", + "61025", + "61028", + "61036", + "61041", + "61059", + "61062", + "61075", + "61085", + "61087", + ], + "Johnson": [ + "62909", + "62912", + "62923", + "62939", + "62943", + "62967", + "62972", + "62985", + "62995", + ], + "Kane": [ + "60109", + "60110", + "60118", + "60119", + "60120", + "60121", + "60123", + "60124", + "60134", + "60136", + "60140", + "60144", + "60147", + "60151", + "60170", + "60174", + "60175", + "60177", + "60183", + "60505", + "60506", + "60507", + "60510", + "60511", + "60512", + "60539", + "60542", + "60554", + "60568", + ], + "Kankakee": [ + "60901", + "60935", + "60940", + "60950", + "60954", + "60958", + "60961", + "60964", + "60969", + ], + "Kendall": [ + "60512", + "60536", + "60537", + "60538", + "60541", + "60543", + "60545", + "60560", + ], + "Knox": [ + "61401", + "61402", + "61410", + "61414", + "61428", + "61436", + "61439", + "61448", + "61458", + "61467", + "61485", + "61488", + "61489", + ], + "Lake": [ + "60002", + "60010", + "60011", + "60015", + "60020", + "60030", + "60031", + "60035", + "60036", + "60037", + "60039", + "60040", + "60041", + "60042", + "60044", + "60045", + "60046", + "60047", + "60048", + "60060", + "60061", + "60064", + "60069", + "60073", + "60075", + "60079", + "60083", + "60084", + "60085", + "60086", + "60087", + "60088", + "60089", + "60096", + "60099", + ], + "LaSalle": [ + "61301", + "61316", + "61321", + "61325", + "61332", + "61334", + "61341", + "61342", + "61348", + "61350", + "61354", + "61358", + "61360", + "61364", + "61370", + "61371", + "61372", + "61373", + "60518", + "60531", + "60549", + "60551", + "60557", + ], + "Lawrence": [ + "62417", + "62439", + "62460", + "62466", + ], + "Lee": [ + "61006", + "61021", + "61031", + "61042", + "61057", + "61310", + "61318", + "61324", + "61331", + "61353", + "61367", + "61378", + "60530", + "60553", + ], + "Livingston": [ + "60460", + "60311", + "60319", + "60921", + "60929", + "60934", + "61739", + "61740", + "61741", + "61743", + "61764", + "61769", + "61775", + ], + "Logan": [ + "61723", + "62512", + "62518", + "62519", + "62541", + "62543", + "62548", + "62634", + "62635", + "62643", + "62656", + "62666", + "62671", + "61751", + ], + "McDonough": [ + "61411", + "61416", + "61420", + "61422", + "61438", + "61440", + "61455", + "61326", + "62374", + ], + "McHenry": [ + "60001", + "60012", + "60013", + "60014", + "60021", + "60033", + "60034", + "60039", + "60050", + "60051", + "60071", + "60072", + "60081", + "60097", + "60098", + "60102", + "60140", + "60142", + "60152", + "60156", + "60180", + ], + "McLean": [ + "61701", + "61702", + "61704", + "61705", + "61709", + "61710", + "61720", + "61722", + "61724", + "61725", + "61728", + "61730", + "61731", + "61732", + "61736", + "61737", + "61744", + "61745", + "61748", + "61752", + "61753", + "61754", + "61758", + "61761", + "61770", + "61772", + "61774", + "61776", + ], + "Macon": [ + "62501", + "62513", + "62514", + "62521", + "62522", + "62523", + "62524", + "62525", + "62526", + "62532", + "62535", + "62537", + "62544", + "62549", + "62551", + "62554", + "62573", + "62756", + ], + "Macoupin": [ + "62023", + "62033", + "62630", + "62626", + "62640", + "62649", + "62667", + "62672", + "62674", + "62079", + "62690", + "62093", + ], + "Madison": [ + "62001", + "62002", + "62018", + "62021", + "62024", + "62025", + "62026", + "62034", + "62035", + "62040", + "62046", + "62048", + "62060", + "62061", + "62062", + "62067", + "62074", + "62081", + "62094", + "62095", + "62097", + "62234", + "62249", + "62281", + "62294", + ], + "Marion": [ + "62801", + "62807", + "62849", + "62853", + "62854", + "62870", + "62875", + "62881", + "62882", + "62892", + "62893", + ], + "Marshall": [ + "61369", + "61375", + "61377", + "61424", + "61537", + "61540", + "61541", + "61565", + ], + "Mason": [ + "61532", + "61546", + "61567", + "61617", + "61644", + "61655", + "61664", + "61682", + ], + "Massac": [ + "62908", + "62910", + "62953", + "62960", + ], + "Menard": [ + "62613", + "62642", + "62659", + "62673", + "62675", + "62688", + ], + "Mercer": [ + "61231", + "61260", + "61263", + "61272", + "61276", + "61281", + "61412", + "61442", + "61465", + "61466", + "61486", + ], + "Monroe": [ + "62236", + "62244", + "62248", + "62256", + "62279", + "62295", + "62298", + ], + "Montgomery": [ + "62015", + "62017", + "62019", + "62032", + "62049", + "62051", + "62056", + "62075", + "62076", + "62077", + "62091", + "62094", + "62533", + "62538", + "62572", + ], + "Morgan": [ + "62601", + "62628", + "62631", + "62638", + "62650", + "62651", + "62660", + "62665", + "62668", + "62692", + "62695", + ], + "Moultrie": [ + "61914", + "61925", + "61928", + "61937", + "61951", + ], + "Ogle": [ + "60113", + "61007", + "61015", + "61020", + "61030", + "61043", + "61047", + "61049", + "61052", + "61054", + "61061", + "61084", + "61091", + ], + "Peoria": [ + "61451", + "61517", + "61525", + "61526", + "61528", + "61529", + "61533", + "61536", + "61547", + "61559", + "61562", + "61601", + "61602", + "61603", + "61604", + "61605", + "61606", + "61607", + "61614", + "61615", + "61625", + "61629", + "61630", + "61633", + "61634", + "61635", + "61636", + "61637", + "61638", + "61639", + "61641", + "61650", + "61651", + "61652", + "61653", + "61654", + "61655", + "61656", + ], + "Perry": [ + "62238", + "62274", + "62997", + ], + "Piatt": [ + "61813", + "61818", + "61830", + "61839", + "61854", + "61855", + "61856", + "61884", + "61929", + "61936", + ], + "Pike": [ + "62312", + "62314", + "62323", + "62340", + "62343", + "62345", + "62352", + "62355", + "62356", + "62357", + "62361", + "62362", + "62363", + "62366", + "62370", + ], + "Pope": [ + "62928", + "62938", + "62947", + ], + "Pulaski": [ + "62941", + "62956", + "62963", + "62964", + "62970", + "62973", + "62976", + "62992", + "62996", + ], + "Putnam": [ + "61326", + "61327", + "61335", + "61336", + "61340", + "61363", + "61560", + ], + "Randolph": [ + "62217", + "62233", + "62237", + "62241", + "62242", + "62259", + "62261", + "62272", + "62277", + "62278", + "62280", + "62286", + "62288", + "62292", + "62297", + ], + "Richland": [ + "62419", + "62421", + "62425", + "62450", + "62452", + "62868", + ], + "RockIsland": [ + "61201", + "61204", + "61232", + "61236", + "61237", + "61239", + "61240", + "61242", + "61244", + "61256", + "61257", + "61259", + "61264", + "61265", + "61266", + "61275", + "61278", + "61279", + "61282", + "61284", + "61299", + ], + "Saline": [ + "62917", + "62930", + "62935", + "62946", + "62965", + "62977", + "62987", + ], + "Sangamon": [ + "62515", + "62520", + "62530", + "62536", + "62539", + "62545", + "62558", + "62561", + "62563", + "62615", + "62625", + "62629", + "62661", + "62662", + "62670", + "62677", + "62684", + "62689", + "62693", + "62701", + "62702", + "62703", + "62704", + "62705", + "62706", + "62707", + "62708", + "62711", + "62712", + "62713", + "62716", + "62719", + "62722", + "62723", + "62726", + "62736", + "62739", + "62746", + "62756", + "62757", + "62761", + "62762", + "62763", + "62764", + "62765", + "62766", + "62767", + "62769", + "62776", + "62777", + "62781", + "62786", + "62791", + "62794", + "62796", + ], + "Schuyler": [ + "61452", + "62319", + "62344", + "62367", + "62624", + "62639", + "62681", + ], + "Scott": [ + "62610", + "62621", + "62663", + "62694", + ], + "Shelby": [ + "61957", + "62422", + "62431", + "62438", + "62444", + "62462", + "62463", + "62465", + "62534", + "62553", + "62565", + "62571", + ], + "StClair": [ + "62059", + "62071", + "62201", + "62202", + "62203", + "62204", + "62205", + "62206", + "62207", + "62208", + "62220", + "62221", + "62222", + "62223", + "62225", + "62226", + "62232", + "62239", + "62240", + "62243", + "62254", + "62255", + "62257", + "62258", + "62260", + "62264", + "62269", + "62282", + "62285", + "62289", + ], + "Stark": [ + "61421", + "61426", + "61449", + "61479", + "61483", + "61491", + ], + "Stephenson": [ + "61013", + "61018", + "61019", + "61027", + "61032", + "61039", + "61044", + "61048", + "61050", + "61060", + "61062", + "61089", + ], + "Tazewell": [ + "61534", + "61535", + "61550", + "61554", + "61555", + "61558", + "61564", + "61721", + "61733", + "61734", + "61747", + "61755", + "61759", + ], + "Union": [ + "62905", + "62906", + "62920", + "62926", + "62952", + "62961", + "62998", + ], + "Vermilion": [ + "61810", + "61811", + "61812", + "61814", + "61817", + "61832", + "61833", + "61834", + "61841", + "61844", + "61846", + "61848", + "61850", + "61857", + "61858", + "61865", + "61870", + "61876", + "61883", + "60932", + "60960", + "60963", + "60968", + "60973", + ], + "Wabash": [ + "62410", + "62811", + "62852", + "62863", + ], + "Warren": [ + "61417", + "61423", + "61435", + "61447", + "61453", + "61462", + "61478", + ], + "Washington": [ + "62214", + "62263", + "62268", + "62271", + "62276", + "62803", + "62808", + "62831", + "62848", + "62876", + "62877", + ], + "Wayne": [ + "62446", + "62809", + "62823", + "62837", + "62842", + "62850", + "62851", + "62878", + "62895", + ], + "White": [ + "62820", + "62821", + "62827", + "62834", + "62835", + "62844", + "62861", + "62862", + "62869", + ], + "Whiteside": [ + "61037", + "61081", + "61230", + "61243", + "61250", + "61251", + "61252", + "61261", + "61270", + "61277", + "61283", + ], + "Will": [ + "60401", + "60403", + "60404", + "60408", + "60410", + "60440", + "60441", + "60442", + "60446", + "60448", + "60449", + "60451", + "60481", + "60484", + "60490", + "60491", + "60503", + "60544", + "60564", + "60585", + "60586", + ], + "Williamson": [ + "62841", + "62915", + "62918", + "62921", + "62922", + "62933", + "62948", + "62949", + "62951", + "62959", + "62974", + ], + "Winnebago": [ + "61016", + "61024", + "61063", + "61073", + "61077", + "61079", + "61080", + "61088", + "61101", + "61102", + "61103", + "61104", + "61105", + "61106", + "61107", + "61108", + "61109", + "61110", + "61111", + "61112", + "61114", + "61115", + "61125", + "61126", + "61130", + "61131", + "61132", + ], + "Woodford": [ + "61516", + "61530", + "61545", + "61548", + "61560", + "61561", + "61571", + "61729", + "61738", + "61742", + "61760", + "61771", ], } @@ -323,6 +1768,7 @@ for z in zips: ZIP_TO_COUNTY_IL[z] = county + def get_county_by_zip(zip_code): """ Returns the county name for a given Illinois zip code. @@ -330,6 +1776,7 @@ def get_county_by_zip(zip_code): """ return ZIP_TO_COUNTY_IL.get(str(zip_code)) + def get_zips_by_county(county_name): """ Returns a list of zip codes for a given Illinois county name. diff --git a/efile_app/efile/views.py b/efile_app/efile/views.py index 9e986b8..35da3b2 100644 --- a/efile_app/efile/views.py +++ b/efile_app/efile/views.py @@ -1,11 +1,9 @@ # views.py - Complete updated file for Illinois eFile system + from django.contrib import messages -from django.shortcuts import render, redirect from django.http import JsonResponse +from django.shortcuts import redirect, render from django.views.decorators.http import require_http_methods -from django.views.decorators.csrf import csrf_exempt -from django.contrib.auth.decorators import login_required -import json # Preserving original imports for when we un-comment all the other methods. # from django.contrib.auth import authenticate, login @@ -183,27 +181,27 @@ def expert_form(request): """ Display the expert form for case details and parties """ - if request.method == 'POST': + if request.method == "POST": # Handle form submission # This would process the form data and save it - messages.success(request, 'Case details saved successfully!') - return redirect('dashboard') # or next step - + messages.success(request, "Case details saved successfully!") + return redirect("dashboard") # or next step + # Get existing case data from session if available - from .utils.case_data_utils import get_case_data + from efile.utils.case_data_utils import get_case_data + case_data = get_case_data(request) - + print(f"Expert form view - case_data from session: {case_data}") - - context = { - 'case_data': case_data - } - - return render(request, 'efile/expert_form.html', context) + + context = {"case_data": case_data} + + return render(request, "efile/expert_form.html", context) # API Endpoints for User Profile and Authentication + @require_http_methods(["GET"]) def api_user_profile(request): """ @@ -212,23 +210,14 @@ def api_user_profile(request): try: # Mock user profile data - replace with actual user data profile_data = { - 'username': 'john_doe', - 'first_name': 'John', - 'last_name': 'Doe', - 'email': 'john.doe@example.com', - 'preferred_county': 'cook', - 'location': { - 'county': 'Cook County', - 'state': 'Illinois' - } + "username": "john_doe", + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "preferred_county": "cook", + "location": {"county": "Cook County", "state": "Illinois"}, } - - return JsonResponse({ - 'success': True, - 'data': profile_data - }) + + return JsonResponse({"success": True, "data": profile_data}) except Exception as e: - return JsonResponse({ - 'success': False, - 'error': str(e) - }, status=500) + return JsonResponse({"success": False, "error": str(e)}, status=500) diff --git a/efile_app/efile/views/__pycache__/__init__.cpython-313.pyc b/efile_app/efile/views/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 1e61da3..0000000 Binary files a/efile_app/efile/views/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/efile_app/efile/views/__pycache__/login.cpython-313.pyc b/efile_app/efile/views/__pycache__/login.cpython-313.pyc deleted file mode 100644 index d9097ee..0000000 Binary files a/efile_app/efile/views/__pycache__/login.cpython-313.pyc and /dev/null differ diff --git a/efile_app/efile/views/__pycache__/options.cpython-313.pyc b/efile_app/efile/views/__pycache__/options.cpython-313.pyc deleted file mode 100644 index 2a0e167..0000000 Binary files a/efile_app/efile/views/__pycache__/options.cpython-313.pyc and /dev/null differ diff --git a/efile_app/efile/views/__pycache__/register.cpython-313.pyc b/efile_app/efile/views/__pycache__/register.cpython-313.pyc deleted file mode 100644 index e2d3cb2..0000000 Binary files a/efile_app/efile/views/__pycache__/register.cpython-313.pyc and /dev/null differ diff --git a/efile_app/efile/views/api_views.py b/efile_app/efile/views/api_views.py index a3ff709..85c447a 100644 --- a/efile_app/efile/views/api_views.py +++ b/efile_app/efile/views/api_views.py @@ -1,103 +1,84 @@ import json + from django.http import JsonResponse -from django.views.decorators.http import require_http_methods from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import ensure_csrf_cookie -@method_decorator(ensure_csrf_cookie, name='dispatch') +@method_decorator(ensure_csrf_cookie, name="dispatch") class GetCaseDataView(View): """API endpoint to retrieve case data from session.""" - + def get(self, request): try: # Get case data from session - case_data = request.session.get('case_data', {}) - + case_data = request.session.get("case_data", {}) + print(f"Retrieved case data from session: {case_data}") - - return JsonResponse({ - 'success': True, - 'data': case_data - }) - + + return JsonResponse({"success": True, "data": case_data}) + except Exception as e: print(f"Error retrieving case data: {e}") - return JsonResponse({ - 'success': False, - 'error': 'Server error occurred' - }, status=500) + return JsonResponse({"success": False, "error": "Server error occurred"}, status=500) -@method_decorator(ensure_csrf_cookie, name='dispatch') +@method_decorator(ensure_csrf_cookie, name="dispatch") class SaveCaseDataView(View): """API endpoint to save case data to session.""" - + def post(self, request): try: data = json.loads(request.body) - + # Extract case data from form submission case_data = { - 'court': data.get('court', ''), - 'case_category': data.get('case_category', ''), - 'case_type': data.get('case_type', ''), - 'filing_type': data.get('filing_type', ''), - 'document_type': data.get('document_type', ''), - 'petitioner_first_name': data.get('petitioner_first_name', ''), - 'petitioner_last_name': data.get('petitioner_last_name', ''), - 'petitioner_address': data.get('petitioner_address', ''), - 'petitioner_party_type': data.get('petitioner_party_type', ''), # Add party type - 'new_first_name': data.get('new_first_name', ''), - 'new_last_name': data.get('new_last_name', ''), - 'new_name_party_type': data.get('new_name_party_type', ''), # Add party type - 'optional_services': data.get('optional_services', []), + "court": data.get("court", ""), + "case_category": data.get("case_category", ""), + "case_type": data.get("case_type", ""), + "filing_type": data.get("filing_type", ""), + "document_type": data.get("document_type", ""), + "petitioner_first_name": data.get("petitioner_first_name", ""), + "petitioner_last_name": data.get("petitioner_last_name", ""), + "petitioner_address": data.get("petitioner_address", ""), + "petitioner_party_type": data.get("petitioner_party_type", ""), # Add party type + "new_first_name": data.get("new_first_name", ""), + "new_last_name": data.get("new_last_name", ""), + "new_name_party_type": data.get("new_name_party_type", ""), # Add party type + "optional_services": data.get("optional_services", []), } - + # Validate required fields - required_fields = ['court', 'case_category', 'case_type', 'filing_type', 'document_type'] + required_fields = ["court", "case_category", "case_type", "filing_type", "document_type"] missing_fields = [field for field in required_fields if not case_data.get(field)] - + if missing_fields: - return JsonResponse({ - 'success': False, - 'error': f'Missing required fields: {", ".join(missing_fields)}' - }, status=400) - + err_missing = f"Missing required fields: {', '.join(missing_fields)}" + return JsonResponse({"success": False, "error": err_missing}, status=400) + # For name change cases, validate party information - if 'name change' in case_data.get('case_type', '').lower(): - party_fields = ['petitioner_first_name', 'petitioner_last_name', 'new_first_name', 'new_last_name'] + if "name change" in case_data.get("case_type", "").lower(): + party_fields = ["petitioner_first_name", "petitioner_last_name", "new_first_name", "new_last_name"] missing_party_fields = [field for field in party_fields if not case_data.get(field)] - + if missing_party_fields: - return JsonResponse({ - 'success': False, - 'error': f'Missing party information for name change case: {", ".join(missing_party_fields)}' - }, status=400) - + err_party = f"Missing party information for name change case: {', '.join(missing_party_fields)}" + return JsonResponse({"success": False, "error": err_party}, status=400) + # Save to session - request.session['case_data'] = case_data + request.session["case_data"] = case_data request.session.modified = True - + print(f"Saved case data to session: {case_data}") - - return JsonResponse({ - 'success': True, - 'message': 'Case data saved successfully' - }) - + + return JsonResponse({"success": True, "message": "Case data saved successfully"}) + except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON data' - }, status=400) + return JsonResponse({"success": False, "error": "Invalid JSON data"}, status=400) except Exception as e: print(f"Error saving case data: {e}") - return JsonResponse({ - 'success': False, - 'error': 'Server error occurred' - }, status=500) + return JsonResponse({"success": False, "error": "Server error occurred"}, status=500) # Function-based view wrapper for easy URL mapping diff --git a/efile_app/efile/views/confirmation.py b/efile_app/efile/views/confirmation.py index b40d40a..1df2011 100644 --- a/efile_app/efile/views/confirmation.py +++ b/efile_app/efile/views/confirmation.py @@ -3,13 +3,10 @@ def filing_confirmation(request): """Confirmation page after successful filing submission.""" - + # You can add logic here to retrieve filing details from session # or from database if you're storing submitted filings - - context = { - 'page_title': 'Filing Confirmation', - 'success_message': 'Your filing has been successfully submitted!' - } - - return render(request, 'efile/confirmation.html', context) + + context = {"page_title": "Filing Confirmation", "success_message": "Your filing has been successfully submitted!"} + + return render(request, "efile/confirmation.html", context) diff --git a/efile_app/efile/views/expert_form.py b/efile_app/efile/views/expert_form.py index e35ef78..30e0fcb 100644 --- a/efile_app/efile/views/expert_form.py +++ b/efile_app/efile/views/expert_form.py @@ -1,35 +1,34 @@ -from django.contrib import messages -from django.shortcuts import redirect, render +from django.shortcuts import render def efile_expert_form(request): """Expert form view for creating filings with cascading dropdowns.""" # Get auth tokens from session if available - auth_tokens = request.session.get('auth_tokens', None) + auth_tokens = request.session.get("auth_tokens", None) print(f"Auth Tokens: {auth_tokens}") - + # Get existing case data from session to populate form - case_data = request.session.get('case_data', {}) - + case_data = request.session.get("case_data", {}) + print(f"Case data from session: {case_data}") - + # Check if we have all required data for upload - required_fields = ['court', 'case_category', 'case_type', 'filing_type', 'document_type'] + required_fields = ["court", "case_category", "case_type", "filing_type", "document_type"] has_all_required = all(case_data.get(field) for field in required_fields) - + # For name change cases, also check for party information has_party_info = True # Default for non-name change cases - if has_all_required and 'name change' in case_data.get('case_type', '').lower(): - party_fields = ['petitioner_first_name', 'petitioner_last_name', 'new_first_name', 'new_last_name'] + if has_all_required and "name change" in case_data.get("case_type", "").lower(): + party_fields = ["petitioner_first_name", "petitioner_last_name", "new_first_name", "new_last_name"] has_party_info = all(case_data.get(field) for field in party_fields) - + # Display the form for data collection with existing data populated context = { - 'case_data': case_data, - 'auth_tokens': auth_tokens, - 'can_proceed_to_upload': has_all_required and has_party_info, - 'missing_required_fields': not has_all_required, - 'missing_party_info': has_all_required and not has_party_info + "case_data": case_data, + "auth_tokens": auth_tokens, + "can_proceed_to_upload": has_all_required and has_party_info, + "missing_required_fields": not has_all_required, + "missing_party_info": has_all_required and not has_party_info, } - - return render(request, 'efile/expert_form.html', context) \ No newline at end of file + + return render(request, "efile/expert_form.html", context) diff --git a/efile_app/efile/views/login.py b/efile_app/efile/views/login.py index 0c9253b..a8c1218 100644 --- a/efile_app/efile/views/login.py +++ b/efile_app/efile/views/login.py @@ -63,7 +63,7 @@ def efile_logout(request): # Clear any existing messages first storage = get_messages(request) - for message in storage: + for _message in storage: pass # This consumes all messages logout(request) diff --git a/efile_app/efile/views/options.py b/efile_app/efile/views/options.py index a25fdac..ffc711a 100644 --- a/efile_app/efile/views/options.py +++ b/efile_app/efile/views/options.py @@ -1,17 +1,15 @@ -from django.shortcuts import redirect, render +from django.shortcuts import render + from ..utils.case_data_utils import get_case_data def efile_options(request): """Options view that displays saved case data and provides next steps.""" - + # Get case data from session case_data = get_case_data(request) - + # Pass case data to template for display - context = { - 'case_data': case_data, - 'has_case_data': bool(case_data) - } - - return render(request, 'efile/options.html', context) + context = {"case_data": case_data, "has_case_data": bool(case_data)} + + return render(request, "efile/options.html", context) diff --git a/efile_app/efile/views/review.py b/efile_app/efile/views/review.py index 4cf4c6c..f73e63b 100644 --- a/efile_app/efile/views/review.py +++ b/efile_app/efile/views/review.py @@ -1,96 +1,102 @@ -from django.shortcuts import redirect, render from django.contrib import messages -from ..utils.case_data_utils import get_case_data, get_petitioner_info, get_name_sought_info, get_case_classification +from django.shortcuts import redirect, render + +from ..utils.case_data_utils import get_case_classification, get_case_data, get_name_sought_info, get_petitioner_info def case_review(request): """Review view for case details before final submission.""" - - + # Get case data from session case_data = get_case_data(request) print(case_data) - + # Add user email from session if available and not already in case_data - user_email = request.session.get('user_email') - if user_email and not case_data.get('email'): - case_data['email'] = user_email - + user_email = request.session.get("user_email") + if user_email and not case_data.get("email"): + case_data["email"] = user_email + # If no case data exists, redirect back to expert form if not case_data: - messages.error(request, 'Please complete the case details first.') - return redirect('expert_form') - + messages.error(request, "Please complete the case details first.") + return redirect("expert_form") + # Get organized case information petitioner_info = get_petitioner_info(request) name_sought_info = get_name_sought_info(request) case_classification = get_case_classification(request) - + # Use friendly names if available, otherwise fallback to raw values - friendly_case_type = case_data.get('case_type_name', case_classification['case_type']) - friendly_filing_type = case_data.get('filing_type_name', case_classification['filing_type']) - friendly_court = case_data.get('court_name', case_classification['court']) - friendly_case_category = case_data.get('case_category_name', case_classification.get('case_category', '')) - friendly_document_type = case_data.get('document_type_name', case_classification.get('document_type', '')) - + friendly_case_type = case_data.get("case_type_name", case_classification["case_type"]) + friendly_filing_type = case_data.get("filing_type_name", case_classification["filing_type"]) + friendly_court = case_data.get("court_name", case_classification["court"]) + friendly_case_category = case_data.get("case_category_name", case_classification.get("case_category", "")) + friendly_document_type = case_data.get("document_type_name", case_classification.get("document_type", "")) + # Organize data for review review_sections = { - 'case_classification': { - 'title': 'Case Classification', - 'items': [ - {'label': 'County/Court', 'value': friendly_court, 'raw': case_classification['court']}, - {'label': 'Case Category', 'value': friendly_case_category, 'raw': case_classification.get('case_category', '')}, - {'label': 'Case Type', 'value': friendly_case_type, 'raw': case_classification['case_type']}, - {'label': 'Filing Type', 'value': friendly_filing_type, 'raw': case_classification['filing_type']}, - {'label': 'Document Type', 'value': friendly_document_type, 'raw': case_classification.get('document_type', '')}, - ] + "case_classification": { + "title": "Case Classification", + "items": [ + {"label": "County/Court", "value": friendly_court, "raw": case_classification["court"]}, + { + "label": "Case Category", + "value": friendly_case_category, + "raw": case_classification.get("case_category", ""), + }, + {"label": "Case Type", "value": friendly_case_type, "raw": case_classification["case_type"]}, + {"label": "Filing Type", "value": friendly_filing_type, "raw": case_classification["filing_type"]}, + { + "label": "Document Type", + "value": friendly_document_type, + "raw": case_classification.get("document_type", ""), + }, + ], + }, + "petitioner_info": { + "title": "Petitioner Information", + "items": [ + {"label": "First Name", "value": petitioner_info.get("first_name", "")}, + {"label": "Last Name", "value": petitioner_info.get("last_name", "")}, + {"label": "Address", "value": petitioner_info.get("address", "")}, + {"label": "City", "value": petitioner_info.get("city", "")}, + {"label": "State", "value": petitioner_info.get("state", "")}, + {"label": "Zip Code", "value": petitioner_info.get("zip_code", "")}, + {"label": "Phone", "value": petitioner_info.get("phone", "")}, + {"label": "Email", "value": petitioner_info.get("email", "")}, + ], }, - 'petitioner_info': { - 'title': 'Petitioner Information', - 'items': [ - {'label': 'First Name', 'value': petitioner_info.get('first_name', '')}, - {'label': 'Last Name', 'value': petitioner_info.get('last_name', '')}, - {'label': 'Address', 'value': petitioner_info.get('address', '')}, - {'label': 'City', 'value': petitioner_info.get('city', '')}, - {'label': 'State', 'value': petitioner_info.get('state', '')}, - {'label': 'Zip Code', 'value': petitioner_info.get('zip_code', '')}, - {'label': 'Phone', 'value': petitioner_info.get('phone', '')}, - {'label': 'Email', 'value': petitioner_info.get('email', '')}, - ] - } } - + # Add name sought info if it's a name change case - if 'name change' in friendly_case_type.lower(): - review_sections['name_sought'] = { - 'title': 'Name Change Details', - 'items': [ - {'label': 'First Name', 'value': name_sought_info.get('first_name', '')}, - {'label': 'Last Name', 'value': name_sought_info.get('last_name', '')}, - {'label': 'Reason for Change', 'value': case_data.get('reason_for_name_change', '')}, - ] + if "name change" in friendly_case_type.lower(): + review_sections["name_sought"] = { + "title": "Name Change Details", + "items": [ + {"label": "First Name", "value": name_sought_info.get("first_name", "")}, + {"label": "Last Name", "value": name_sought_info.get("last_name", "")}, + {"label": "Reason for Change", "value": case_data.get("reason_for_name_change", "")}, + ], } - + # Add optional services if any - optional_services = case_data.get('optional_services', []) + optional_services = case_data.get("optional_services", []) if optional_services: - review_sections['optional_services'] = { - 'title': 'Optional Services', - 'items': [ - {'label': 'Selected Services', 'value': ', '.join(optional_services)} - ] + review_sections["optional_services"] = { + "title": "Optional Services", + "items": [{"label": "Selected Services", "value": ", ".join(optional_services)}], } - + context = { - 'case_data': case_data, - 'review_sections': review_sections, - 'friendly_names': { - 'case_type': friendly_case_type, - 'filing_type': friendly_filing_type, - 'court': friendly_court, - 'case_category': friendly_case_category, - 'document_type': friendly_document_type, - } + "case_data": case_data, + "review_sections": review_sections, + "friendly_names": { + "case_type": friendly_case_type, + "filing_type": friendly_filing_type, + "court": friendly_court, + "case_category": friendly_case_category, + "document_type": friendly_document_type, + }, } - - return render(request, 'efile/review.html', context) + + return render(request, "efile/review.html", context) diff --git a/efile_app/efile/views/session_api.py b/efile_app/efile/views/session_api.py index f7d45ad..55269c1 100644 --- a/efile_app/efile/views/session_api.py +++ b/efile_app/efile/views/session_api.py @@ -1,7 +1,9 @@ +import json + from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods -import json + from ..utils.zip_to_county_il import get_county_by_zip @@ -11,79 +13,90 @@ def save_form_data_to_session(request): """Save form data (including petitioner contact info) to Django session and derive county from zip.""" try: data = json.loads(request.body) - form_data = data.get('data', {}) + form_data = data.get("data", {}) # Start from existing case_data so we don't clobber other fields - case_data = request.session.get('case_data', {}) + case_data = request.session.get("case_data", {}) # Update case_data fields with provided form values (preserve existing when not provided) - case_data.update({ - 'court': form_data.get('court', case_data.get('court', '')), - 'case_category': form_data.get('case_category', case_data.get('case_category', '')), - 'case_type': form_data.get('case_type', case_data.get('case_type', '')), - 'filing_type': form_data.get('filing_type', case_data.get('filing_type', '')), - 'document_type': form_data.get('document_type', case_data.get('document_type', '')), - - # simplified contact/address fields - 'first_name': form_data.get('first_name', case_data.get('first_name', '')), - 'last_name': form_data.get('last_name', case_data.get('last_name', '')), - 'address': form_data.get('address', case_data.get('address', '')), - 'address_line2': form_data.get('address_line2', case_data.get('address_line2', '')), - 'city': form_data.get('city', case_data.get('city', '')), - 'state': form_data.get('state', case_data.get('state', '')), - 'zip': form_data.get('zip', case_data.get('zip', '')), - 'email': form_data.get('email', case_data.get('email', '')), - 'phone': form_data.get('phone', case_data.get('phone', '')), - - # optional services and friendly names - 'optional_services': form_data.get('optional_services', case_data.get('optional_services', [])), - 'court_name': form_data.get('court_name', case_data.get('court_name', '')), - 'case_category_name': form_data.get('case_category_name', case_data.get('case_category_name', '')), - 'case_type_name': form_data.get('case_type_name', case_data.get('case_type_name', '')), - 'filing_type_name': form_data.get('filing_type_name', case_data.get('filing_type_name', '')), - 'document_type_name': form_data.get('document_type_name', case_data.get('document_type_name', '')), - }) + case_data.update( + { + "court": form_data.get("court", case_data.get("court", "")), + "case_category": form_data.get("case_category", case_data.get("case_category", "")), + "case_type": form_data.get("case_type", case_data.get("case_type", "")), + "filing_type": form_data.get("filing_type", case_data.get("filing_type", "")), + "document_type": form_data.get("document_type", case_data.get("document_type", "")), + # simplified contact/address fields + "first_name": form_data.get("first_name", case_data.get("first_name", "")), + "last_name": form_data.get("last_name", case_data.get("last_name", "")), + "address": form_data.get("address", case_data.get("address", "")), + "address_line2": form_data.get("address_line2", case_data.get("address_line2", "")), + "city": form_data.get("city", case_data.get("city", "")), + "state": form_data.get("state", case_data.get("state", "")), + "zip": form_data.get("zip", case_data.get("zip", "")), + "email": form_data.get("email", case_data.get("email", "")), + "phone": form_data.get("phone", case_data.get("phone", "")), + # optional services and friendly names + "optional_services": form_data.get("optional_services", case_data.get("optional_services", [])), + "court_name": form_data.get("court_name", case_data.get("court_name", "")), + "case_category_name": form_data.get("case_category_name", case_data.get("case_category_name", "")), + "case_type_name": form_data.get("case_type_name", case_data.get("case_type_name", "")), + "filing_type_name": form_data.get("filing_type_name", case_data.get("filing_type_name", "")), + "document_type_name": form_data.get("document_type_name", case_data.get("document_type_name", "")), + } + ) # Add all dynamic fields that might be present in the form data # This includes petitioner information, name change details, etc. dynamic_fields = [ - 'petitioner_first_name', 'petitioner_last_name', 'petitioner_address', - 'petitioner_phone', 'petitioner_email', - 'new_first_name', 'new_last_name', - 'reason_for_change', 'minor_first_name', 'minor_last_name', - 'parent_first_name', 'parent_last_name', 'guardian_first_name', 'guardian_last_name' + "petitioner_first_name", + "petitioner_last_name", + "petitioner_address", + "petitioner_phone", + "petitioner_email", + "new_first_name", + "new_last_name", + "reason_for_change", + "minor_first_name", + "minor_last_name", + "parent_first_name", + "parent_last_name", + "guardian_first_name", + "guardian_last_name", ] - + for field in dynamic_fields: if field in form_data: case_data[field] = form_data[field] - + # Also save any other fields that might be dynamically added but not in our predefined list for key, value in form_data.items(): if key not in case_data and value: # Only add if not already handled and has a value case_data[key] = value # Try to derive county from zip code and save it - zip_code = case_data.get('zip') or case_data.get('zip_code') or form_data.get('zip') or form_data.get('zip_code', '') + zip_code = ( + case_data.get("zip") or case_data.get("zip_code") or form_data.get("zip") or form_data.get("zip_code", "") + ) if zip_code: try: county = get_county_by_zip(zip_code) if county: # Save simplified county key and keep petitioner_county for backward compatibility - case_data['county'] = county - case_data['petitioner_county'] = county + case_data["county"] = county + case_data["petitioner_county"] = county except Exception: # If mapping fails, ignore and continue pass # Persist to session - request.session['case_data'] = case_data + request.session["case_data"] = case_data request.session.modified = True - return JsonResponse({'success': True, 'message': 'Case data saved to session'}) + return JsonResponse({"success": True, "message": "Case data saved to session"}) except Exception as e: - return JsonResponse({'success': False, 'error': str(e)}, status=500) + return JsonResponse({"success": False, "error": str(e)}, status=500) @csrf_exempt @@ -92,32 +105,26 @@ def save_upload_data_to_session(request): """Save upload data and file information to Django session for review.""" try: data = json.loads(request.body) - + upload_data = { - 'files': data.get('files', {}), - 'options': data.get('options', {}), + "files": data.get("files", {}), + "options": data.get("options", {}), } - + # Save to session - request.session['upload_data'] = upload_data + request.session["upload_data"] = upload_data request.session.modified = True - - return JsonResponse({ - 'success': True, - 'message': 'Upload data saved to session' - }) - + + return JsonResponse({"success": True, "message": "Upload data saved to session"}) + except Exception as e: - return JsonResponse({ - 'success': False, - 'error': str(e) - }, status=500) + return JsonResponse({"success": False, "error": str(e)}, status=500) @require_http_methods(["GET"]) def get_upload_data_from_session(request): """Get upload data from Django session.""" - upload_data = request.session.get('upload_data', {}) + upload_data = request.session.get("upload_data", {}) return JsonResponse(upload_data, safe=False) @@ -127,55 +134,57 @@ def submit_final_filing(request): """Handle final filing submission after user has reviewed all information.""" try: data = json.loads(request.body) - - if not data.get('confirm_submission'): - return JsonResponse({ - 'success': False, - 'error': 'Submission confirmation is required' - }, status=400) - + + if not data.get("confirm_submission"): + return JsonResponse({"success": False, "error": "Submission confirmation is required"}, status=400) + # Get all data from session - case_data = request.session.get('case_data', {}) - upload_data = request.session.get('upload_data', {}) - + case_data = request.session.get("case_data", {}) + upload_data = request.session.get("upload_data", {}) + if not case_data: - return JsonResponse({ - 'success': False, - 'error': 'No case data found in session. Please go back and resubmit your case information.' - }, status=400) - - if not upload_data or not upload_data.get('files'): - return JsonResponse({ - 'success': False, - 'error': 'No upload data found in session. Please go back and resubmit your documents.' - }, status=400) - + return JsonResponse( + { + "success": False, + "error": "No case data found in session. Please go back and resubmit your case information.", + }, + status=400, + ) + + if not upload_data or not upload_data.get("files"): + return JsonResponse( + { + "success": False, + "error": "No upload data found in session. Please go back and resubmit your documents.", + }, + status=400, + ) + # TODO: Integrate with actual efile API submission # This is where you would call the Suffolk LIT Lab efile API # to actually submit the case with all the collected data - + # For now, simulate successful submission # In production, you'd replace this with actual API calls - + # Clear session data after successful submission - if 'case_data' in request.session: - del request.session['case_data'] - if 'upload_data' in request.session: - del request.session['upload_data'] + if "case_data" in request.session: + del request.session["case_data"] + if "upload_data" in request.session: + del request.session["upload_data"] request.session.modified = True - - return JsonResponse({ - 'success': True, - 'message': 'Filing submitted successfully', - 'redirect_url': '/filing-confirmation/', - 'case_id': 'TEMP_' + str(hash(str(case_data)))[:8] # Temporary case ID - }) - + + return JsonResponse( + { + "success": True, + "message": "Filing submitted successfully", + "redirect_url": "/filing-confirmation/", + "case_id": "TEMP_" + str(hash(str(case_data)))[:8], # Temporary case ID + } + ) + except (json.JSONDecodeError, Exception) as e: - return JsonResponse({ - 'success': False, - 'error': f'An error occurred during submission: {str(e)}' - }, status=500) + return JsonResponse({"success": False, "error": f"An error occurred during submission: {str(e)}"}, status=500) @csrf_exempt @@ -183,19 +192,16 @@ def submit_final_filing(request): def clear_session_data(request): """Clear all session data for testing purposes.""" request.session.flush() - return JsonResponse({ - 'success': True, - 'message': 'Session data cleared' - }) + return JsonResponse({"success": True, "message": "Session data cleared"}) @require_http_methods(["GET"]) def debug_session_data(request): """Debug endpoint to view session contents.""" session_data = { - 'case_data': request.session.get('case_data', {}), - 'upload_data': request.session.get('upload_data', {}), - 'session_key': request.session.session_key, - 'session_items': dict(request.session.items()) + "case_data": request.session.get("case_data", {}), + "upload_data": request.session.get("upload_data", {}), + "session_key": request.session.session_key, + "session_items": dict(request.session.items()), } return JsonResponse(session_data, safe=False) diff --git a/efile_app/efile/views/upload.py b/efile_app/efile/views/upload.py index 0e67ab7..9d1c9f8 100644 --- a/efile_app/efile/views/upload.py +++ b/efile_app/efile/views/upload.py @@ -1,13 +1,14 @@ -from django.shortcuts import redirect, render +import logging +import uuid + +import requests from django.contrib import messages from django.http import JsonResponse +from django.shortcuts import redirect, render from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods -import requests -import json -import logging -import uuid -from ..utils.case_data_utils import get_case_data, get_petitioner_info, get_name_sought_info, get_case_classification + +from ..utils.case_data_utils import get_case_classification, get_case_data, get_name_sought_info, get_petitioner_info from ..utils.s3_upload import s3_handler logger = logging.getLogger(__name__) @@ -15,305 +16,294 @@ def efile_upload(request): """Upload view for document submission and filing creation.""" - + # Get case data from session case_data = get_case_data(request) - + # If no case data exists, redirect back to expert form if not case_data: - messages.error(request, 'Please complete the case details first.') - return redirect('expert_form') - + messages.error(request, "Please complete the case details first.") + return redirect("expert_form") + # Get organized case information petitioner_info = get_petitioner_info(request) name_sought_info = get_name_sought_info(request) case_classification = get_case_classification(request) - + # Use friendly names if available, otherwise fallback to raw values - friendly_case_type = case_data.get('case_type_name', case_classification['case_type']) - friendly_filing_type = case_data.get('filing_type_name', case_classification['filing_type']) - friendly_court = case_data.get('court_name', case_classification['court']) - + friendly_case_type = case_data.get("case_type_name", case_classification["case_type"]) + friendly_filing_type = case_data.get("filing_type_name", case_classification["filing_type"]) + friendly_court = case_data.get("court_name", case_classification["court"]) + context = { - 'case_data': case_data, - 'petitioner_info': petitioner_info, - 'name_sought_info': name_sought_info, - 'case_classification': case_classification, - 'case_type': friendly_case_type, - 'filing_type': friendly_filing_type, - 'court': friendly_court, - 'case_type_raw': case_classification['case_type'], - 'filing_type_raw': case_classification['filing_type'], - 'court_raw': case_classification['court'], + "case_data": case_data, + "petitioner_info": petitioner_info, + "name_sought_info": name_sought_info, + "case_classification": case_classification, + "case_type": friendly_case_type, + "filing_type": friendly_filing_type, + "court": friendly_court, + "case_type_raw": case_classification["case_type"], + "filing_type_raw": case_classification["filing_type"], + "court_raw": case_classification["court"], } - return render(request, 'efile/upload.html', context) + return render(request, "efile/upload.html", context) @csrf_exempt @require_http_methods(["POST"]) def create_filing(request): """Create a filing with Suffolk LIT Lab API using collected case data.""" - + try: # Get case data from session case_data = get_case_data(request) - + if not case_data: - return JsonResponse({ - 'success': False, - 'error': 'No case data found. Please complete the expert form first.' - }, status=400) - + return JsonResponse( + {"success": False, "error": "No case data found. Please complete the expert form first."}, status=400 + ) + # Get auth tokens - auth_tokens = request.session.get('auth_tokens') - if not auth_tokens or 'token' not in auth_tokens: - return JsonResponse({ - 'success': False, - 'error': 'Authentication required. Please log in first.' - }, status=401) - + auth_tokens = request.session.get("auth_tokens") + if not auth_tokens or "token" not in auth_tokens: + return JsonResponse( + {"success": False, "error": "Authentication required. Please log in first."}, status=401 + ) + # Transform case data to Suffolk API payload format filing_payload = transform_case_data_to_filing_payload(case_data) - + # Make POST request to Suffolk LIT Lab filing API api_url = "https://efile-test.suffolklitlab.org/filings/" - - headers = { - 'Authorization': f"Bearer {auth_tokens['token']}", - 'Content-Type': 'application/json' - } - + + headers = {"Authorization": f"Bearer {auth_tokens['token']}", "Content-Type": "application/json"} + response = requests.post(api_url, headers=headers, json=filing_payload, timeout=30) - + if response.status_code == 201: # Filing created successfully filing_data = response.json() - + # Save filing ID to session for future reference - request.session['current_filing_id'] = filing_data.get('id') + request.session["current_filing_id"] = filing_data.get("id") request.session.modified = True - - return JsonResponse({ - 'success': True, - 'filing_id': filing_data.get('id'), - 'message': 'Filing created successfully', - 'data': filing_data - }) + + return JsonResponse( + { + "success": True, + "filing_id": filing_data.get("id"), + "message": "Filing created successfully", + "data": filing_data, + } + ) else: # API error error_detail = response.text try: error_json = response.json() - error_detail = error_json.get('detail', error_json) - except: + error_detail = error_json.get("detail", error_json) + except ValueError: pass - - return JsonResponse({ - 'success': False, - 'error': f'Filing creation failed: {error_detail}', - 'status_code': response.status_code - }, status=response.status_code) - + + return JsonResponse( + { + "success": False, + "error": f"Filing creation failed: {error_detail}", + "status_code": response.status_code, + }, + status=response.status_code, + ) + except requests.RequestException as e: - return JsonResponse({ - 'success': False, - 'error': f'Network error: {str(e)}' - }, status=500) + return JsonResponse({"success": False, "error": f"Network error: {str(e)}"}, status=500) except Exception as e: - return JsonResponse({ - 'success': False, - 'error': f'Unexpected error: {str(e)}' - }, status=500) + return JsonResponse({"success": False, "error": f"Unexpected error: {str(e)}"}, status=500) def transform_case_data_to_filing_payload(case_data): """ Transform collected case data into Suffolk LIT Lab API filing payload format. """ - + # Base filing payload structure payload = { - 'jurisdiction': 'illinois', - 'court': case_data.get('court'), - 'category': case_data.get('case_category'), - 'case_type': case_data.get('case_type'), - 'filing_type': case_data.get('filing_type'), - 'document_type': case_data.get('document_type'), - 'parties': [], - 'optional_services': case_data.get('optional_services', []) + "jurisdiction": "illinois", + "court": case_data.get("court"), + "category": case_data.get("case_category"), + "case_type": case_data.get("case_type"), + "filing_type": case_data.get("filing_type"), + "document_type": case_data.get("document_type"), + "parties": [], + "optional_services": case_data.get("optional_services", []), } - + # Add petitioner party if this is a name change case - if 'name change' in case_data.get('case_type', '').lower(): + if "name change" in case_data.get("case_type", "").lower(): # Add petitioner - if case_data.get('petitioner_first_name') or case_data.get('petitioner_last_name'): + if case_data.get("petitioner_first_name") or case_data.get("petitioner_last_name"): petitioner = { - 'party_type': 'petitioner', - 'name': { - 'first': case_data.get('petitioner_first_name', ''), - 'last': case_data.get('petitioner_last_name', ''), - 'full': f"{case_data.get('petitioner_first_name', '')} {case_data.get('petitioner_last_name', '')}".strip() + "party_type": "petitioner", + "name": { + "first": case_data.get("petitioner_first_name", ""), + "last": case_data.get("petitioner_last_name", ""), + "full": ( + f"{case_data.get('petitioner_first_name', '')} {case_data.get('petitioner_last_name', '')}" + ).strip(), }, - 'address': case_data.get('petitioner_address', ''), - 'role': 'Petitioner' + "address": case_data.get("petitioner_address", ""), + "role": "Petitioner", } - payload['parties'].append(petitioner) - + payload["parties"].append(petitioner) + # Add name sought information as additional case details - if case_data.get('new_first_name') or case_data.get('new_last_name'): - payload['name_change_details'] = { - 'new_name': { - 'first': case_data.get('new_first_name', ''), - 'last': case_data.get('new_last_name', ''), - 'full': f"{case_data.get('new_first_name', '')} {case_data.get('new_last_name', '')}".strip() + if case_data.get("new_first_name") or case_data.get("new_last_name"): + payload["name_change_details"] = { + "new_name": { + "first": case_data.get("new_first_name", ""), + "last": case_data.get("new_last_name", ""), + "full": (f"{case_data.get('new_first_name', '')} {case_data.get('new_last_name', '')}").strip(), } } - + # Add case metadata - payload['metadata'] = { - 'created_via': 'illinois_efile_system', - 'case_classification': { - 'court': case_data.get('court'), - 'category': case_data.get('case_category'), - 'case_type': case_data.get('case_type'), - 'filing_type': case_data.get('filing_type'), - 'document_type': case_data.get('document_type') - } + payload["metadata"] = { + "created_via": "illinois_efile_system", + "case_classification": { + "court": case_data.get("court"), + "category": case_data.get("case_category"), + "case_type": case_data.get("case_type"), + "filing_type": case_data.get("filing_type"), + "document_type": case_data.get("document_type"), + }, } - + return payload -@csrf_exempt +@csrf_exempt @require_http_methods(["POST"]) def upload_documents(request): """Handle document uploads using S3, then submit to Suffolk API.""" - + try: # Get current filing ID from session - filing_id = request.session.get('current_filing_id') - + filing_id = request.session.get("current_filing_id") + if not filing_id: - return JsonResponse({ - 'success': False, - 'error': 'No active filing found. Please create a filing first.' - }, status=400) - + return JsonResponse( + {"success": False, "error": "No active filing found. Please create a filing first."}, status=400 + ) + # Get auth tokens - auth_tokens = request.session.get('auth_tokens') - if not auth_tokens or 'token' not in auth_tokens: - return JsonResponse({ - 'success': False, - 'error': 'Authentication required.' - }, status=401) - + auth_tokens = request.session.get("auth_tokens") + if not auth_tokens or "token" not in auth_tokens: + return JsonResponse({"success": False, "error": "Authentication required."}, status=401) + # Handle file uploads - uploaded_files = request.FILES.getlist('documents') - file_type = request.POST.get('file_type', 'document') - + uploaded_files = request.FILES.getlist("documents") + file_type = request.POST.get("file_type", "document") + if not uploaded_files: - return JsonResponse({ - 'success': False, - 'error': 'No documents provided.' - }, status=400) - + return JsonResponse({"success": False, "error": "No documents provided."}, status=400) + s3_upload_results = [] - + # First upload all files to S3 for uploaded_file in uploaded_files: # Validate file validation_result = s3_handler.validate_file( - uploaded_file, - max_size_mb=10, - allowed_types=['.pdf'] # Only PDFs for efile + uploaded_file, + max_size_mb=10, + allowed_types=[".pdf"], # Only PDFs for efile ) - - if not validation_result['valid']: - return JsonResponse({ - 'success': False, - 'error': f'File validation failed for {uploaded_file.name}: {validation_result["error"]}' - }, status=400) - + + if not validation_result["valid"]: + return JsonResponse( + { + "success": False, + "error": f"File validation failed for {uploaded_file.name}: {validation_result['error']}", + }, + status=400, + ) + # Prepare metadata metadata = { - 'file-type': file_type, - 'filing-id': str(filing_id), - 'original-size': str(uploaded_file.size), - 'original-name': uploaded_file.name + "file-type": file_type, + "filing-id": str(filing_id), + "original-size": str(uploaded_file.size), + "original-name": uploaded_file.name, } - + # Upload to S3 - upload_result = s3_handler.upload_file( - uploaded_file, - file_type=file_type, - metadata=metadata - ) - - if not upload_result['success']: - return JsonResponse({ - 'success': False, - 'error': f'S3 upload failed for {uploaded_file.name}: {upload_result["error"]}' - }, status=500) + upload_result = s3_handler.upload_file(uploaded_file, file_type=file_type, metadata=metadata) + + if not upload_result["success"]: + return JsonResponse( + {"success": False, "error": f"S3 upload failed for {uploaded_file.name}: {upload_result['error']}"}, + status=500, + ) print(upload_result) # Debugging line, can be removed later - s3_upload_results.append({ - 'original_name': uploaded_file.name, - 'url': upload_result['url'], - 'public_url': s3_handler.get_public_url(upload_result['key']), - 'key': upload_result['key'], - 'size': upload_result['size'] - }) + s3_upload_results.append( + { + "original_name": uploaded_file.name, + "url": upload_result["url"], + "public_url": s3_handler.get_public_url(upload_result["key"]), + "key": upload_result["key"], + "size": upload_result["size"], + } + ) print("DEBUG: S3 upload results:", s3_upload_results) # Now submit the S3 URLs to Suffolk API submitted_documents = [] - + for s3_result in s3_upload_results: # Submit document URL to Suffolk API instead of uploading file document_payload = { - 'filing_id': filing_id, - 'document_url': s3_result['public_url'], # Use public S3 URL - 'document_name': s3_result['original_name'], - 'document_size': s3_result['size'] + "filing_id": filing_id, + "document_url": s3_result["public_url"], # Use public S3 URL + "document_name": s3_result["original_name"], + "document_size": s3_result["size"], } - + api_url = f"https://efile-test.suffolklitlab.org/filings/{filing_id}/documents/" - - headers = { - 'Authorization': f"Bearer {auth_tokens['token']}", - 'Content-Type': 'application/json' - } - + + headers = {"Authorization": f"Bearer {auth_tokens['token']}", "Content-Type": "application/json"} + response = requests.post(api_url, headers=headers, json=document_payload, timeout=60) - + if response.status_code == 201: document_data = response.json() - document_data['s3_url'] = s3_result['public_url'] - document_data['s3_key'] = s3_result['key'] + document_data["s3_url"] = s3_result["public_url"] + document_data["s3_key"] = s3_result["key"] submitted_documents.append(document_data) else: # If submission fails, we should clean up the S3 file logger.warning(f"Failed to submit document to Suffolk API, cleaning up S3 file: {s3_result['key']}") - s3_handler.delete_file(s3_result['key']) - - return JsonResponse({ - 'success': False, - 'error': f'Failed to submit {s3_result["original_name"]} to Suffolk API: {response.text}' - }, status=response.status_code) - - return JsonResponse({ - 'success': True, - 'message': f'Successfully uploaded {len(submitted_documents)} document(s)', - 'documents': submitted_documents, - 's3_uploads': s3_upload_results - }) - + s3_handler.delete_file(s3_result["key"]) + + return JsonResponse( + { + "success": False, + "error": f"Failed to submit {s3_result['original_name']} to Suffolk API: {response.text}", + }, + status=response.status_code, + ) + + return JsonResponse( + { + "success": True, + "message": f"Successfully uploaded {len(submitted_documents)} document(s)", + "documents": submitted_documents, + "s3_uploads": s3_upload_results, + } + ) + except Exception as e: logger.error(f"Upload error: {e}") - return JsonResponse({ - 'success': False, - 'error': f'Upload error: {str(e)}' - }, status=500) + return JsonResponse({"success": False, "error": f"Upload error: {str(e)}"}, status=500) @csrf_exempt @@ -324,33 +314,30 @@ def test_s3_connection(request): # Reinitialize the global handler to pick up the corrected credentials global s3_handler from ..utils.s3_upload import S3UploadHandler + s3_handler = S3UploadHandler() - + # Test S3 connection if s3_handler._ensure_initialized(): - response = s3_handler.s3_client.list_objects_v2( - Bucket=s3_handler.bucket_name, - MaxKeys=1 + # Ensure the client is initialized for type checkers + if s3_handler.s3_client is None: + return JsonResponse({"success": False, "error": "S3 client not initialized"}, status=500) + response = s3_handler.s3_client.list_objects_v2(Bucket=s3_handler.bucket_name, MaxKeys=1) + + return JsonResponse( + { + "success": True, + "message": "S3 connection successful", + "bucket": s3_handler.bucket_name, + "region": s3_handler.region_name, + "objects_exist": "Contents" in response, + } ) - - return JsonResponse({ - 'success': True, - 'message': 'S3 connection successful', - 'bucket': s3_handler.bucket_name, - 'region': s3_handler.region_name, - 'objects_exist': 'Contents' in response - }) else: - return JsonResponse({ - 'success': False, - 'error': 'S3 client not initialized - check AWS credentials' - }) - + return JsonResponse({"success": False, "error": "S3 client not initialized - check AWS credentials"}) + except Exception as e: - return JsonResponse({ - 'success': False, - 'error': f'S3 connection failed: {str(e)}' - }) + return JsonResponse({"success": False, "error": f"S3 connection failed: {str(e)}"}) @csrf_exempt @@ -362,89 +349,82 @@ def simple_s3_upload(request): print(f"DEBUG: Request method: {request.method}") print(f"DEBUG: Request FILES: {list(request.FILES.keys())}") print(f"DEBUG: Request POST: {list(request.POST.keys())}") - + # Handle file uploads - uploaded_files = request.FILES.getlist('documents') - + uploaded_files = request.FILES.getlist("documents") + print(f"DEBUG: Found {len(uploaded_files)} files") - + if not uploaded_files: - return JsonResponse({ - 'success': False, - 'error': 'No documents provided.' - }, status=400) - + return JsonResponse({"success": False, "error": "No documents provided."}, status=400) + # Reinitialize S3 handler global s3_handler from ..utils.s3_upload import S3UploadHandler + s3_handler = S3UploadHandler() - + if not s3_handler._ensure_initialized(): - return JsonResponse({ - 'success': False, - 'error': 'S3 not configured properly. Check AWS credentials.' - }, status=500) - + return JsonResponse( + {"success": False, "error": "S3 not configured properly. Check AWS credentials."}, status=500 + ) + s3_upload_results = [] - + # Upload all files to S3 for i, uploaded_file in enumerate(uploaded_files): # Validate file - validation_result = s3_handler.validate_file( - uploaded_file, - max_size_mb=10, - allowed_types=['.pdf'] - ) - - if not validation_result['valid']: - return JsonResponse({ - 'success': False, - 'error': f'File validation failed for {uploaded_file.name}: {validation_result["error"]}' - }, status=400) - + validation_result = s3_handler.validate_file(uploaded_file, max_size_mb=10, allowed_types=[".pdf"]) + + if not validation_result["valid"]: + return JsonResponse( + { + "success": False, + "error": f"File validation failed for {uploaded_file.name}: {validation_result['error']}", + }, + status=400, + ) + # Prepare metadata - file_type = 'lead' if i == 0 else 'supporting' + file_type = "lead" if i == 0 else "supporting" metadata = { - 'file-type': file_type, - 'original-size': str(uploaded_file.size), - 'original-name': uploaded_file.name, - 'upload-session': str(uuid.uuid4())[:8] + "file-type": file_type, + "original-size": str(uploaded_file.size), + "original-name": uploaded_file.name, + "upload-session": str(uuid.uuid4())[:8], } - + # Upload to S3 - upload_result = s3_handler.upload_file( - uploaded_file, - file_type=file_type, - metadata=metadata + upload_result = s3_handler.upload_file(uploaded_file, file_type=file_type, metadata=metadata) + + if not upload_result["success"]: + return JsonResponse( + {"success": False, "error": f"S3 upload failed for {uploaded_file.name}: {upload_result['error']}"}, + status=500, + ) + + s3_upload_results.append( + { + "original_name": uploaded_file.name, + "url": upload_result["url"], + "public_url": s3_handler.get_public_url(upload_result["key"]), + "key": upload_result["key"], + "size": upload_result["size"], + "type": file_type, + } ) - - if not upload_result['success']: - return JsonResponse({ - 'success': False, - 'error': f'S3 upload failed for {uploaded_file.name}: {upload_result["error"]}' - }, status=500) - - s3_upload_results.append({ - 'original_name': uploaded_file.name, - 'url': upload_result['url'], - 'public_url': s3_handler.get_public_url(upload_result['key']), - 'key': upload_result['key'], - 'size': upload_result['size'], - 'type': file_type - }) - - return JsonResponse({ - 'success': True, - 'message': f'Successfully uploaded {len(s3_upload_results)} file(s) to S3', - 'files': s3_upload_results - }) - + + return JsonResponse( + { + "success": True, + "message": f"Successfully uploaded {len(s3_upload_results)} file(s) to S3", + "files": s3_upload_results, + } + ) + except Exception as e: logger.error(f"Simple S3 upload error: {e}") - return JsonResponse({ - 'success': False, - 'error': f'Upload error: {str(e)}' - }, status=500) + return JsonResponse({"success": False, "error": f"Upload error: {str(e)}"}, status=500) @csrf_exempt @@ -453,55 +433,53 @@ def mock_s3_upload(request): """Mock S3 upload for testing when AWS permissions aren't available.""" try: # Handle file uploads - uploaded_files = request.FILES.getlist('documents') - + uploaded_files = request.FILES.getlist("documents") + if not uploaded_files: - return JsonResponse({ - 'success': False, - 'error': 'No documents provided.' - }, status=400) - + return JsonResponse({"success": False, "error": "No documents provided."}, status=400) + mock_upload_results = [] - + # Simulate S3 upload results for i, uploaded_file in enumerate(uploaded_files): # Validate file type - if not (uploaded_file.name.lower().endswith('.pdf') or uploaded_file.content_type == 'application/pdf'): - return JsonResponse({ - 'success': False, - 'error': f'Invalid file type: {uploaded_file.name}. Only PDF files allowed.' - }, status=400) - + if not (uploaded_file.name.lower().endswith(".pdf") or uploaded_file.content_type == "application/pdf"): + return JsonResponse( + {"success": False, "error": f"Invalid file type: {uploaded_file.name}. Only PDF files allowed."}, + status=400, + ) + # Simulate file size validation max_size = 10 * 1024 * 1024 # 10MB if uploaded_file.size > max_size: - return JsonResponse({ - 'success': False, - 'error': f'File too large: {uploaded_file.name}. Maximum size is 10MB.' - }, status=400) - + return JsonResponse( + {"success": False, "error": f"File too large: {uploaded_file.name}. Maximum size is 10MB."}, + status=400, + ) + # Generate mock S3 URLs file_id = str(uuid.uuid4())[:8] - file_type = 'lead' if i == 0 else 'supporting' - - mock_upload_results.append({ - 'original_name': uploaded_file.name, - 'url': f'https://forms-mvp-xf6361.s3.amazonaws.com/efile-documents/{file_type}/{file_id}.pdf', - 'public_url': f'https://forms-mvp-xf6361.s3.amazonaws.com/efile-documents/{file_type}/{file_id}.pdf', - 'key': f'efile-documents/{file_type}/{file_id}.pdf', - 'size': uploaded_file.size, - 'type': file_type - }) - - return JsonResponse({ - 'success': True, - 'message': f'Mock upload: Successfully processed {len(mock_upload_results)} file(s)', - 'files': mock_upload_results - }) - + file_type = "lead" if i == 0 else "supporting" + + mock_upload_results.append( + { + "original_name": uploaded_file.name, + "url": f"https://forms-mvp-xf6361.s3.amazonaws.com/efile-documents/{file_type}/{file_id}.pdf", + "public_url": f"https://forms-mvp-xf6361.s3.amazonaws.com/efile-documents/{file_type}/{file_id}.pdf", + "key": f"efile-documents/{file_type}/{file_id}.pdf", + "size": uploaded_file.size, + "type": file_type, + } + ) + + return JsonResponse( + { + "success": True, + "message": f"Mock upload: Successfully processed {len(mock_upload_results)} file(s)", + "files": mock_upload_results, + } + ) + except Exception as e: logger.error(f"Mock S3 upload error: {e}") - return JsonResponse({ - 'success': False, - 'error': f'Upload error: {str(e)}' - }, status=500) \ No newline at end of file + return JsonResponse({"success": False, "error": f"Upload error: {str(e)}"}, status=500) diff --git a/efile_app/pyproject.toml b/efile_app/pyproject.toml index 0f13631..ef35031 100644 --- a/efile_app/pyproject.toml +++ b/efile_app/pyproject.toml @@ -1,10 +1,100 @@ +[project] +name = "form-submission-mvp" +version = "0.1.0" +description = "Minimal Django app for form submission and review" +requires-python = ">=3.10" +dependencies = [ + "boto3>=1.40.12", + "Django==5.2.5", + "whitenoise>=6.7", + "gunicorn>=22.0", + "uvicorn>=0.30", + "requests>=2.31,<3", + "python-dotenv>=1.0", + "psycopg[binary]>=3.1", + "dj-database-url>=2.2", +] + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +exclude = [ + "staticfiles*", +] + +[tool.uv] + +[dependency-groups] +dev = [ + "ruff>=0.5.0,<1", + "ty", + "pytest>=8.0", + "pytest-django>=4.8", + "pytest-cov>=5.0", + "pytest-xdist>=3.6", + "pre-commit>=3.7", + "boto3-stubs>=1.40.12", +] + +[project.optional-dependencies] +dev = [ + "ruff>=0.5.0,<1", + "ty", + "pytest>=8.0", + "pytest-django>=4.8", + "pytest-cov>=5.0", + "pytest-xdist>=3.6", + "pre-commit>=3.7", +] + +[tool.ruff] +target-version = "py310" +line-length = 120 +src = ["efile"] +extend-exclude = [ + "**/migrations/*", +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false + +[tool.ruff.lint] +select = [ + "E", "F", + "I", + "UP", + "B", +] +ignore = [] + +[tool.ruff.lint.isort] +known-first-party = ["efile"] + +# Ty (Rust-based Python type checker) +[tool.ty.src] +include = ["efile"] +exclude = ["**/migrations/**"] + +# pytest configuration (pytest and pytest-django) [tool.pytest.ini_options] minversion = "6.0" -addopts = "-ra -q --strict-markers --strict-config" +DJANGO_SETTINGS_MODULE = "efile.settings_dev" +pythonpath = [ + ".", +] testpaths = [ - "tests", - "efile", + ".", ] +# original: +#addopts = "-ra --strict-markers --strict-config" +# new: +#addopts = "--reuse-db --nomigrations" +# merged: +addopts = "-ra --strict-markers --strict-config --reuse-db --nomigrations" markers = [ "slow: marks tests as slow (deselect with '-m \"not slow\"')", "integration: marks tests as integration tests", diff --git a/efile_app/uv.lock b/efile_app/uv.lock new file mode 100644 index 0000000..cec88c5 --- /dev/null +++ b/efile_app/uv.lock @@ -0,0 +1,926 @@ +version = 1 +revision = 2 +requires-python = ">=3.10" + +[[package]] +name = "asgiref" +version = "3.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload_time = "2025-07-08T09:07:43.344Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload_time = "2025-07-08T09:07:41.548Z" }, +] + +[[package]] +name = "boto3" +version = "1.40.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/1d/e7928de166f328d5c7993924c4df048de1c8de759f8d088aab0fa1a0e6b5/boto3-1.40.15.tar.gz", hash = "sha256:271b379ce5ad35ca82f1009e917528a182eed0e2de197ccffb0c51acadec5c79", size = 111977, upload_time = "2025-08-21T19:28:39.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/c1/25a79b651e916c5205740b6ad9d3039c81f588447821232667170afdb9bb/boto3-1.40.15-py3-none-any.whl", hash = "sha256:52b8aa78c9906c4e49dcec6817c041df33c9825073bf66e7df8fc00afbe47b4b", size = 140075, upload_time = "2025-08-21T19:28:38.166Z" }, +] + +[[package]] +name = "boto3-stubs" +version = "1.40.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore-stubs" }, + { name = "types-s3transfer" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/54/c8a0d43c5d17e20433b23ee78cc8348b0cba5a5255d6c2f66aafa86c64ad/boto3_stubs-1.40.15.tar.gz", hash = "sha256:47370ffdfd9f1899900bba554f4ae1846423c459beaccf11e2eae46896af5119", size = 101393, upload_time = "2025-08-21T19:48:27.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/40/fe3cb27e3eee35815902a37d26b7c0af308fe2b08cd0671fb0a8475dfa12/boto3_stubs-1.40.15-py3-none-any.whl", hash = "sha256:95b6a828b758ed56d90ea2530a6794506ca403cfbef3bd2584a2e7c43e3f6607", size = 70011, upload_time = "2025-08-21T19:48:20.458Z" }, +] + +[[package]] +name = "botocore" +version = "1.40.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/ed/b99cdd9d415f8dcb6313e64458958ba5e305927896068e1b69fed5979e50/botocore-1.40.15.tar.gz", hash = "sha256:4960800e4c5a7b43db22550979c22f5a324cbaf75ef494bbb2cf400ef1e6aca7", size = 14369447, upload_time = "2025-08-21T19:28:30.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d7/9490322303df0c0dfa62a3c8c7e15e31259f1a435adacaf058a437cf4e6b/botocore-1.40.15-py3-none-any.whl", hash = "sha256:b364e039d2b67e509cfb089cb39b295251e48a60cc68fd591defbe10b44d83f9", size = 14028055, upload_time = "2025-08-21T19:28:25.62Z" }, +] + +[[package]] +name = "botocore-stubs" +version = "1.38.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-awscrt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/45/27cabc7c3022dcb12de5098cc646b374065f5e72fae13600ff1756f365ee/botocore_stubs-1.38.46.tar.gz", hash = "sha256:a04e69766ab8bae338911c1897492f88d05cd489cd75f06e6eb4f135f9da8c7b", size = 42299, upload_time = "2025-06-29T22:58:24.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/84/06490071e26bab22ac79a684e98445df118adcf80c58c33ba5af184030f2/botocore_stubs-1.38.46-py3-none-any.whl", hash = "sha256:cc21d9a7dd994bdd90872db4664d817c4719b51cda8004fd507a4bf65b085a75", size = 66083, upload_time = "2025-06-29T22:58:22.234Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload_time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload_time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload_time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload_time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload_time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload_time = "2025-08-09T07:55:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload_time = "2025-08-09T07:55:38.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload_time = "2025-08-09T07:55:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload_time = "2025-08-09T07:55:41.706Z" }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload_time = "2025-08-09T07:55:43.262Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload_time = "2025-08-09T07:55:44.903Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload_time = "2025-08-09T07:55:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload_time = "2025-08-09T07:55:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload_time = "2025-08-09T07:55:48.744Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload_time = "2025-08-09T07:55:50.305Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload_time = "2025-08-09T07:55:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload_time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload_time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload_time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload_time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload_time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload_time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload_time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload_time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload_time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload_time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload_time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload_time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload_time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload_time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload_time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload_time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload_time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload_time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload_time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload_time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload_time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload_time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload_time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload_time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload_time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload_time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload_time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload_time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload_time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload_time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload_time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload_time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload_time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload_time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload_time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload_time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload_time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload_time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload_time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload_time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload_time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload_time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload_time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload_time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload_time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload_time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload_time = "2025-05-20T23:19:47.796Z" }, +] + +[[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.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/4e/08b493f1f1d8a5182df0044acc970799b58a8d289608e0d891a03e9d269a/coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27", size = 823798, upload_time = "2025-08-17T00:26:43.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/f4/350759710db50362685f922259c140592dba15eb4e2325656a98413864d9/coverage-7.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d92d6edb0ccafd20c6fbf9891ca720b39c2a6a4b4a6f9cf323ca2c986f33e475", size = 216403, upload_time = "2025-08-17T00:24:19.083Z" }, + { url = "https://files.pythonhosted.org/packages/29/7e/e467c2bb4d5ecfd166bfd22c405cce4c50de2763ba1d78e2729c59539a42/coverage-7.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7202da14dc0236884fcc45665ffb2d79d4991a53fbdf152ab22f69f70923cc22", size = 216802, upload_time = "2025-08-17T00:24:21.824Z" }, + { url = "https://files.pythonhosted.org/packages/62/ab/2accdd1ccfe63b890e5eb39118f63c155202df287798364868a2884a50af/coverage-7.10.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ada418633ae24ec8d0fcad5efe6fc7aa3c62497c6ed86589e57844ad04365674", size = 243558, upload_time = "2025-08-17T00:24:23.569Z" }, + { url = "https://files.pythonhosted.org/packages/43/04/c14c33d0cfc0f4db6b3504d01a47f4c798563d932a836fd5f2dbc0521d3d/coverage-7.10.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b828e33eca6c3322adda3b5884456f98c435182a44917ded05005adfa1415500", size = 245370, upload_time = "2025-08-17T00:24:24.858Z" }, + { url = "https://files.pythonhosted.org/packages/99/71/147053061f1f51c1d3b3d040c3cb26876964a3a0dca0765d2441411ca568/coverage-7.10.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:802793ba397afcfdbe9f91f89d65ae88b958d95edc8caf948e1f47d8b6b2b606", size = 247228, upload_time = "2025-08-17T00:24:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/cc/92/7ef882205d4d4eb502e6154ee7122c1a1b1ce3f29d0166921e0fb550a5d3/coverage-7.10.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d0b23512338c54101d3bf7a1ab107d9d75abda1d5f69bc0887fd079253e4c27e", size = 245270, upload_time = "2025-08-17T00:24:27.424Z" }, + { url = "https://files.pythonhosted.org/packages/ab/3d/297a20603abcc6c7d89d801286eb477b0b861f3c5a4222730f1c9837be3e/coverage-7.10.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f36b7dcf72d06a8c5e2dd3aca02be2b1b5db5f86404627dff834396efce958f2", size = 243287, upload_time = "2025-08-17T00:24:28.697Z" }, + { url = "https://files.pythonhosted.org/packages/65/f9/b04111438f41f1ddd5dc88706d5f8064ae5bb962203c49fe417fa23a362d/coverage-7.10.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fce316c367a1dc2c411821365592eeb335ff1781956d87a0410eae248188ba51", size = 244164, upload_time = "2025-08-17T00:24:30.393Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e5/c7d9eb7a9ea66cf92d069077719fb2b07782dcd7050b01a9b88766b52154/coverage-7.10.4-cp310-cp310-win32.whl", hash = "sha256:8c5dab29fc8070b3766b5fc85f8d89b19634584429a2da6d42da5edfadaf32ae", size = 218917, upload_time = "2025-08-17T00:24:31.67Z" }, + { url = "https://files.pythonhosted.org/packages/66/30/4d9d3b81f5a836b31a7428b8a25e6d490d4dca5ff2952492af130153c35c/coverage-7.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:4b0d114616f0fccb529a1817457d5fb52a10e106f86c5fb3b0bd0d45d0d69b93", size = 219822, upload_time = "2025-08-17T00:24:32.89Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ba/2c9817e62018e7d480d14f684c160b3038df9ff69c5af7d80e97d143e4d1/coverage-7.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:05d5f98ec893d4a2abc8bc5f046f2f4367404e7e5d5d18b83de8fde1093ebc4f", size = 216514, upload_time = "2025-08-17T00:24:34.188Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/093412a959a6b6261446221ba9fb23bb63f661a5de70b5d130763c87f916/coverage-7.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9267efd28f8994b750d171e58e481e3bbd69e44baed540e4c789f8e368b24b88", size = 216914, upload_time = "2025-08-17T00:24:35.881Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/2fdf4a71cfe93b07eae845ebf763267539a7d8b7e16b062f959d56d7e433/coverage-7.10.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4456a039fdc1a89ea60823d0330f1ac6f97b0dbe9e2b6fb4873e889584b085fb", size = 247308, upload_time = "2025-08-17T00:24:37.61Z" }, + { url = "https://files.pythonhosted.org/packages/ba/16/33f6cded458e84f008b9f6bc379609a6a1eda7bffe349153b9960803fc11/coverage-7.10.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c2bfbd2a9f7e68a21c5bd191be94bfdb2691ac40d325bac9ef3ae45ff5c753d9", size = 249241, upload_time = "2025-08-17T00:24:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/84/98/9c18e47c889be58339ff2157c63b91a219272503ee32b49d926eea2337f2/coverage-7.10.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab7765f10ae1df7e7fe37de9e64b5a269b812ee22e2da3f84f97b1c7732a0d8", size = 251346, upload_time = "2025-08-17T00:24:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/6d/07/00a6c0d53e9a22d36d8e95ddd049b860eef8f4b9fd299f7ce34d8e323356/coverage-7.10.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a09b13695166236e171ec1627ff8434b9a9bae47528d0ba9d944c912d33b3d2", size = 249037, upload_time = "2025-08-17T00:24:41.904Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0e/1e1b944d6a6483d07bab5ef6ce063fcf3d0cc555a16a8c05ebaab11f5607/coverage-7.10.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5c9e75dfdc0167d5675e9804f04a56b2cf47fb83a524654297000b578b8adcb7", size = 247090, upload_time = "2025-08-17T00:24:43.193Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/2ce5ab8a728b8e25ced077111581290ffaef9efaf860a28e25435ab925cf/coverage-7.10.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c751261bfe6481caba15ec005a194cb60aad06f29235a74c24f18546d8377df0", size = 247732, upload_time = "2025-08-17T00:24:44.906Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f3/706c4a24f42c1c5f3a2ca56637ab1270f84d9e75355160dc34d5e39bb5b7/coverage-7.10.4-cp311-cp311-win32.whl", hash = "sha256:051c7c9e765f003c2ff6e8c81ccea28a70fb5b0142671e4e3ede7cebd45c80af", size = 218961, upload_time = "2025-08-17T00:24:46.241Z" }, + { url = "https://files.pythonhosted.org/packages/e8/aa/6b9ea06e0290bf1cf2a2765bba89d561c5c563b4e9db8298bf83699c8b67/coverage-7.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a647b152f10be08fb771ae4a1421dbff66141e3d8ab27d543b5eb9ea5af8e52", size = 219851, upload_time = "2025-08-17T00:24:48.795Z" }, + { url = "https://files.pythonhosted.org/packages/8b/be/f0dc9ad50ee183369e643cd7ed8f2ef5c491bc20b4c3387cbed97dd6e0d1/coverage-7.10.4-cp311-cp311-win_arm64.whl", hash = "sha256:b09b9e4e1de0d406ca9f19a371c2beefe3193b542f64a6dd40cfcf435b7d6aa0", size = 218530, upload_time = "2025-08-17T00:24:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/781c9e4dd57cabda2a28e2ce5b00b6be416015265851060945a5ed4bd85e/coverage-7.10.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a1f0264abcabd4853d4cb9b3d164adbf1565da7dab1da1669e93f3ea60162d79", size = 216706, upload_time = "2025-08-17T00:24:51.528Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8c/51255202ca03d2e7b664770289f80db6f47b05138e06cce112b3957d5dfd/coverage-7.10.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:536cbe6b118a4df231b11af3e0f974a72a095182ff8ec5f4868c931e8043ef3e", size = 216939, upload_time = "2025-08-17T00:24:53.171Z" }, + { url = "https://files.pythonhosted.org/packages/06/7f/df11131483698660f94d3c847dc76461369782d7a7644fcd72ac90da8fd0/coverage-7.10.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9a4c0d84134797b7bf3f080599d0cd501471f6c98b715405166860d79cfaa97e", size = 248429, upload_time = "2025-08-17T00:24:54.934Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fa/13ac5eda7300e160bf98f082e75f5c5b4189bf3a883dd1ee42dbedfdc617/coverage-7.10.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c155fc0f9cee8c9803ea0ad153ab6a3b956baa5d4cd993405dc0b45b2a0b9e0", size = 251178, upload_time = "2025-08-17T00:24:56.353Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/f63b56a58ad0bec68a840e7be6b7ed9d6f6288d790760647bb88f5fea41e/coverage-7.10.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5f2ab6e451d4b07855d8bcf063adf11e199bff421a4ba57f5bb95b7444ca62", size = 252313, upload_time = "2025-08-17T00:24:57.692Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b6/79338f1ea27b01266f845afb4485976211264ab92407d1c307babe3592a7/coverage-7.10.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:685b67d99b945b0c221be0780c336b303a7753b3e0ec0d618c795aada25d5e7a", size = 250230, upload_time = "2025-08-17T00:24:59.293Z" }, + { url = "https://files.pythonhosted.org/packages/bc/93/3b24f1da3e0286a4dc5832427e1d448d5296f8287464b1ff4a222abeeeb5/coverage-7.10.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0c079027e50c2ae44da51c2e294596cbc9dbb58f7ca45b30651c7e411060fc23", size = 248351, upload_time = "2025-08-17T00:25:00.676Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/d59412f869e49dcc5b89398ef3146c8bfaec870b179cc344d27932e0554b/coverage-7.10.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3749aa72b93ce516f77cf5034d8e3c0dfd45c6e8a163a602ede2dc5f9a0bb927", size = 249788, upload_time = "2025-08-17T00:25:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/cc/52/04a3b733f40a0cc7c4a5b9b010844111dbf906df3e868b13e1ce7b39ac31/coverage-7.10.4-cp312-cp312-win32.whl", hash = "sha256:fecb97b3a52fa9bcd5a7375e72fae209088faf671d39fae67261f37772d5559a", size = 219131, upload_time = "2025-08-17T00:25:03.79Z" }, + { url = "https://files.pythonhosted.org/packages/83/dd/12909fc0b83888197b3ec43a4ac7753589591c08d00d9deda4158df2734e/coverage-7.10.4-cp312-cp312-win_amd64.whl", hash = "sha256:26de58f355626628a21fe6a70e1e1fad95702dafebfb0685280962ae1449f17b", size = 219939, upload_time = "2025-08-17T00:25:05.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/c7/058bb3220fdd6821bada9685eadac2940429ab3c97025ce53549ff423cc1/coverage-7.10.4-cp312-cp312-win_arm64.whl", hash = "sha256:67e8885408f8325198862bc487038a4980c9277d753cb8812510927f2176437a", size = 218572, upload_time = "2025-08-17T00:25:06.897Z" }, + { url = "https://files.pythonhosted.org/packages/46/b0/4a3662de81f2ed792a4e425d59c4ae50d8dd1d844de252838c200beed65a/coverage-7.10.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b8e1d2015d5dfdbf964ecef12944c0c8c55b885bb5c0467ae8ef55e0e151233", size = 216735, upload_time = "2025-08-17T00:25:08.617Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e8/e2dcffea01921bfffc6170fb4406cffb763a3b43a047bbd7923566708193/coverage-7.10.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25735c299439018d66eb2dccf54f625aceb78645687a05f9f848f6e6c751e169", size = 216982, upload_time = "2025-08-17T00:25:10.384Z" }, + { url = "https://files.pythonhosted.org/packages/9d/59/cc89bb6ac869704d2781c2f5f7957d07097c77da0e8fdd4fd50dbf2ac9c0/coverage-7.10.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:715c06cb5eceac4d9b7cdf783ce04aa495f6aff657543fea75c30215b28ddb74", size = 247981, upload_time = "2025-08-17T00:25:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/aa/23/3da089aa177ceaf0d3f96754ebc1318597822e6387560914cc480086e730/coverage-7.10.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e017ac69fac9aacd7df6dc464c05833e834dc5b00c914d7af9a5249fcccf07ef", size = 250584, upload_time = "2025-08-17T00:25:13.483Z" }, + { url = "https://files.pythonhosted.org/packages/ad/82/e8693c368535b4e5fad05252a366a1794d481c79ae0333ed943472fd778d/coverage-7.10.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad180cc40b3fccb0f0e8c702d781492654ac2580d468e3ffc8065e38c6c2408", size = 251856, upload_time = "2025-08-17T00:25:15.27Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/8b9cb13292e602fa4135b10a26ac4ce169a7fc7c285ff08bedd42ff6acca/coverage-7.10.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:becbdcd14f685fada010a5f792bf0895675ecf7481304fe159f0cd3f289550bd", size = 250015, upload_time = "2025-08-17T00:25:16.759Z" }, + { url = "https://files.pythonhosted.org/packages/10/e7/e5903990ce089527cf1c4f88b702985bd65c61ac245923f1ff1257dbcc02/coverage-7.10.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0b485ca21e16a76f68060911f97ebbe3e0d891da1dbbce6af7ca1ab3f98b9097", size = 247908, upload_time = "2025-08-17T00:25:18.232Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/7d464f116df1df7fe340669af1ddbe1a371fc60f3082ff3dc837c4f1f2ab/coverage-7.10.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d098ccfe8e1e0a1ed9a0249138899948afd2978cbf48eb1cc3fcd38469690", size = 249525, upload_time = "2025-08-17T00:25:20.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/722e0cdbf6c19e7235c2020837d4e00f3b07820fd012201a983238cc3a30/coverage-7.10.4-cp313-cp313-win32.whl", hash = "sha256:8630f8af2ca84b5c367c3df907b1706621abe06d6929f5045fd628968d421e6e", size = 219173, upload_time = "2025-08-17T00:25:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/97/7e/aa70366f8275955cd51fa1ed52a521c7fcebcc0fc279f53c8c1ee6006dfe/coverage-7.10.4-cp313-cp313-win_amd64.whl", hash = "sha256:f68835d31c421736be367d32f179e14ca932978293fe1b4c7a6a49b555dff5b2", size = 219969, upload_time = "2025-08-17T00:25:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/ac/96/c39d92d5aad8fec28d4606556bfc92b6fee0ab51e4a548d9b49fb15a777c/coverage-7.10.4-cp313-cp313-win_arm64.whl", hash = "sha256:6eaa61ff6724ca7ebc5326d1fae062d85e19b38dd922d50903702e6078370ae7", size = 218601, upload_time = "2025-08-17T00:25:25.295Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/34d549a6177bd80fa5db758cb6fd3057b7ad9296d8707d4ab7f480b0135f/coverage-7.10.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:702978108876bfb3d997604930b05fe769462cc3000150b0e607b7b444f2fd84", size = 217445, upload_time = "2025-08-17T00:25:27.129Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c0/433da866359bf39bf595f46d134ff2d6b4293aeea7f3328b6898733b0633/coverage-7.10.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e8f978e8c5521d9c8f2086ac60d931d583fab0a16f382f6eb89453fe998e2484", size = 217676, upload_time = "2025-08-17T00:25:28.641Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d7/2b99aa8737f7801fd95222c79a4ebc8c5dd4460d4bed7ef26b17a60c8d74/coverage-7.10.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:df0ac2ccfd19351411c45e43ab60932b74472e4648b0a9edf6a3b58846e246a9", size = 259002, upload_time = "2025-08-17T00:25:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/08/cf/86432b69d57debaef5abf19aae661ba8f4fcd2882fa762e14added4bd334/coverage-7.10.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73a0d1aaaa3796179f336448e1576a3de6fc95ff4f07c2d7251d4caf5d18cf8d", size = 261178, upload_time = "2025-08-17T00:25:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/23/78/85176593f4aa6e869cbed7a8098da3448a50e3fac5cb2ecba57729a5220d/coverage-7.10.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:873da6d0ed6b3ffc0bc01f2c7e3ad7e2023751c0d8d86c26fe7322c314b031dc", size = 263402, upload_time = "2025-08-17T00:25:33.339Z" }, + { url = "https://files.pythonhosted.org/packages/88/1d/57a27b6789b79abcac0cc5805b31320d7a97fa20f728a6a7c562db9a3733/coverage-7.10.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c6446c75b0e7dda5daa876a1c87b480b2b52affb972fedd6c22edf1aaf2e00ec", size = 260957, upload_time = "2025-08-17T00:25:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e5/3e5ddfd42835c6def6cd5b2bdb3348da2e34c08d9c1211e91a49e9fd709d/coverage-7.10.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6e73933e296634e520390c44758d553d3b573b321608118363e52113790633b9", size = 258718, upload_time = "2025-08-17T00:25:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/d364f0f7ef111615dc4e05a6ed02cac7b6f2ac169884aa57faeae9eb5fa0/coverage-7.10.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52073d4b08d2cb571234c8a71eb32af3c6923149cf644a51d5957ac128cf6aa4", size = 259848, upload_time = "2025-08-17T00:25:37.754Z" }, + { url = "https://files.pythonhosted.org/packages/10/c6/bbea60a3b309621162e53faf7fac740daaf083048ea22077418e1ecaba3f/coverage-7.10.4-cp313-cp313t-win32.whl", hash = "sha256:e24afb178f21f9ceb1aefbc73eb524769aa9b504a42b26857243f881af56880c", size = 219833, upload_time = "2025-08-17T00:25:39.252Z" }, + { url = "https://files.pythonhosted.org/packages/44/a5/f9f080d49cfb117ddffe672f21eab41bd23a46179a907820743afac7c021/coverage-7.10.4-cp313-cp313t-win_amd64.whl", hash = "sha256:be04507ff1ad206f4be3d156a674e3fb84bbb751ea1b23b142979ac9eebaa15f", size = 220897, upload_time = "2025-08-17T00:25:40.772Z" }, + { url = "https://files.pythonhosted.org/packages/46/89/49a3fc784fa73d707f603e586d84a18c2e7796707044e9d73d13260930b7/coverage-7.10.4-cp313-cp313t-win_arm64.whl", hash = "sha256:f3e3ff3f69d02b5dad67a6eac68cc9c71ae343b6328aae96e914f9f2f23a22e2", size = 219160, upload_time = "2025-08-17T00:25:42.229Z" }, + { url = "https://files.pythonhosted.org/packages/b5/22/525f84b4cbcff66024d29f6909d7ecde97223f998116d3677cfba0d115b5/coverage-7.10.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a59fe0af7dd7211ba595cf7e2867458381f7e5d7b4cffe46274e0b2f5b9f4eb4", size = 216717, upload_time = "2025-08-17T00:25:43.875Z" }, + { url = "https://files.pythonhosted.org/packages/a6/58/213577f77efe44333a416d4bcb251471e7f64b19b5886bb515561b5ce389/coverage-7.10.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a6c35c5b70f569ee38dc3350cd14fdd0347a8b389a18bb37538cc43e6f730e6", size = 216994, upload_time = "2025-08-17T00:25:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/17/85/34ac02d0985a09472f41b609a1d7babc32df87c726c7612dc93d30679b5a/coverage-7.10.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acb7baf49f513554c4af6ef8e2bd6e8ac74e6ea0c7386df8b3eb586d82ccccc4", size = 248038, upload_time = "2025-08-17T00:25:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/47/4f/2140305ec93642fdaf988f139813629cbb6d8efa661b30a04b6f7c67c31e/coverage-7.10.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a89afecec1ed12ac13ed203238b560cbfad3522bae37d91c102e690b8b1dc46c", size = 250575, upload_time = "2025-08-17T00:25:48.613Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b5/41b5784180b82a083c76aeba8f2c72ea1cb789e5382157b7dc852832aea2/coverage-7.10.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:480442727f464407d8ade6e677b7f21f3b96a9838ab541b9a28ce9e44123c14e", size = 251927, upload_time = "2025-08-17T00:25:50.881Z" }, + { url = "https://files.pythonhosted.org/packages/78/ca/c1dd063e50b71f5aea2ebb27a1c404e7b5ecf5714c8b5301f20e4e8831ac/coverage-7.10.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a89bf193707f4a17f1ed461504031074d87f035153239f16ce86dfb8f8c7ac76", size = 249930, upload_time = "2025-08-17T00:25:52.422Z" }, + { url = "https://files.pythonhosted.org/packages/8d/66/d8907408612ffee100d731798e6090aedb3ba766ecf929df296c1a7ee4fb/coverage-7.10.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3ddd912c2fc440f0fb3229e764feec85669d5d80a988ff1b336a27d73f63c818", size = 247862, upload_time = "2025-08-17T00:25:54.316Z" }, + { url = "https://files.pythonhosted.org/packages/29/db/53cd8ec8b1c9c52d8e22a25434785bfc2d1e70c0cfb4d278a1326c87f741/coverage-7.10.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a538944ee3a42265e61c7298aeba9ea43f31c01271cf028f437a7b4075592cf", size = 249360, upload_time = "2025-08-17T00:25:55.833Z" }, + { url = "https://files.pythonhosted.org/packages/4f/75/5ec0a28ae4a0804124ea5a5becd2b0fa3adf30967ac656711fb5cdf67c60/coverage-7.10.4-cp314-cp314-win32.whl", hash = "sha256:fd2e6002be1c62476eb862b8514b1ba7e7684c50165f2a8d389e77da6c9a2ebd", size = 219449, upload_time = "2025-08-17T00:25:57.984Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ab/66e2ee085ec60672bf5250f11101ad8143b81f24989e8c0e575d16bb1e53/coverage-7.10.4-cp314-cp314-win_amd64.whl", hash = "sha256:ec113277f2b5cf188d95fb66a65c7431f2b9192ee7e6ec9b72b30bbfb53c244a", size = 220246, upload_time = "2025-08-17T00:25:59.868Z" }, + { url = "https://files.pythonhosted.org/packages/37/3b/00b448d385f149143190846217797d730b973c3c0ec2045a7e0f5db3a7d0/coverage-7.10.4-cp314-cp314-win_arm64.whl", hash = "sha256:9744954bfd387796c6a091b50d55ca7cac3d08767795b5eec69ad0f7dbf12d38", size = 218825, upload_time = "2025-08-17T00:26:01.44Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2e/55e20d3d1ce00b513efb6fd35f13899e1c6d4f76c6cbcc9851c7227cd469/coverage-7.10.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5af4829904dda6aabb54a23879f0f4412094ba9ef153aaa464e3c1b1c9bc98e6", size = 217462, upload_time = "2025-08-17T00:26:03.014Z" }, + { url = "https://files.pythonhosted.org/packages/47/b3/aab1260df5876f5921e2c57519e73a6f6eeacc0ae451e109d44ee747563e/coverage-7.10.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7bba5ed85e034831fac761ae506c0644d24fd5594727e174b5a73aff343a7508", size = 217675, upload_time = "2025-08-17T00:26:04.606Z" }, + { url = "https://files.pythonhosted.org/packages/67/23/1cfe2aa50c7026180989f0bfc242168ac7c8399ccc66eb816b171e0ab05e/coverage-7.10.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57d555b0719834b55ad35045de6cc80fc2b28e05adb6b03c98479f9553b387f", size = 259176, upload_time = "2025-08-17T00:26:06.159Z" }, + { url = "https://files.pythonhosted.org/packages/9d/72/5882b6aeed3f9de7fc4049874fd7d24213bf1d06882f5c754c8a682606ec/coverage-7.10.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ba62c51a72048bb1ea72db265e6bd8beaabf9809cd2125bbb5306c6ce105f214", size = 261341, upload_time = "2025-08-17T00:26:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/1b/70/a0c76e3087596ae155f8e71a49c2c534c58b92aeacaf4d9d0cbbf2dde53b/coverage-7.10.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0acf0c62a6095f07e9db4ec365cc58c0ef5babb757e54745a1aa2ea2a2564af1", size = 263600, upload_time = "2025-08-17T00:26:11.045Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5f/27e4cd4505b9a3c05257fb7fc509acbc778c830c450cb4ace00bf2b7bda7/coverage-7.10.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1033bf0f763f5cf49ffe6594314b11027dcc1073ac590b415ea93463466deec", size = 261036, upload_time = "2025-08-17T00:26:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/02/d6/cf2ae3a7f90ab226ea765a104c4e76c5126f73c93a92eaea41e1dc6a1892/coverage-7.10.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92c29eff894832b6a40da1789b1f252305af921750b03ee4535919db9179453d", size = 258794, upload_time = "2025-08-17T00:26:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/39f222eab0d78aa2001cdb7852aa1140bba632db23a5cfd832218b496d6c/coverage-7.10.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:822c4c830989c2093527e92acd97be4638a44eb042b1bdc0e7a278d84a070bd3", size = 259946, upload_time = "2025-08-17T00:26:15.899Z" }, + { url = "https://files.pythonhosted.org/packages/74/b2/49d82acefe2fe7c777436a3097f928c7242a842538b190f66aac01f29321/coverage-7.10.4-cp314-cp314t-win32.whl", hash = "sha256:e694d855dac2e7cf194ba33653e4ba7aad7267a802a7b3fc4347d0517d5d65cd", size = 220226, upload_time = "2025-08-17T00:26:17.566Z" }, + { url = "https://files.pythonhosted.org/packages/06/b0/afb942b6b2fc30bdbc7b05b087beae11c2b0daaa08e160586cf012b6ad70/coverage-7.10.4-cp314-cp314t-win_amd64.whl", hash = "sha256:efcc54b38ef7d5bfa98050f220b415bc5bb3d432bd6350a861cf6da0ede2cdcd", size = 221346, upload_time = "2025-08-17T00:26:19.311Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/e0531c9d1525cb6eac5b5733c76f27f3053ee92665f83f8899516fea6e76/coverage-7.10.4-cp314-cp314t-win_arm64.whl", hash = "sha256:6f3a3496c0fa26bfac4ebc458747b778cff201c8ae94fa05e1391bab0dbc473c", size = 219368, upload_time = "2025-08-17T00:26:21.011Z" }, + { url = "https://files.pythonhosted.org/packages/bb/78/983efd23200921d9edb6bd40512e1aa04af553d7d5a171e50f9b2b45d109/coverage-7.10.4-py3-none-any.whl", hash = "sha256:065d75447228d05121e5c938ca8f0e91eed60a1eb2d1258d42d5084fecfc3302", size = 208365, upload_time = "2025-08-17T00:26:41.479Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[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.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/05/2ec51009f4ce424877dbd8ad95868faec0c3494ed0ff1635f9ab53d9e0ee/dj_database_url-3.0.1.tar.gz", hash = "sha256:8994961efb888fc6bf8c41550870c91f6f7691ca751888ebaa71442b7f84eff8", size = 12556, upload_time = "2025-07-02T09:40:11.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/5e/86a43c6fdaa41c12d58e4ff3ebbfd6b71a7cb0360a08614e3754ef2e9afb/dj_database_url-3.0.1-py3-none-any.whl", hash = "sha256:43950018e1eeea486bf11136384aec0fe55b29fe6fd8a44553231b85661d9383", size = 8808, upload_time = "2025-07-02T09:40:26.326Z" }, +] + +[[package]] +name = "django" +version = "5.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/9b/779f853c3d2d58b9e08346061ff3e331cdec3fe3f53aae509e256412a593/django-5.2.5.tar.gz", hash = "sha256:0745b25681b129a77aae3d4f6549b62d3913d74407831abaa0d9021a03954bae", size = 10859748, upload_time = "2025-08-06T08:26:29.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/6e/98a1d23648e0085bb5825326af17612ecd8fc76be0ce96ea4dc35e17b926/django-5.2.5-py3-none-any.whl", hash = "sha256:2b2ada0ee8a5ff743a40e2b9820d1f8e24c11bac9ae6469cd548f0057ea6ddcd", size = 8302999, upload_time = "2025-08-06T08:26:23.562Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload_time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload_time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload_time = "2024-04-08T09:04:19.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload_time = "2024-04-08T09:04:17.414Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload_time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload_time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "form-submission-mvp" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "boto3" }, + { name = "dj-database-url" }, + { name = "django" }, + { name = "gunicorn" }, + { name = "psycopg", extra = ["binary"] }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "uvicorn" }, + { name = "whitenoise" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "ty" }, +] + +[package.dev-dependencies] +dev = [ + { name = "boto3-stubs" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "ty" }, +] + +[package.metadata] +requires-dist = [ + { name = "boto3", specifier = ">=1.40.12" }, + { name = "dj-database-url", specifier = ">=2.2" }, + { name = "django", specifier = "==5.2.5" }, + { name = "gunicorn", specifier = ">=22.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.7" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" }, + { name = "pytest-django", marker = "extra == 'dev'", specifier = ">=4.8" }, + { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.6" }, + { name = "python-dotenv", specifier = ">=1.0" }, + { name = "requests", specifier = ">=2.31,<3" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5.0,<1" }, + { name = "ty", marker = "extra == 'dev'" }, + { name = "uvicorn", specifier = ">=0.30" }, + { name = "whitenoise", specifier = ">=6.7" }, +] +provides-extras = ["dev"] + +[package.metadata.requires-dev] +dev = [ + { name = "boto3-stubs", specifier = ">=1.40.12" }, + { name = "pre-commit", specifier = ">=3.7" }, + { name = "pytest", specifier = ">=8.0" }, + { name = "pytest-cov", specifier = ">=5.0" }, + { name = "pytest-django", specifier = ">=4.8" }, + { name = "pytest-xdist", specifier = ">=3.6" }, + { name = "ruff", specifier = ">=0.5.0,<1" }, + { name = "ty" }, +] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload_time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload_time = "2024-08-10T20:25:24.996Z" }, +] + +[[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.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload_time = "2025-08-09T19:35:00.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload_time = "2025-08-09T19:34:59.1Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload_time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload_time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload_time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload_time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload_time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload_time = "2024-06-04T18:44:08.352Z" }, +] + +[[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 = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload_time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload_time = "2025-05-07T22:47:40.376Z" }, +] + +[[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 = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload_time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload_time = "2025-08-09T18:56:13.192Z" }, +] + +[[package]] +name = "psycopg" +version = "3.2.9" +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/27/4a/93a6ab570a8d1a4ad171a1f4256e205ce48d828781312c0bbaff36380ecb/psycopg-3.2.9.tar.gz", hash = "sha256:2fbb46fcd17bc81f993f28c47f1ebea38d66ae97cc2dbc3cad73b37cefbff700", size = 158122, upload_time = "2025-05-13T16:11:15.533Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b0/a73c195a56eb6b92e937a5ca58521a5c3346fb233345adc80fd3e2f542e2/psycopg-3.2.9-py3-none-any.whl", hash = "sha256:01a8dadccdaac2123c916208c96e06631641c0566b22005493f09663c7a8d3b6", size = 202705, upload_time = "2025-05-13T16:06:26.584Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/ce/d677bc51f9b180986e5515268603519cee682eb6b5e765ae46cdb8526579/psycopg_binary-3.2.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:528239bbf55728ba0eacbd20632342867590273a9bacedac7538ebff890f1093", size = 4033081, upload_time = "2025-05-13T16:06:29.666Z" }, + { url = "https://files.pythonhosted.org/packages/de/f4/b56263eb20dc36d71d7188622872098400536928edf86895736e28546b3c/psycopg_binary-3.2.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4978c01ca4c208c9d6376bd585e2c0771986b76ff7ea518f6d2b51faece75e8", size = 4082141, upload_time = "2025-05-13T16:06:33.81Z" }, + { url = "https://files.pythonhosted.org/packages/68/47/5316c3b0a2b1ff5f1d440a27638250569994534874a2ce88bf24f5c51c0f/psycopg_binary-3.2.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ed2bab85b505d13e66a914d0f8cdfa9475c16d3491cf81394e0748b77729af2", size = 4678993, upload_time = "2025-05-13T16:06:36.309Z" }, + { url = "https://files.pythonhosted.org/packages/53/24/b2c667b59f07fd7d7805c0c2074351bf2b98a336c5030d961db316512ffb/psycopg_binary-3.2.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799fa1179ab8a58d1557a95df28b492874c8f4135101b55133ec9c55fc9ae9d7", size = 4500117, upload_time = "2025-05-13T16:06:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/ae/91/a08f8878b0fe0b34b083c149df950bce168bc1b18b2fe849fa42bf4378d4/psycopg_binary-3.2.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb37ac3955d19e4996c3534abfa4f23181333974963826db9e0f00731274b695", size = 4766985, upload_time = "2025-05-13T16:06:42.502Z" }, + { url = "https://files.pythonhosted.org/packages/10/be/3a45d5b7d8f4c4332fd42465f2170b5aef4d28a7c79e79ac7e5e1dac74d7/psycopg_binary-3.2.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:001e986656f7e06c273dd4104e27f4b4e0614092e544d950c7c938d822b1a894", size = 4461990, upload_time = "2025-05-13T16:06:45.971Z" }, + { url = "https://files.pythonhosted.org/packages/03/ce/20682b9a4fc270d8dc644a0b16c1978732146c6ff0abbc48fbab2f4a70aa/psycopg_binary-3.2.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa5c80d8b4cbf23f338db88a7251cef8bb4b68e0f91cf8b6ddfa93884fdbb0c1", size = 3777947, upload_time = "2025-05-13T16:06:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/07/5c/f6d486e00bcd8709908ccdd436b2a190d390dfd61e318de4060bc6ee2a1e/psycopg_binary-3.2.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:39a127e0cf9b55bd4734a8008adf3e01d1fd1cb36339c6a9e2b2cbb6007c50ee", size = 3337502, upload_time = "2025-05-13T16:06:51.378Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a1/086508e929c0123a7f532840bb0a0c8a1ebd7e06aef3ee7fa44a3589bcdf/psycopg_binary-3.2.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fb7599e436b586e265bea956751453ad32eb98be6a6e694252f4691c31b16edb", size = 3440809, upload_time = "2025-05-13T16:06:54.552Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/3a347a0f894355a6b173fca2202eca279b6197727b24e4896cf83f4263ee/psycopg_binary-3.2.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5d2c9fe14fe42b3575a0b4e09b081713e83b762c8dc38a3771dd3265f8f110e7", size = 3497231, upload_time = "2025-05-13T16:06:58.858Z" }, + { url = "https://files.pythonhosted.org/packages/18/31/0845a385eb6f4521b398793293b5f746a101e80d5c43792990442d26bc2e/psycopg_binary-3.2.9-cp310-cp310-win_amd64.whl", hash = "sha256:7e4660fad2807612bb200de7262c88773c3483e85d981324b3c647176e41fdc8", size = 2936845, upload_time = "2025-05-13T16:07:02.712Z" }, + { url = "https://files.pythonhosted.org/packages/b6/84/259ea58aca48e03c3c793b4ccfe39ed63db7b8081ef784d039330d9eed96/psycopg_binary-3.2.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2504e9fd94eabe545d20cddcc2ff0da86ee55d76329e1ab92ecfcc6c0a8156c4", size = 4040785, upload_time = "2025-05-13T16:07:07.569Z" }, + { url = "https://files.pythonhosted.org/packages/25/22/ce58ffda2b7e36e45042b4d67f1bbd4dd2ccf4cfd2649696685c61046475/psycopg_binary-3.2.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:093a0c079dd6228a7f3c3d82b906b41964eaa062a9a8c19f45ab4984bf4e872b", size = 4087601, upload_time = "2025-05-13T16:07:11.75Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/b043e85268650c245025e80039b79663d8986f857bc3d3a72b1de67f3550/psycopg_binary-3.2.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:387c87b51d72442708e7a853e7e7642717e704d59571da2f3b29e748be58c78a", size = 4676524, upload_time = "2025-05-13T16:07:17.038Z" }, + { url = "https://files.pythonhosted.org/packages/da/29/7afbfbd3740ea52fda488db190ef2ef2a9ff7379b85501a2142fb9f7dd56/psycopg_binary-3.2.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9ac10a2ebe93a102a326415b330fff7512f01a9401406896e78a81d75d6eddc", size = 4495671, upload_time = "2025-05-13T16:07:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/ea/eb/df69112d18a938cbb74efa1573082248437fa663ba66baf2cdba8a95a2d0/psycopg_binary-3.2.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72fdbda5b4c2a6a72320857ef503a6589f56d46821592d4377c8c8604810342b", size = 4768132, upload_time = "2025-05-13T16:07:25.818Z" }, + { url = "https://files.pythonhosted.org/packages/76/fe/4803b20220c04f508f50afee9169268553f46d6eed99640a08c8c1e76409/psycopg_binary-3.2.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f34e88940833d46108f949fdc1fcfb74d6b5ae076550cd67ab59ef47555dba95", size = 4458394, upload_time = "2025-05-13T16:07:29.148Z" }, + { url = "https://files.pythonhosted.org/packages/0f/0f/5ecc64607ef6f62b04e610b7837b1a802ca6f7cb7211339f5d166d55f1dd/psycopg_binary-3.2.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a3e0f89fe35cb03ff1646ab663dabf496477bab2a072315192dbaa6928862891", size = 3776879, upload_time = "2025-05-13T16:07:32.503Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d8/1c3d6e99b7db67946d0eac2cd15d10a79aa7b1e3222ce4aa8e7df72027f5/psycopg_binary-3.2.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6afb3e62f2a3456f2180a4eef6b03177788df7ce938036ff7f09b696d418d186", size = 3333329, upload_time = "2025-05-13T16:07:35.555Z" }, + { url = "https://files.pythonhosted.org/packages/d7/02/a4e82099816559f558ccaf2b6945097973624dc58d5d1c91eb1e54e5a8e9/psycopg_binary-3.2.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:cc19ed5c7afca3f6b298bfc35a6baa27adb2019670d15c32d0bb8f780f7d560d", size = 3435683, upload_time = "2025-05-13T16:07:37.863Z" }, + { url = "https://files.pythonhosted.org/packages/91/e4/f27055290d58e8818bed8a297162a096ef7f8ecdf01d98772d4b02af46c4/psycopg_binary-3.2.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc75f63653ce4ec764c8f8c8b0ad9423e23021e1c34a84eb5f4ecac8538a4a4a", size = 3497124, upload_time = "2025-05-13T16:07:40.567Z" }, + { url = "https://files.pythonhosted.org/packages/67/3d/17ed07579625529534605eeaeba34f0536754a5667dbf20ea2624fc80614/psycopg_binary-3.2.9-cp311-cp311-win_amd64.whl", hash = "sha256:3db3ba3c470801e94836ad78bf11fd5fab22e71b0c77343a1ee95d693879937a", size = 2939520, upload_time = "2025-05-13T16:07:45.467Z" }, + { url = "https://files.pythonhosted.org/packages/29/6f/ec9957e37a606cd7564412e03f41f1b3c3637a5be018d0849914cb06e674/psycopg_binary-3.2.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be7d650a434921a6b1ebe3fff324dbc2364393eb29d7672e638ce3e21076974e", size = 4022205, upload_time = "2025-05-13T16:07:48.195Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/497b8bea72b20a862ac95a94386967b745a472d9ddc88bc3f32d5d5f0d43/psycopg_binary-3.2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76b4722a529390683c0304501f238b365a46b1e5fb6b7249dbc0ad6fea51a0", size = 4083795, upload_time = "2025-05-13T16:07:50.917Z" }, + { url = "https://files.pythonhosted.org/packages/42/07/af9503e8e8bdad3911fd88e10e6a29240f9feaa99f57d6fac4a18b16f5a0/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96a551e4683f1c307cfc3d9a05fec62c00a7264f320c9962a67a543e3ce0d8ff", size = 4655043, upload_time = "2025-05-13T16:07:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/aff8c9850df1648cc6a5cc7a381f11ee78d98a6b807edd4a5ae276ad60ad/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61d0a6ceed8f08c75a395bc28cb648a81cf8dee75ba4650093ad1a24a51c8724", size = 4477972, upload_time = "2025-05-13T16:07:57.925Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/8e9d1b77ec1a632818fe2f457c3a65af83c68710c4c162d6866947d08cc5/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad280bbd409bf598683dda82232f5215cfc5f2b1bf0854e409b4d0c44a113b1d", size = 4737516, upload_time = "2025-05-13T16:08:01.616Z" }, + { url = "https://files.pythonhosted.org/packages/46/ec/222238f774cd5a0881f3f3b18fb86daceae89cc410f91ef6a9fb4556f236/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76eddaf7fef1d0994e3d536ad48aa75034663d3a07f6f7e3e601105ae73aeff6", size = 4436160, upload_time = "2025-05-13T16:08:04.278Z" }, + { url = "https://files.pythonhosted.org/packages/37/78/af5af2a1b296eeca54ea7592cd19284739a844974c9747e516707e7b3b39/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:52e239cd66c4158e412318fbe028cd94b0ef21b0707f56dcb4bdc250ee58fd40", size = 3753518, upload_time = "2025-05-13T16:08:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ac/8a3ed39ea069402e9e6e6a2f79d81a71879708b31cc3454283314994b1ae/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:08bf9d5eabba160dd4f6ad247cf12f229cc19d2458511cab2eb9647f42fa6795", size = 3313598, upload_time = "2025-05-13T16:08:09.999Z" }, + { url = "https://files.pythonhosted.org/packages/da/43/26549af068347c808fbfe5f07d2fa8cef747cfff7c695136172991d2378b/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1b2cf018168cad87580e67bdde38ff5e51511112f1ce6ce9a8336871f465c19a", size = 3407289, upload_time = "2025-05-13T16:08:12.66Z" }, + { url = "https://files.pythonhosted.org/packages/67/55/ea8d227c77df8e8aec880ded398316735add8fda5eb4ff5cc96fac11e964/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:14f64d1ac6942ff089fc7e926440f7a5ced062e2ed0949d7d2d680dc5c00e2d4", size = 3472493, upload_time = "2025-05-13T16:08:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/6ff2a5bc53c3cd653d281666728e29121149179c73fddefb1e437024c192/psycopg_binary-3.2.9-cp312-cp312-win_amd64.whl", hash = "sha256:7a838852e5afb6b4126f93eb409516a8c02a49b788f4df8b6469a40c2157fa21", size = 2927400, upload_time = "2025-05-13T16:08:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/28/0b/f61ff4e9f23396aca674ed4d5c9a5b7323738021d5d72d36d8b865b3deaf/psycopg_binary-3.2.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:98bbe35b5ad24a782c7bf267596638d78aa0e87abc7837bdac5b2a2ab954179e", size = 4017127, upload_time = "2025-05-13T16:08:21.391Z" }, + { url = "https://files.pythonhosted.org/packages/bc/00/7e181fb1179fbfc24493738b61efd0453d4b70a0c4b12728e2b82db355fd/psycopg_binary-3.2.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:72691a1615ebb42da8b636c5ca9f2b71f266be9e172f66209a361c175b7842c5", size = 4080322, upload_time = "2025-05-13T16:08:24.049Z" }, + { url = "https://files.pythonhosted.org/packages/58/fd/94fc267c1d1392c4211e54ccb943be96ea4032e761573cf1047951887494/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ab464bfba8c401f5536d5aa95f0ca1dd8257b5202eede04019b4415f491351", size = 4655097, upload_time = "2025-05-13T16:08:27.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/17/31b3acf43de0b2ba83eac5878ff0dea5a608ca2a5c5dd48067999503a9de/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8aeefebe752f46e3c4b769e53f1d4ad71208fe1150975ef7662c22cca80fab", size = 4482114, upload_time = "2025-05-13T16:08:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/78/b4d75e5fd5a85e17f2beb977abbba3389d11a4536b116205846b0e1cf744/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7e4e4dd177a8665c9ce86bc9caae2ab3aa9360b7ce7ec01827ea1baea9ff748", size = 4737693, upload_time = "2025-05-13T16:08:34.625Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/7325a8550e3388b00b5e54f4ced5e7346b531eb4573bf054c3dbbfdc14fe/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fc2915949e5c1ea27a851f7a472a7da7d0a40d679f0a31e42f1022f3c562e87", size = 4437423, upload_time = "2025-05-13T16:08:37.444Z" }, + { url = "https://files.pythonhosted.org/packages/1a/db/cef77d08e59910d483df4ee6da8af51c03bb597f500f1fe818f0f3b925d3/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1fa38a4687b14f517f049477178093c39c2a10fdcced21116f47c017516498f", size = 3758667, upload_time = "2025-05-13T16:08:40.116Z" }, + { url = "https://files.pythonhosted.org/packages/95/3e/252fcbffb47189aa84d723b54682e1bb6d05c8875fa50ce1ada914ae6e28/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5be8292d07a3ab828dc95b5ee6b69ca0a5b2e579a577b39671f4f5b47116dfd2", size = 3320576, upload_time = "2025-05-13T16:08:43.243Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cd/9b5583936515d085a1bec32b45289ceb53b80d9ce1cea0fef4c782dc41a7/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:778588ca9897b6c6bab39b0d3034efff4c5438f5e3bd52fda3914175498202f9", size = 3411439, upload_time = "2025-05-13T16:08:47.321Z" }, + { url = "https://files.pythonhosted.org/packages/45/6b/6f1164ea1634c87956cdb6db759e0b8c5827f989ee3cdff0f5c70e8331f2/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f0d5b3af045a187aedbd7ed5fc513bd933a97aaff78e61c3745b330792c4345b", size = 3477477, upload_time = "2025-05-13T16:08:51.166Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/bf54cfec79377929da600c16114f0da77a5f1670f45e0c3af9fcd36879bc/psycopg_binary-3.2.9-cp313-cp313-win_amd64.whl", hash = "sha256:2290bc146a1b6a9730350f695e8b670e1d1feb8446597bed0bbe7c3c30e0abcb", size = 2928009, upload_time = "2025-05-13T16:08:53.67Z" }, +] + +[[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 = "pytest" +version = "8.4.1" +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/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload_time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload_time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "pytest-cov" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload_time = "2025-06-12T10:47:47.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload_time = "2025-06-12T10:47:45.932Z" }, +] + +[[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-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload_time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload_time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload_time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload_time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload_time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload_time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload_time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload_time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload_time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload_time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload_time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload_time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload_time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload_time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload_time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload_time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload_time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload_time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload_time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload_time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload_time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload_time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload_time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload_time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload_time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload_time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload_time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload_time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload_time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload_time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload_time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload_time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload_time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload_time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload_time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload_time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload_time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload_time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload_time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload_time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload_time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload_time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload_time = "2024-08-06T20:33:04.33Z" }, +] + +[[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 = "ruff" +version = "0.12.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload_time = "2025-08-21T18:23:22.595Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload_time = "2025-08-21T18:22:26.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload_time = "2025-08-21T18:22:30.925Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload_time = "2025-08-21T18:22:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload_time = "2025-08-21T18:22:36.764Z" }, + { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload_time = "2025-08-21T18:22:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload_time = "2025-08-21T18:22:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload_time = "2025-08-21T18:22:45.612Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload_time = "2025-08-21T18:22:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload_time = "2025-08-21T18:22:51.567Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload_time = "2025-08-21T18:22:54.609Z" }, + { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload_time = "2025-08-21T18:22:57.413Z" }, + { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload_time = "2025-08-21T18:23:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload_time = "2025-08-21T18:23:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload_time = "2025-08-21T18:23:06.935Z" }, + { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload_time = "2025-08-21T18:23:10.225Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload_time = "2025-08-21T18:23:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload_time = "2025-08-21T18:23:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload_time = "2025-08-21T18:23:20.137Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/05/d52bf1e65044b4e5e27d4e63e8d1579dbdec54fce685908ae09bc3720030/s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf", size = 150589, upload_time = "2025-07-18T19:22:42.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/4f/d073e09df851cfa251ef7840007d04db3293a0482ce607d2b993926089be/s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724", size = 85308, upload_time = "2025-07-18T19:22:40.947Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload_time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload_time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload_time = "2024-12-10T12:05:30.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload_time = "2024-12-10T12:05:27.824Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload_time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload_time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload_time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload_time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload_time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload_time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload_time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload_time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload_time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload_time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload_time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload_time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload_time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload_time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload_time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload_time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload_time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload_time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload_time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload_time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload_time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload_time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload_time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload_time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload_time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload_time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload_time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload_time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload_time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload_time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload_time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload_time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "ty" +version = "0.0.1a19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/04/281c1a3c9c53dae5826b9d01a3412de653e3caf1ca50ce1265da66e06d73/ty-0.0.1a19.tar.gz", hash = "sha256:894f6a13a43989c8ef891ae079b3b60a0c0eae00244abbfbbe498a3840a235ac", size = 4098412, upload_time = "2025-08-19T13:29:58.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/65/a61cfcc7248b0257a3110bf98d3d910a4729c1063abdbfdcd1cad9012323/ty-0.0.1a19-py3-none-linux_armv6l.whl", hash = "sha256:e0e7762f040f4bab1b37c57cb1b43cc3bc5afb703fa5d916dfcafa2ef885190e", size = 8143744, upload_time = "2025-08-19T13:29:13.88Z" }, + { url = "https://files.pythonhosted.org/packages/02/d9/232afef97d9afa2274d23a4c49a3ad690282ca9696e1b6bbb6e4e9a1b072/ty-0.0.1a19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cd0a67ac875f49f34d9a0b42dcabf4724194558a5dd36867209d5695c67768f7", size = 8305799, upload_time = "2025-08-19T13:29:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/20/14/099d268da7a9cccc6ba38dfc124f6742a1d669bc91f2c61a3465672b4f71/ty-0.0.1a19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ff8b1c0b85137333c39eccd96c42603af8ba7234d6e2ed0877f66a4a26750dd4", size = 7901431, upload_time = "2025-08-19T13:29:21.635Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cd/3f1ca6e1d7f77cc4d08910a3fc4826313c031c0aae72286ae859e737670c/ty-0.0.1a19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fef34a29f4b97d78aa30e60adbbb12137cf52b8b2b0f1a408dd0feb0466908a", size = 8051501, upload_time = "2025-08-19T13:29:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/47/72/ddbec39f48ce3f5f6a3fa1f905c8fff2873e59d2030f738814032bd783e3/ty-0.0.1a19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b0f219cb43c0c50fc1091f8ebd5548d3ef31ee57866517b9521d5174978af9fd", size = 7981234, upload_time = "2025-08-19T13:29:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0f/58e76b8d4634df066c790d362e8e73b25852279cd6f817f099b42a555a66/ty-0.0.1a19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22abb6c1f14c65c1a2fafd38e25dd3c87994b3ab88cb0b323235b51dbad082d9", size = 8916394, upload_time = "2025-08-19T13:29:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/70/30/01bfd93ccde11540b503e2539e55f6a1fc6e12433a229191e248946eb753/ty-0.0.1a19-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5b49225c349a3866e38dd297cb023a92d084aec0e895ed30ca124704bff600e6", size = 9412024, upload_time = "2025-08-19T13:29:30.942Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a2/2216d752f5f22c5c0995f9b13f18337301220f2a7d952c972b33e6a63583/ty-0.0.1a19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88f41728b3b07402e0861e3c34412ca963268e55f6ab1690208f25d37cb9d63c", size = 9032657, upload_time = "2025-08-19T13:29:33.933Z" }, + { url = "https://files.pythonhosted.org/packages/24/c7/e6650b0569be1b69a03869503d07420c9fb3e90c9109b09726c44366ce63/ty-0.0.1a19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33814a1197ec3e930fcfba6fb80969fe7353957087b42b88059f27a173f7510b", size = 8812775, upload_time = "2025-08-19T13:29:36.505Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/b8a20e06b97fe8203059d56d8f91cec4f9633e7ba65f413d80f16aa0be04/ty-0.0.1a19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71b7f2b674a287258f628acafeecd87691b169522945ff6192cd8a69af15857", size = 8631417, upload_time = "2025-08-19T13:29:38.837Z" }, + { url = "https://files.pythonhosted.org/packages/be/99/821ca1581dcf3d58ffb7bbe1cde7e1644dbdf53db34603a16a459a0b302c/ty-0.0.1a19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a7f8ef9ac4c38e8651c18c7380649c5a3fa9adb1a6012c721c11f4bbdc0ce24", size = 7928900, upload_time = "2025-08-19T13:29:41.08Z" }, + { url = "https://files.pythonhosted.org/packages/08/cb/59f74a0522e57565fef99e2287b2bc803ee47ff7dac250af26960636939f/ty-0.0.1a19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:60f40e72f0fbf4e54aa83d9a6cb1959f551f83de73af96abbb94711c1546bd60", size = 8003310, upload_time = "2025-08-19T13:29:43.165Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b3/1209b9acb5af00a2755114042e48fb0f71decc20d9d77a987bf5b3d1a102/ty-0.0.1a19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:64971e4d3e3f83dc79deb606cc438255146cab1ab74f783f7507f49f9346d89d", size = 8496463, upload_time = "2025-08-19T13:29:46.136Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d6/a4b6ba552d347a08196d83a4d60cb23460404a053dd3596e23a922bce544/ty-0.0.1a19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9aadbff487e2e1486e83543b4f4c2165557f17432369f419be9ba48dc47625ca", size = 8700633, upload_time = "2025-08-19T13:29:49.351Z" }, + { url = "https://files.pythonhosted.org/packages/96/c5/258f318d68b95685c8d98fb654a38882c9d01ce5d9426bed06124f690f04/ty-0.0.1a19-py3-none-win32.whl", hash = "sha256:00b75b446357ee22bcdeb837cb019dc3bc1dc5e5013ff0f46a22dfe6ce498fe2", size = 7811441, upload_time = "2025-08-19T13:29:52.077Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/039227eee3c0c0cddc25f45031eea0f7f10440713f12d333f2f29cf8e934/ty-0.0.1a19-py3-none-win_amd64.whl", hash = "sha256:aaef76b2f44f6379c47adfe58286f0c56041cb2e374fd8462ae8368788634469", size = 8441186, upload_time = "2025-08-19T13:29:54.53Z" }, + { url = "https://files.pythonhosted.org/packages/74/5f/bceb29009670ae6f759340f9cb434121bc5ed84ad0f07bdc6179eaaa3204/ty-0.0.1a19-py3-none-win_arm64.whl", hash = "sha256:893755bb35f30653deb28865707e3b16907375c830546def2741f6ff9a764710", size = 8000810, upload_time = "2025-08-19T13:29:56.796Z" }, +] + +[[package]] +name = "types-awscrt" +version = "0.27.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/ce/5d84526a39f44c420ce61b16654193f8437d74b54f21597ea2ac65d89954/types_awscrt-0.27.6.tar.gz", hash = "sha256:9d3f1865a93b8b2c32f137514ac88cb048b5bc438739945ba19d972698995bfb", size = 16937, upload_time = "2025-08-13T01:54:54.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/af/e3d20e3e81d235b3964846adf46a334645a8a9b25a0d3d472743eb079552/types_awscrt-0.27.6-py3-none-any.whl", hash = "sha256:18aced46da00a57f02eb97637a32e5894dc5aa3dc6a905ba3e5ed85b9f3c526b", size = 39626, upload_time = "2025-08-13T01:54:53.454Z" }, +] + +[[package]] +name = "types-s3transfer" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/c1/45038f259d6741c252801044e184fec4dbaeff939a58f6160d7c32bf4975/types_s3transfer-0.13.0.tar.gz", hash = "sha256:203dadcb9865c2f68fb44bc0440e1dc05b79197ba4a641c0976c26c9af75ef52", size = 14175, upload_time = "2025-05-28T02:16:07.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/5d/6bbe4bf6a79fb727945291aef88b5ecbdba857a603f1bbcf1a6be0d3f442/types_s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:79c8375cbf48a64bff7654c02df1ec4b20d74f8c5672fc13e382f593ca5565b3", size = 19588, upload_time = "2025-05-28T02:16:06.709Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload_time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload_time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload_time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload_time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload_time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload_time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.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/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload_time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload_time = "2025-06-28T16:15:44.816Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.34.0" +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/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload_time = "2025-08-13T14:24:07.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload_time = "2025-08-13T14:24:05.111Z" }, +] + +[[package]] +name = "whitenoise" +version = "6.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/cf/c15c2f21aee6b22a9f6fc9be3f7e477e2442ec22848273db7f4eb73d6162/whitenoise-6.9.0.tar.gz", hash = "sha256:8c4a7c9d384694990c26f3047e118c691557481d624f069b7f7752a2f735d609", size = 25920, upload_time = "2025-02-06T22:16:34.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b2/2ce9263149fbde9701d352bda24ea1362c154e196d2fda2201f18fc585d7/whitenoise-6.9.0-py3-none-any.whl", hash = "sha256:c8a489049b7ee9889617bb4c274a153f3d979e8f51d2efd0f5b403caf41c57df", size = 20161, upload_time = "2025-02-06T22:16:32.589Z" }, +] diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..8cb4f01 --- /dev/null +++ b/fly.toml @@ -0,0 +1,29 @@ +# fly.toml app configuration file generated for forms-mvp-staging on 2025-08-20T14:42:16-07:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'forms-mvp-staging' +primary_region = 'lax' + +[build] + +[env] + DJANGO_SETTINGS_MODULE = "efile.settings_staging" + +[deploy] + # Run database migrations before each release + release_command = "uv run python manage.py migrate --noinput" + +[http_service] + internal_port = 8000 + force_https = true + auto_stop_machines = 'stop' + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[vm]] + memory = '1gb' + cpu_kind = 'shared' + cpus = 1 diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 6da37db..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,88 +0,0 @@ -[project] -name = "form-submission-mvp" -version = "0.1.0" -description = "Minimal Django app for form submission and review" -requires-python = ">=3.10" -dependencies = [ - "Django==5.2.5", - "requests>=2.31,<3", -] - -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[tool.uv] -# uv will create and manage a local .venv by default when using `uv sync` -# You can commit `uv.lock` to lock dependencies. - -[dependency-groups] -# Allow `uv sync --group dev` -dev = [ - "ruff>=0.5.0,<1", - "ty", - "pytest>=8.0", - "pytest-django>=4.8", - "pytest-cov>=5.0", - "pytest-xdist>=3.6", - "pre-commit>=3.7", -] - -[project.optional-dependencies] -# Development-only tools -dev = [ - "ruff>=0.5.0,<1", - "ty", - "pytest>=8.0", - "pytest-django>=4.8", - "pytest-cov>=5.0", - "pytest-xdist>=3.6", - "pre-commit>=3.7", -] - -[tool.ruff] -# Align with project Python requirement -target-version = "py310" -line-length = 120 -src = ["efile_app"] -extend-exclude = [ - "**/migrations/*", -] - -[tool.ruff.format] -quote-style = "double" -indent-style = "space" -skip-magic-trailing-comma = false - -[tool.ruff.lint] -select = [ - # pycodestyle / pyflakes - "E", "F", - # isort imports - "I", - # pyupgrade - "UP", - # bugbear - "B", -] -ignore = [] - -[tool.ruff.lint.isort] -known-first-party = ["efile_app"] - -# Ty (Rust-based Python type checker) -[tool.ty.src] -include = ["efile_app"] -exclude = ["**/migrations/**"] - -# pytest configuration (pytest and pytest-django) -[tool.pytest.ini_options] -DJANGO_SETTINGS_MODULE = "efile.settings" -pythonpath = [ - "efile_app", -] -testpaths = [ - "efile_app", -] -addopts = "-ra --strict-markers" - diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 31d4c7d..0000000 --- a/uv.lock +++ /dev/null @@ -1,648 +0,0 @@ -version = 1 -revision = 2 -requires-python = ">=3.10" - -[[package]] -name = "asgiref" -version = "3.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload_time = "2025-07-08T09:07:43.344Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload_time = "2025-07-08T09:07:41.548Z" }, -] - -[[package]] -name = "certifi" -version = "2025.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload_time = "2025-08-03T03:07:47.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload_time = "2025-08-03T03:07:45.777Z" }, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload_time = "2023-08-12T20:38:17.776Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload_time = "2023-08-12T20:38:16.269Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload_time = "2025-08-09T07:57:28.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload_time = "2025-08-09T07:55:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload_time = "2025-08-09T07:55:38.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload_time = "2025-08-09T07:55:40.072Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload_time = "2025-08-09T07:55:41.706Z" }, - { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload_time = "2025-08-09T07:55:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload_time = "2025-08-09T07:55:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload_time = "2025-08-09T07:55:46.346Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload_time = "2025-08-09T07:55:47.539Z" }, - { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload_time = "2025-08-09T07:55:48.744Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload_time = "2025-08-09T07:55:50.305Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload_time = "2025-08-09T07:55:51.461Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload_time = "2025-08-09T07:55:53.12Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload_time = "2025-08-09T07:55:54.712Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload_time = "2025-08-09T07:55:56.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload_time = "2025-08-09T07:55:57.582Z" }, - { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload_time = "2025-08-09T07:55:59.147Z" }, - { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload_time = "2025-08-09T07:56:00.364Z" }, - { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload_time = "2025-08-09T07:56:01.678Z" }, - { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload_time = "2025-08-09T07:56:02.87Z" }, - { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload_time = "2025-08-09T07:56:04.089Z" }, - { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload_time = "2025-08-09T07:56:05.658Z" }, - { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload_time = "2025-08-09T07:56:07.176Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload_time = "2025-08-09T07:56:08.475Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload_time = "2025-08-09T07:56:09.708Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload_time = "2025-08-09T07:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload_time = "2025-08-09T07:56:13.014Z" }, - { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload_time = "2025-08-09T07:56:14.428Z" }, - { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload_time = "2025-08-09T07:56:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload_time = "2025-08-09T07:56:17.314Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload_time = "2025-08-09T07:56:18.641Z" }, - { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload_time = "2025-08-09T07:56:20.289Z" }, - { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload_time = "2025-08-09T07:56:21.551Z" }, - { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload_time = "2025-08-09T07:56:23.115Z" }, - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload_time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload_time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload_time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload_time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload_time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload_time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload_time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload_time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload_time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload_time = "2025-08-09T07:56:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload_time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload_time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload_time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload_time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload_time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload_time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload_time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload_time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload_time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload_time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload_time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload_time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload_time = "2025-08-09T07:57:26.864Z" }, -] - -[[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.10.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/2c/253cc41cd0f40b84c1c34c5363e0407d73d4a1cae005fed6db3b823175bd/coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619", size = 822936, upload_time = "2025-08-10T21:27:39.968Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/44/e14576c34b37764c821866909788ff7463228907ab82bae188dab2b421f1/coverage-7.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53808194afdf948c462215e9403cca27a81cf150d2f9b386aee4dab614ae2ffe", size = 215964, upload_time = "2025-08-10T21:25:22.828Z" }, - { url = "https://files.pythonhosted.org/packages/e6/15/f4f92d9b83100903efe06c9396ee8d8bdba133399d37c186fc5b16d03a87/coverage-7.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f4d1b837d1abf72187a61645dbf799e0d7705aa9232924946e1f57eb09a3bf00", size = 216361, upload_time = "2025-08-10T21:25:25.603Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3a/c92e8cd5e89acc41cfc026dfb7acedf89661ce2ea1ee0ee13aacb6b2c20c/coverage-7.10.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2a90dd4505d3cc68b847ab10c5ee81822a968b5191664e8a0801778fa60459fa", size = 243115, upload_time = "2025-08-10T21:25:27.09Z" }, - { url = "https://files.pythonhosted.org/packages/23/53/c1d8c2778823b1d95ca81701bb8f42c87dc341a2f170acdf716567523490/coverage-7.10.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d52989685ff5bf909c430e6d7f6550937bc6d6f3e6ecb303c97a86100efd4596", size = 244927, upload_time = "2025-08-10T21:25:28.77Z" }, - { url = "https://files.pythonhosted.org/packages/79/41/1e115fd809031f432b4ff8e2ca19999fb6196ab95c35ae7ad5e07c001130/coverage-7.10.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdb558a1d97345bde3a9f4d3e8d11c9e5611f748646e9bb61d7d612a796671b5", size = 246784, upload_time = "2025-08-10T21:25:30.195Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b2/0eba9bdf8f1b327ae2713c74d4b7aa85451bb70622ab4e7b8c000936677c/coverage-7.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c9e6331a8f09cb1fc8bda032752af03c366870b48cce908875ba2620d20d0ad4", size = 244828, upload_time = "2025-08-10T21:25:31.785Z" }, - { url = "https://files.pythonhosted.org/packages/1f/cc/74c56b6bf71f2a53b9aa3df8bc27163994e0861c065b4fe3a8ac290bed35/coverage-7.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:992f48bf35b720e174e7fae916d943599f1a66501a2710d06c5f8104e0756ee1", size = 242844, upload_time = "2025-08-10T21:25:33.37Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/ac183fbe19ac5596c223cb47af5737f4437e7566100b7e46cc29b66695a5/coverage-7.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c5595fc4ad6a39312c786ec3326d7322d0cf10e3ac6a6df70809910026d67cfb", size = 243721, upload_time = "2025-08-10T21:25:34.939Z" }, - { url = "https://files.pythonhosted.org/packages/57/96/cb90da3b5a885af48f531905234a1e7376acfc1334242183d23154a1c285/coverage-7.10.3-cp310-cp310-win32.whl", hash = "sha256:9e92fa1f2bd5a57df9d00cf9ce1eb4ef6fccca4ceabec1c984837de55329db34", size = 218481, upload_time = "2025-08-10T21:25:36.935Z" }, - { url = "https://files.pythonhosted.org/packages/15/67/1ba4c7d75745c4819c54a85766e0a88cc2bff79e1760c8a2debc34106dc2/coverage-7.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b96524d6e4a3ce6a75c56bb15dbd08023b0ae2289c254e15b9fbdddf0c577416", size = 219382, upload_time = "2025-08-10T21:25:38.267Z" }, - { url = "https://files.pythonhosted.org/packages/87/04/810e506d7a19889c244d35199cbf3239a2f952b55580aa42ca4287409424/coverage-7.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2ff2e2afdf0d51b9b8301e542d9c21a8d084fd23d4c8ea2b3a1b3c96f5f7397", size = 216075, upload_time = "2025-08-10T21:25:39.891Z" }, - { url = "https://files.pythonhosted.org/packages/2e/50/6b3fbab034717b4af3060bdaea6b13dfdc6b1fad44b5082e2a95cd378a9a/coverage-7.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ecc5d1b9a8c570f6c9b808fa9a2b16836b3dd5414a6d467ae942208b095f85", size = 216476, upload_time = "2025-08-10T21:25:41.137Z" }, - { url = "https://files.pythonhosted.org/packages/c7/96/4368c624c1ed92659812b63afc76c492be7867ac8e64b7190b88bb26d43c/coverage-7.10.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1af4461b25fe92889590d438905e1fc79a95680ec2a1ff69a591bb3fdb6c7157", size = 246865, upload_time = "2025-08-10T21:25:42.408Z" }, - { url = "https://files.pythonhosted.org/packages/34/12/5608f76070939395c17053bf16e81fd6c06cf362a537ea9d07e281013a27/coverage-7.10.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3966bc9a76b09a40dc6063c8b10375e827ea5dfcaffae402dd65953bef4cba54", size = 248800, upload_time = "2025-08-10T21:25:44.098Z" }, - { url = "https://files.pythonhosted.org/packages/ce/52/7cc90c448a0ad724283cbcdfd66b8d23a598861a6a22ac2b7b8696491798/coverage-7.10.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:205a95b87ef4eb303b7bc5118b47b6b6604a644bcbdb33c336a41cfc0a08c06a", size = 250904, upload_time = "2025-08-10T21:25:45.384Z" }, - { url = "https://files.pythonhosted.org/packages/e6/70/9967b847063c1c393b4f4d6daab1131558ebb6b51f01e7df7150aa99f11d/coverage-7.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b3801b79fb2ad61e3c7e2554bab754fc5f105626056980a2b9cf3aef4f13f84", size = 248597, upload_time = "2025-08-10T21:25:47.059Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fe/263307ce6878b9ed4865af42e784b42bb82d066bcf10f68defa42931c2c7/coverage-7.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0dc69c60224cda33d384572da945759756e3f06b9cdac27f302f53961e63160", size = 246647, upload_time = "2025-08-10T21:25:48.334Z" }, - { url = "https://files.pythonhosted.org/packages/8e/27/d27af83ad162eba62c4eb7844a1de6cf7d9f6b185df50b0a3514a6f80ddd/coverage-7.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a83d4f134bab2c7ff758e6bb1541dd72b54ba295ced6a63d93efc2e20cb9b124", size = 247290, upload_time = "2025-08-10T21:25:49.945Z" }, - { url = "https://files.pythonhosted.org/packages/28/83/904ff27e15467a5622dbe9ad2ed5831b4a616a62570ec5924d06477dff5a/coverage-7.10.3-cp311-cp311-win32.whl", hash = "sha256:54e409dd64e5302b2a8fdf44ec1c26f47abd1f45a2dcf67bd161873ee05a59b8", size = 218521, upload_time = "2025-08-10T21:25:51.208Z" }, - { url = "https://files.pythonhosted.org/packages/b8/29/bc717b8902faaccf0ca486185f0dcab4778561a529dde51cb157acaafa16/coverage-7.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:30c601610a9b23807c5e9e2e442054b795953ab85d525c3de1b1b27cebeb2117", size = 219412, upload_time = "2025-08-10T21:25:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7a/5a1a7028c11bb589268c656c6b3f2bbf06e0aced31bbdf7a4e94e8442cc0/coverage-7.10.3-cp311-cp311-win_arm64.whl", hash = "sha256:dabe662312a97958e932dee056f2659051d822552c0b866823e8ba1c2fe64770", size = 218091, upload_time = "2025-08-10T21:25:54.102Z" }, - { url = "https://files.pythonhosted.org/packages/b8/62/13c0b66e966c43d7aa64dadc8cd2afa1f5a2bf9bb863bdabc21fb94e8b63/coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42", size = 216262, upload_time = "2025-08-10T21:25:55.367Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f0/59fdf79be7ac2f0206fc739032f482cfd3f66b18f5248108ff192741beae/coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294", size = 216496, upload_time = "2025-08-10T21:25:56.759Z" }, - { url = "https://files.pythonhosted.org/packages/34/b1/bc83788ba31bde6a0c02eb96bbc14b2d1eb083ee073beda18753fa2c4c66/coverage-7.10.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dd4564207b160d0d45c36a10bc0a3d12563028e8b48cd6459ea322302a156d7", size = 247989, upload_time = "2025-08-10T21:25:58.067Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/f8bdf88357956c844bd872e87cb16748a37234f7f48c721dc7e981145eb7/coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437", size = 250738, upload_time = "2025-08-10T21:25:59.406Z" }, - { url = "https://files.pythonhosted.org/packages/ae/df/6396301d332b71e42bbe624670af9376f63f73a455cc24723656afa95796/coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587", size = 251868, upload_time = "2025-08-10T21:26:00.65Z" }, - { url = "https://files.pythonhosted.org/packages/91/21/d760b2df6139b6ef62c9cc03afb9bcdf7d6e36ed4d078baacffa618b4c1c/coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea", size = 249790, upload_time = "2025-08-10T21:26:02.009Z" }, - { url = "https://files.pythonhosted.org/packages/69/91/5dcaa134568202397fa4023d7066d4318dc852b53b428052cd914faa05e1/coverage-7.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a374d4e923814e8b72b205ef6b3d3a647bb50e66f3558582eda074c976923613", size = 247907, upload_time = "2025-08-10T21:26:03.757Z" }, - { url = "https://files.pythonhosted.org/packages/38/ed/70c0e871cdfef75f27faceada461206c1cc2510c151e1ef8d60a6fedda39/coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb", size = 249344, upload_time = "2025-08-10T21:26:05.11Z" }, - { url = "https://files.pythonhosted.org/packages/5f/55/c8a273ed503cedc07f8a00dcd843daf28e849f0972e4c6be4c027f418ad6/coverage-7.10.3-cp312-cp312-win32.whl", hash = "sha256:187ecdcac21f9636d570e419773df7bd2fda2e7fa040f812e7f95d0bddf5f79a", size = 218693, upload_time = "2025-08-10T21:26:06.534Z" }, - { url = "https://files.pythonhosted.org/packages/94/58/dd3cfb2473b85be0b6eb8c5b6d80b6fc3f8f23611e69ef745cef8cf8bad5/coverage-7.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:4a50ad2524ee7e4c2a95e60d2b0b83283bdfc745fe82359d567e4f15d3823eb5", size = 219501, upload_time = "2025-08-10T21:26:08.195Z" }, - { url = "https://files.pythonhosted.org/packages/56/af/7cbcbf23d46de6f24246e3f76b30df099d05636b30c53c158a196f7da3ad/coverage-7.10.3-cp312-cp312-win_arm64.whl", hash = "sha256:c112f04e075d3495fa3ed2200f71317da99608cbb2e9345bdb6de8819fc30571", size = 218135, upload_time = "2025-08-10T21:26:09.584Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/239e4de9cc149c80e9cc359fab60592365b8c4cbfcad58b8a939d18c6898/coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a", size = 216298, upload_time = "2025-08-10T21:26:10.973Z" }, - { url = "https://files.pythonhosted.org/packages/56/da/28717da68f8ba68f14b9f558aaa8f3e39ada8b9a1ae4f4977c8f98b286d5/coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a", size = 216546, upload_time = "2025-08-10T21:26:12.616Z" }, - { url = "https://files.pythonhosted.org/packages/de/bb/e1ade16b9e3f2d6c323faeb6bee8e6c23f3a72760a5d9af102ef56a656cb/coverage-7.10.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:488e9b50dc5d2aa9521053cfa706209e5acf5289e81edc28291a24f4e4488f46", size = 247538, upload_time = "2025-08-10T21:26:14.455Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2f/6ae1db51dc34db499bfe340e89f79a63bd115fc32513a7bacdf17d33cd86/coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4", size = 250141, upload_time = "2025-08-10T21:26:15.787Z" }, - { url = "https://files.pythonhosted.org/packages/4f/ed/33efd8819895b10c66348bf26f011dd621e804866c996ea6893d682218df/coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a", size = 251415, upload_time = "2025-08-10T21:26:17.535Z" }, - { url = "https://files.pythonhosted.org/packages/26/04/cb83826f313d07dc743359c9914d9bc460e0798da9a0e38b4f4fabc207ed/coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3", size = 249575, upload_time = "2025-08-10T21:26:18.921Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fd/ae963c7a8e9581c20fa4355ab8940ca272554d8102e872dbb932a644e410/coverage-7.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fe72cbdd12d9e0f4aca873fa6d755e103888a7f9085e4a62d282d9d5b9f7928c", size = 247466, upload_time = "2025-08-10T21:26:20.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/e8/b68d1487c6af370b8d5ef223c6d7e250d952c3acfbfcdbf1a773aa0da9d2/coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21", size = 249084, upload_time = "2025-08-10T21:26:21.638Z" }, - { url = "https://files.pythonhosted.org/packages/66/4d/a0bcb561645c2c1e21758d8200443669d6560d2a2fb03955291110212ec4/coverage-7.10.3-cp313-cp313-win32.whl", hash = "sha256:24d0c13de473b04920ddd6e5da3c08831b1170b8f3b17461d7429b61cad59ae0", size = 218735, upload_time = "2025-08-10T21:26:23.009Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c3/78b4adddbc0feb3b223f62761e5f9b4c5a758037aaf76e0a5845e9e35e48/coverage-7.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:3564aae76bce4b96e2345cf53b4c87e938c4985424a9be6a66ee902626edec4c", size = 219531, upload_time = "2025-08-10T21:26:24.474Z" }, - { url = "https://files.pythonhosted.org/packages/70/1b/1229c0b2a527fa5390db58d164aa896d513a1fbb85a1b6b6676846f00552/coverage-7.10.3-cp313-cp313-win_arm64.whl", hash = "sha256:f35580f19f297455f44afcd773c9c7a058e52eb6eb170aa31222e635f2e38b87", size = 218162, upload_time = "2025-08-10T21:26:25.847Z" }, - { url = "https://files.pythonhosted.org/packages/fc/26/1c1f450e15a3bf3eaecf053ff64538a2612a23f05b21d79ce03be9ff5903/coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84", size = 217003, upload_time = "2025-08-10T21:26:27.231Z" }, - { url = "https://files.pythonhosted.org/packages/29/96/4b40036181d8c2948454b458750960956a3c4785f26a3c29418bbbee1666/coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e", size = 217238, upload_time = "2025-08-10T21:26:28.83Z" }, - { url = "https://files.pythonhosted.org/packages/62/23/8dfc52e95da20957293fb94d97397a100e63095ec1e0ef5c09dd8c6f591a/coverage-7.10.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1ae22b97003c74186e034a93e4f946c75fad8c0ce8d92fbbc168b5e15ee2841f", size = 258561, upload_time = "2025-08-10T21:26:30.475Z" }, - { url = "https://files.pythonhosted.org/packages/59/95/00e7fcbeda3f632232f4c07dde226afe3511a7781a000aa67798feadc535/coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5", size = 260735, upload_time = "2025-08-10T21:26:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4c/f4666cbc4571804ba2a65b078ff0de600b0b577dc245389e0bc9b69ae7ca/coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8", size = 262960, upload_time = "2025-08-10T21:26:33.701Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a5/8a9e8a7b12a290ed98b60f73d1d3e5e9ced75a4c94a0d1a671ce3ddfff2a/coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1", size = 260515, upload_time = "2025-08-10T21:26:35.16Z" }, - { url = "https://files.pythonhosted.org/packages/86/11/bb59f7f33b2cac0c5b17db0d9d0abba9c90d9eda51a6e727b43bd5fce4ae/coverage-7.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:669fe0d4e69c575c52148511029b722ba8d26e8a3129840c2ce0522e1452b256", size = 258278, upload_time = "2025-08-10T21:26:36.539Z" }, - { url = "https://files.pythonhosted.org/packages/cc/22/3646f8903743c07b3e53fded0700fed06c580a980482f04bf9536657ac17/coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b", size = 259408, upload_time = "2025-08-10T21:26:37.954Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5c/6375e9d905da22ddea41cd85c30994b8b6f6c02e44e4c5744b76d16b026f/coverage-7.10.3-cp313-cp313t-win32.whl", hash = "sha256:cc0ee4b2ccd42cab7ee6be46d8a67d230cb33a0a7cd47a58b587a7063b6c6b0e", size = 219396, upload_time = "2025-08-10T21:26:39.426Z" }, - { url = "https://files.pythonhosted.org/packages/33/3b/7da37fd14412b8c8b6e73c3e7458fef6b1b05a37f990a9776f88e7740c89/coverage-7.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:03db599f213341e2960430984e04cf35fb179724e052a3ee627a068653cf4a7c", size = 220458, upload_time = "2025-08-10T21:26:40.905Z" }, - { url = "https://files.pythonhosted.org/packages/28/cc/59a9a70f17edab513c844ee7a5c63cf1057041a84cc725b46a51c6f8301b/coverage-7.10.3-cp313-cp313t-win_arm64.whl", hash = "sha256:46eae7893ba65f53c71284585a262f083ef71594f05ec5c85baf79c402369098", size = 218722, upload_time = "2025-08-10T21:26:42.362Z" }, - { url = "https://files.pythonhosted.org/packages/2d/84/bb773b51a06edbf1231b47dc810a23851f2796e913b335a0fa364773b842/coverage-7.10.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bce8b8180912914032785850d8f3aacb25ec1810f5f54afc4a8b114e7a9b55de", size = 216280, upload_time = "2025-08-10T21:26:44.132Z" }, - { url = "https://files.pythonhosted.org/packages/92/a8/4d8ca9c111d09865f18d56facff64d5fa076a5593c290bd1cfc5dceb8dba/coverage-7.10.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07790b4b37d56608536f7c1079bd1aa511567ac2966d33d5cec9cf520c50a7c8", size = 216557, upload_time = "2025-08-10T21:26:45.598Z" }, - { url = "https://files.pythonhosted.org/packages/fe/b2/eb668bfc5060194bc5e1ccd6f664e8e045881cfee66c42a2aa6e6c5b26e8/coverage-7.10.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e79367ef2cd9166acedcbf136a458dfe9a4a2dd4d1ee95738fb2ee581c56f667", size = 247598, upload_time = "2025-08-10T21:26:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/fd/b0/9faa4ac62c8822219dd83e5d0e73876398af17d7305968aed8d1606d1830/coverage-7.10.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:419d2a0f769f26cb1d05e9ccbc5eab4cb5d70231604d47150867c07822acbdf4", size = 250131, upload_time = "2025-08-10T21:26:48.65Z" }, - { url = "https://files.pythonhosted.org/packages/4e/90/203537e310844d4bf1bdcfab89c1e05c25025c06d8489b9e6f937ad1a9e2/coverage-7.10.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee221cf244757cdc2ac882e3062ab414b8464ad9c884c21e878517ea64b3fa26", size = 251485, upload_time = "2025-08-10T21:26:50.368Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b2/9d894b26bc53c70a1fe503d62240ce6564256d6d35600bdb86b80e516e7d/coverage-7.10.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c2079d8cdd6f7373d628e14b3357f24d1db02c9dc22e6a007418ca7a2be0435a", size = 249488, upload_time = "2025-08-10T21:26:52.045Z" }, - { url = "https://files.pythonhosted.org/packages/b4/28/af167dbac5281ba6c55c933a0ca6675d68347d5aee39cacc14d44150b922/coverage-7.10.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bd8df1f83c0703fa3ca781b02d36f9ec67ad9cb725b18d486405924f5e4270bd", size = 247419, upload_time = "2025-08-10T21:26:53.533Z" }, - { url = "https://files.pythonhosted.org/packages/f4/1c/9a4ddc9f0dcb150d4cd619e1c4bb39bcf694c6129220bdd1e5895d694dda/coverage-7.10.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b4e25e0fa335c8aa26e42a52053f3786a61cc7622b4d54ae2dad994aa754fec", size = 248917, upload_time = "2025-08-10T21:26:55.11Z" }, - { url = "https://files.pythonhosted.org/packages/92/27/c6a60c7cbe10dbcdcd7fc9ee89d531dc04ea4c073800279bb269954c5a9f/coverage-7.10.3-cp314-cp314-win32.whl", hash = "sha256:d7c3d02c2866deb217dce664c71787f4b25420ea3eaf87056f44fb364a3528f5", size = 218999, upload_time = "2025-08-10T21:26:56.637Z" }, - { url = "https://files.pythonhosted.org/packages/36/09/a94c1369964ab31273576615d55e7d14619a1c47a662ed3e2a2fe4dee7d4/coverage-7.10.3-cp314-cp314-win_amd64.whl", hash = "sha256:9c8916d44d9e0fe6cdb2227dc6b0edd8bc6c8ef13438bbbf69af7482d9bb9833", size = 219801, upload_time = "2025-08-10T21:26:58.207Z" }, - { url = "https://files.pythonhosted.org/packages/23/59/f5cd2a80f401c01cf0f3add64a7b791b7d53fd6090a4e3e9ea52691cf3c4/coverage-7.10.3-cp314-cp314-win_arm64.whl", hash = "sha256:1007d6a2b3cf197c57105cc1ba390d9ff7f0bee215ced4dea530181e49c65ab4", size = 218381, upload_time = "2025-08-10T21:26:59.707Z" }, - { url = "https://files.pythonhosted.org/packages/73/3d/89d65baf1ea39e148ee989de6da601469ba93c1d905b17dfb0b83bd39c96/coverage-7.10.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ebc8791d346410d096818788877d675ca55c91db87d60e8f477bd41c6970ffc6", size = 217019, upload_time = "2025-08-10T21:27:01.242Z" }, - { url = "https://files.pythonhosted.org/packages/7d/7d/d9850230cd9c999ce3a1e600f85c2fff61a81c301334d7a1faa1a5ba19c8/coverage-7.10.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f4e4d8e75f6fd3c6940ebeed29e3d9d632e1f18f6fb65d33086d99d4d073241", size = 217237, upload_time = "2025-08-10T21:27:03.442Z" }, - { url = "https://files.pythonhosted.org/packages/36/51/b87002d417202ab27f4a1cd6bd34ee3b78f51b3ddbef51639099661da991/coverage-7.10.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:24581ed69f132b6225a31b0228ae4885731cddc966f8a33fe5987288bdbbbd5e", size = 258735, upload_time = "2025-08-10T21:27:05.124Z" }, - { url = "https://files.pythonhosted.org/packages/1c/02/1f8612bfcb46fc7ca64a353fff1cd4ed932bb6e0b4e0bb88b699c16794b8/coverage-7.10.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec151569ddfccbf71bac8c422dce15e176167385a00cd86e887f9a80035ce8a5", size = 260901, upload_time = "2025-08-10T21:27:06.68Z" }, - { url = "https://files.pythonhosted.org/packages/aa/3a/fe39e624ddcb2373908bd922756384bb70ac1c5009b0d1674eb326a3e428/coverage-7.10.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ae8e7c56290b908ee817200c0b65929b8050bc28530b131fe7c6dfee3e7d86b", size = 263157, upload_time = "2025-08-10T21:27:08.398Z" }, - { url = "https://files.pythonhosted.org/packages/5e/89/496b6d5a10fa0d0691a633bb2b2bcf4f38f0bdfcbde21ad9e32d1af328ed/coverage-7.10.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb742309766d7e48e9eb4dc34bc95a424707bc6140c0e7d9726e794f11b92a0", size = 260597, upload_time = "2025-08-10T21:27:10.237Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a6/8b5bf6a9e8c6aaeb47d5fe9687014148efc05c3588110246d5fdeef9b492/coverage-7.10.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c65e2a5b32fbe1e499f1036efa6eb9cb4ea2bf6f7168d0e7a5852f3024f471b1", size = 258353, upload_time = "2025-08-10T21:27:11.773Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6d/ad131be74f8afd28150a07565dfbdc86592fd61d97e2dc83383d9af219f0/coverage-7.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d48d2cb07d50f12f4f18d2bb75d9d19e3506c26d96fffabf56d22936e5ed8f7c", size = 259504, upload_time = "2025-08-10T21:27:13.254Z" }, - { url = "https://files.pythonhosted.org/packages/ec/30/fc9b5097092758cba3375a8cc4ff61774f8cd733bcfb6c9d21a60077a8d8/coverage-7.10.3-cp314-cp314t-win32.whl", hash = "sha256:dec0d9bc15ee305e09fe2cd1911d3f0371262d3cfdae05d79515d8cb712b4869", size = 219782, upload_time = "2025-08-10T21:27:14.736Z" }, - { url = "https://files.pythonhosted.org/packages/72/9b/27fbf79451b1fac15c4bda6ec6e9deae27cf7c0648c1305aa21a3454f5c4/coverage-7.10.3-cp314-cp314t-win_amd64.whl", hash = "sha256:424ea93a323aa0f7f01174308ea78bde885c3089ec1bef7143a6d93c3e24ef64", size = 220898, upload_time = "2025-08-10T21:27:16.297Z" }, - { url = "https://files.pythonhosted.org/packages/d1/cf/a32bbf92869cbf0b7c8b84325327bfc718ad4b6d2c63374fef3d58e39306/coverage-7.10.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f5983c132a62d93d71c9ef896a0b9bf6e6828d8d2ea32611f58684fba60bba35", size = 218922, upload_time = "2025-08-10T21:27:18.22Z" }, - { url = "https://files.pythonhosted.org/packages/84/19/e67f4ae24e232c7f713337f3f4f7c9c58afd0c02866fb07c7b9255a19ed7/coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1", size = 207921, upload_time = "2025-08-10T21:27:38.254Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - -[[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 = "django" -version = "5.2.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "asgiref" }, - { name = "sqlparse" }, - { name = "tzdata", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/62/9b/779f853c3d2d58b9e08346061ff3e331cdec3fe3f53aae509e256412a593/django-5.2.5.tar.gz", hash = "sha256:0745b25681b129a77aae3d4f6549b62d3913d74407831abaa0d9021a03954bae", size = 10859748, upload_time = "2025-08-06T08:26:29.978Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/6e/98a1d23648e0085bb5825326af17612ecd8fc76be0ce96ea4dc35e17b926/django-5.2.5-py3-none-any.whl", hash = "sha256:2b2ada0ee8a5ff743a40e2b9820d1f8e24c11bac9ae6469cd548f0057ea6ddcd", size = 8302999, upload_time = "2025-08-06T08:26:23.562Z" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload_time = "2025-05-10T17:42:51.123Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload_time = "2025-05-10T17:42:49.33Z" }, -] - -[[package]] -name = "execnet" -version = "2.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload_time = "2024-04-08T09:04:19.245Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload_time = "2024-04-08T09:04:17.414Z" }, -] - -[[package]] -name = "filelock" -version = "3.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload_time = "2025-03-14T07:11:40.47Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload_time = "2025-03-14T07:11:39.145Z" }, -] - -[[package]] -name = "form-submission-mvp" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "django" }, - { name = "requests" }, -] - -[package.optional-dependencies] -dev = [ - { name = "pre-commit" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, - { name = "pytest-xdist" }, - { name = "ruff" }, - { name = "ty" }, -] - -[package.dev-dependencies] -dev = [ - { name = "pre-commit" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, - { name = "pytest-xdist" }, - { name = "ruff" }, - { name = "ty" }, -] - -[package.metadata] -requires-dist = [ - { name = "django", specifier = "==5.2.5" }, - { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.7" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" }, - { name = "pytest-django", marker = "extra == 'dev'", specifier = ">=4.8" }, - { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.6" }, - { name = "requests", specifier = ">=2.31,<3" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5.0,<1" }, - { name = "ty", marker = "extra == 'dev'" }, -] -provides-extras = ["dev"] - -[package.metadata.requires-dev] -dev = [ - { name = "pre-commit", specifier = ">=3.7" }, - { name = "pytest", specifier = ">=8.0" }, - { name = "pytest-cov", specifier = ">=5.0" }, - { name = "pytest-django", specifier = ">=4.8" }, - { name = "pytest-xdist", specifier = ">=3.6" }, - { name = "ruff", specifier = ">=0.5.0,<1" }, - { name = "ty" }, -] - -[[package]] -name = "identify" -version = "2.6.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload_time = "2025-08-09T19:35:00.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload_time = "2025-08-09T19:34:59.1Z" }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload_time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload_time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload_time = "2024-06-04T18:44:11.171Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload_time = "2024-06-04T18:44:08.352Z" }, -] - -[[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 = "platformdirs" -version = "4.3.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload_time = "2025-05-07T22:47:42.121Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload_time = "2025-05-07T22:47:40.376Z" }, -] - -[[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 = "pre-commit" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload_time = "2025-08-09T18:56:14.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload_time = "2025-08-09T18:56:13.192Z" }, -] - -[[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 = "pytest" -version = "8.4.1" -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/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload_time = "2025-06-18T05:48:06.109Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload_time = "2025-06-18T05:48:03.955Z" }, -] - -[[package]] -name = "pytest-cov" -version = "6.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pluggy" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload_time = "2025-06-12T10:47:47.684Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload_time = "2025-06-12T10:47:45.932Z" }, -] - -[[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-xdist" -version = "3.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "execnet" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload_time = "2025-07-01T13:30:59.346Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload_time = "2025-07-01T13:30:56.632Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload_time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload_time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload_time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload_time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload_time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload_time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload_time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload_time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload_time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload_time = "2024-08-06T20:31:58.304Z" }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload_time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload_time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload_time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload_time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload_time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload_time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload_time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload_time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload_time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload_time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload_time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload_time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload_time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload_time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload_time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload_time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload_time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload_time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload_time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload_time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload_time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload_time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload_time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload_time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload_time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload_time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload_time = "2024-08-06T20:33:04.33Z" }, -] - -[[package]] -name = "requests" -version = "2.32.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload_time = "2025-06-09T16:43:07.34Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload_time = "2025-06-09T16:43:05.728Z" }, -] - -[[package]] -name = "ruff" -version = "0.12.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload_time = "2025-08-07T19:05:47.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload_time = "2025-08-07T19:05:06.15Z" }, - { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload_time = "2025-08-07T19:05:09.759Z" }, - { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload_time = "2025-08-07T19:05:12.551Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload_time = "2025-08-07T19:05:14.821Z" }, - { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload_time = "2025-08-07T19:05:16.712Z" }, - { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload_time = "2025-08-07T19:05:18.709Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload_time = "2025-08-07T19:05:21.025Z" }, - { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload_time = "2025-08-07T19:05:23.423Z" }, - { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload_time = "2025-08-07T19:05:25.507Z" }, - { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload_time = "2025-08-07T19:05:28.14Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload_time = "2025-08-07T19:05:30.413Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload_time = "2025-08-07T19:05:32.492Z" }, - { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload_time = "2025-08-07T19:05:34.449Z" }, - { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload_time = "2025-08-07T19:05:36.541Z" }, - { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload_time = "2025-08-07T19:05:38.468Z" }, - { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload_time = "2025-08-07T19:05:40.391Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload_time = "2025-08-07T19:05:42.866Z" }, -] - -[[package]] -name = "sqlparse" -version = "0.5.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload_time = "2024-12-10T12:05:30.728Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload_time = "2024-12-10T12:05:27.824Z" }, -] - -[[package]] -name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload_time = "2024-11-27T22:38:36.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload_time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload_time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload_time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload_time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload_time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload_time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload_time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload_time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload_time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload_time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload_time = "2024-11-27T22:38:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload_time = "2024-11-27T22:38:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload_time = "2024-11-27T22:38:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload_time = "2024-11-27T22:38:11.443Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload_time = "2024-11-27T22:38:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload_time = "2024-11-27T22:38:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload_time = "2024-11-27T22:38:15.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload_time = "2024-11-27T22:38:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload_time = "2024-11-27T22:38:19.159Z" }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload_time = "2024-11-27T22:38:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload_time = "2024-11-27T22:38:21.659Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload_time = "2024-11-27T22:38:22.693Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload_time = "2024-11-27T22:38:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload_time = "2024-11-27T22:38:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload_time = "2024-11-27T22:38:27.921Z" }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload_time = "2024-11-27T22:38:29.591Z" }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload_time = "2024-11-27T22:38:30.639Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload_time = "2024-11-27T22:38:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload_time = "2024-11-27T22:38:32.837Z" }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload_time = "2024-11-27T22:38:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload_time = "2024-11-27T22:38:35.385Z" }, -] - -[[package]] -name = "ty" -version = "0.0.1a17" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/59/f29cf1adc5c5dd6e739e08138dfa2435d3210841c6b6aa4d5bee7203cabf/ty-0.0.1a17.tar.gz", hash = "sha256:8bd0c5722c630b46a136ffc8f273f47d46cf00d9df2b0c72f1bfd28d1908a7c2", size = 4037064, upload_time = "2025-08-06T12:13:55.862Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/76/1275e4b02a74dbff40408c608dcccb758245173bd794618dadcb092e384f/ty-0.0.1a17-py3-none-linux_armv6l.whl", hash = "sha256:c16b109f05ab34f084b98b9da84c795a23780c9a2f44c237464e52efc5f97139", size = 7970950, upload_time = "2025-08-06T12:13:24.031Z" }, - { url = "https://files.pythonhosted.org/packages/df/15/10947e3a0993b02acb563fa958fb9334937615195dbe6efd17c889d1925d/ty-0.0.1a17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:59e2db756b2b727723ebee58c2e00f172e00a60083a49c8522a19e81887dbc71", size = 8117684, upload_time = "2025-08-06T12:13:26.035Z" }, - { url = "https://files.pythonhosted.org/packages/c0/73/1f982b361b0f161dad3181739f6dc010252e17d5eb8eea625d88f03deb23/ty-0.0.1a17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:79b6d76d64f86414d482f08e09433bedd3e489a1973fa1226b457d4935b592a3", size = 7721774, upload_time = "2025-08-06T12:13:27.528Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fc/a8837d4c1e395730157b16f379f4204035bb75e3fc815a1238c02bab2655/ty-0.0.1a17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdbc144e7d7a5c8dc102715870bf211b51efe581e952a933a0fcba2df9d6ac8d", size = 7841709, upload_time = "2025-08-06T12:13:29.726Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ae/1f69a9aa9f3092c7c1e3bf8e8d2d3db4a7a03108432fc02af24e313c8deb/ty-0.0.1a17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:677388fbe5b9e75764dd8b520eff9c3273f9749ece5425eb34e6fa1359430e3b", size = 7811651, upload_time = "2025-08-06T12:13:31.448Z" }, - { url = "https://files.pythonhosted.org/packages/f9/d8/561283da06dd8f7da44543af9e4a7fde1716f1fe174dde073f78ea291b35/ty-0.0.1a17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdaa4baee9f559ee5bb36b66ad0635e2e4308c6937e8e655a4d4ae1bcf736ad0", size = 8702850, upload_time = "2025-08-06T12:13:34.484Z" }, - { url = "https://files.pythonhosted.org/packages/8a/10/da263a67fea576027b65d78a7d2a55d9829aa22b17e0e10d4201b8d6bd8c/ty-0.0.1a17-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a0637f80301e7a961b87df2e01db22e8f764cd46d371a73b9a968e1894c334ab", size = 9188621, upload_time = "2025-08-06T12:13:36.206Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/e0d2c55df43ecf1bd5f11859fd9f8dd8536643ce1433aec6b8c8bf3b2865/ty-0.0.1a17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:749622726587e758fbbb530d1ab293b7489476cd5502c3ac5da5d6b042bb6a1b", size = 8795061, upload_time = "2025-08-06T12:13:37.99Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3c/ad62544ad7982cb4029f7901953ede9a27c7f6a3afef207fef6a4c6ebfce/ty-0.0.1a17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f1cb91c4b79127193829ab2d1bda79a86ab782fcbdf2988b5a3af37a44a7ae2", size = 8643000, upload_time = "2025-08-06T12:13:40.075Z" }, - { url = "https://files.pythonhosted.org/packages/c5/cb/029bf9f24bb5c5c7c4b139d1f131b19530303fcdd8141607a8d065a87f74/ty-0.0.1a17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7310e783c54f4f9b2380e050b2e992ccbebcf7e73748689f1d8408199cc5a14e", size = 8432265, upload_time = "2025-08-06T12:13:41.631Z" }, - { url = "https://files.pythonhosted.org/packages/ee/6c/b4c7ba46218953a822f17813ed2d86238a04ca7937de286841a2c18ff857/ty-0.0.1a17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:96681e474600811066bf42e3e2cfac7603b7ca1da065fb4f852f94f3df9c944a", size = 7730711, upload_time = "2025-08-06T12:13:43.32Z" }, - { url = "https://files.pythonhosted.org/packages/74/fd/f3aa541e1b7e1d0becf9f28e44e986ae5eb556f827a5312011026938d197/ty-0.0.1a17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ddc72485af203a70267c522ff6ab6cf648ea0a065a7fa542e9e9552c83cdaee", size = 7836927, upload_time = "2025-08-06T12:13:44.614Z" }, - { url = "https://files.pythonhosted.org/packages/94/06/7d8b4b52af385a20705cc063a7f9c144aae3b6aaef165ad2fcc029c9f755/ty-0.0.1a17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7b05f97cc5149b01cb979b9b6b2d773055fb482e490d7169d7c3a213de07ade5", size = 8304523, upload_time = "2025-08-06T12:13:45.983Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a6/e14d4600339a6654e9ccc90ad9662a116f9544e0afb8d0abf1c99d6a2c2d/ty-0.0.1a17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8f80d9b5bc8681fe072ede10d4052035ec1f54c194532932e6c4230a2a5526e5", size = 8492400, upload_time = "2025-08-06T12:13:47.406Z" }, - { url = "https://files.pythonhosted.org/packages/ab/90/19dac956ab9f1ad04b4d1a38df856b44f237b3eda5af5b76a29439e64165/ty-0.0.1a17-py3-none-win32.whl", hash = "sha256:c3ba585145c4a019cb31001a1d0bd606e01fe01b285a20188a42eba165dddd50", size = 7617341, upload_time = "2025-08-06T12:13:49.093Z" }, - { url = "https://files.pythonhosted.org/packages/f5/94/08e2b3f6bc0af97abcd3fcc8ea28797a627296613256ae37e98043c871ca/ty-0.0.1a17-py3-none-win_amd64.whl", hash = "sha256:7d00b569ebd4635c58840d2ed9e1d2d8b36f496619c0bc0c8d1777767786b508", size = 8230727, upload_time = "2025-08-06T12:13:50.705Z" }, - { url = "https://files.pythonhosted.org/packages/98/c6/207bbc2f3bb71df4b1aeabe8e9c31a1cd22c72aff0ab9c1a832b9ae54f6e/ty-0.0.1a17-py3-none-win_arm64.whl", hash = "sha256:636eacc1dceaf09325415a70a03cd57eae53e5c7f281813aaa943a698a45cddb", size = 7782847, upload_time = "2025-08-06T12:13:54.243Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload_time = "2025-07-04T13:28:34.16Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload_time = "2025-07-04T13:28:32.743Z" }, -] - -[[package]] -name = "tzdata" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload_time = "2025-03-23T13:54:43.652Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload_time = "2025-03-23T13:54:41.845Z" }, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload_time = "2025-06-18T14:07:41.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload_time = "2025-06-18T14:07:40.39Z" }, -] - -[[package]] -name = "virtualenv" -version = "20.33.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160, upload_time = "2025-08-05T16:10:55.605Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362, upload_time = "2025-08-05T16:10:52.81Z" }, -]