diff --git a/.github/ansible/production/nest.yaml b/.github/ansible/production/nest.yaml index e73690b6d3..944f2a25d2 100644 --- a/.github/ansible/production/nest.yaml +++ b/.github/ansible/production/nest.yaml @@ -63,7 +63,7 @@ ansible.builtin.command: cmd: crontab /tmp/production_crontab - - name: Retart services + - name: Restart services shell: cmd: docker compose up -d --pull always diff --git a/.github/ansible/production/proxy.yaml b/.github/ansible/production/proxy.yaml index c2bd639677..0bf1bebccd 100644 --- a/.github/ansible/production/proxy.yaml +++ b/.github/ansible/production/proxy.yaml @@ -20,9 +20,9 @@ dest: ~/docker-compose.yaml mode: '0644' - - name: Retart services + - name: Restart services shell: - cmd: docker compose up -d --pull always + cmd: docker compose up -d --pull always && docker compose restart - name: Prune docker images shell: diff --git a/.github/ansible/staging/nest.yaml b/.github/ansible/staging/nest.yaml index b0049a3659..c8dafb5d55 100644 --- a/.github/ansible/staging/nest.yaml +++ b/.github/ansible/staging/nest.yaml @@ -67,7 +67,7 @@ ansible.builtin.command: cmd: crontab /tmp/staging_crontab - - name: Retart services + - name: Restart services shell: cmd: docker compose up -d --pull always diff --git a/.github/ansible/staging/proxy.yaml b/.github/ansible/staging/proxy.yaml index 43ed19d970..dc75d8c107 100644 --- a/.github/ansible/staging/proxy.yaml +++ b/.github/ansible/staging/proxy.yaml @@ -20,9 +20,9 @@ dest: ~/docker-compose.yaml mode: '0644' - - name: Retart services + - name: Restart services shell: - cmd: docker compose up -d --pull always + cmd: docker compose up -d --pull always && docker compose restart - name: Prune docker images shell: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d8b9bcc8d6..26898ec789 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,12 @@ version: 2 updates: - package-ecosystem: docker - directory: /backend/docker + directory: /docker/backend schedule: interval: daily - package-ecosystem: docker - directory: /cspell + directory: /docker/cspell schedule: interval: daily @@ -31,12 +31,12 @@ updates: interval: daily - package-ecosystem: docker - directory: /docs/docker + directory: /docker/docs schedule: interval: daily - package-ecosystem: docker - directory: /frontend/docker + directory: /docker/frontend schedule: interval: daily diff --git a/.github/workflows/check-pr-issue.yaml b/.github/workflows/check-pr-issue.yaml index 8cae60bc39..3a1f8c199b 100644 --- a/.github/workflows/check-pr-issue.yaml +++ b/.github/workflows/check-pr-issue.yaml @@ -5,15 +5,13 @@ on: types: - opened -permissions: - contents: read - issues: read - pull-requests: write - jobs: check-pr-issue: + permissions: + contents: read + issues: read + pull-requests: write runs-on: ubuntu-latest - steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 with: diff --git a/.github/workflows/label-issues.yaml b/.github/workflows/label-issues.yaml index 6261dc299d..53f07ca059 100644 --- a/.github/workflows/label-issues.yaml +++ b/.github/workflows/label-issues.yaml @@ -6,11 +6,10 @@ on: - edited - opened -permissions: - issues: write - jobs: label: + permissions: + issues: write runs-on: ubuntu-latest steps: - name: Apply Labels to Issues diff --git a/.github/workflows/label-pull-requests.yaml b/.github/workflows/label-pull-requests.yaml index f61873877a..f3616e883a 100644 --- a/.github/workflows/label-pull-requests.yaml +++ b/.github/workflows/label-pull-requests.yaml @@ -3,12 +3,11 @@ name: Label Pull Requests on: - pull_request_target -permissions: - contents: read - pull-requests: write - jobs: labeler: + permissions: + contents: read + pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b diff --git a/.github/workflows/run-ci-cd.yaml b/.github/workflows/run-ci-cd.yaml index afd289a004..650f4230ea 100644 --- a/.github/workflows/run-ci-cd.yaml +++ b/.github/workflows/run-ci-cd.yaml @@ -22,12 +22,11 @@ on: env: FORCE_COLOR: 1 -permissions: - contents: read - jobs: pre-commit: name: Run pre-commit checks + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out repository @@ -44,7 +43,7 @@ jobs: python-version: '3.13' - name: Set up pre-commit cache - uses: actions/cache@v5 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb with: path: ~/.cache/pre-commit key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} @@ -61,6 +60,8 @@ jobs: check-frontend: name: Run frontend checks + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out repository @@ -94,6 +95,8 @@ jobs: spellcheck: name: Run spell check + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out repository @@ -109,6 +112,8 @@ jobs: - check-frontend - pre-commit - spellcheck + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out repository @@ -123,11 +128,13 @@ jobs: version: latest scan-ci-dependencies: - name: Run CI Denendencies Scan + name: Run CI Dependencies Scan needs: - check-frontend - pre-commit - spellcheck + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out repository @@ -146,8 +153,9 @@ jobs: needs: - scan-code - scan-ci-dependencies + permissions: + contents: read runs-on: ubuntu-latest - timeout-minutes: 10 steps: - name: Check out repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 @@ -164,7 +172,7 @@ jobs: cache-to: | type=gha,compression=zstd context: backend - file: backend/docker/Dockerfile.test + file: docker/backend/Dockerfile.test load: true platforms: linux/amd64 tags: owasp/nest:test-backend-latest @@ -172,14 +180,16 @@ jobs: - name: Run backend tests run: | docker run -e DJANGO_SETTINGS_MODULE=settings.test --env-file backend/.env.example owasp/nest:test-backend-latest pytest + timeout-minutes: 10 run-frontend-unit-tests: name: Run frontend unit tests needs: - scan-code - scan-ci-dependencies + permissions: + contents: read runs-on: ubuntu-latest - timeout-minutes: 10 steps: - name: Check out repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 @@ -196,7 +206,7 @@ jobs: cache-to: | type=gha,compression=zstd context: frontend - file: frontend/docker/Dockerfile.unit.test + file: docker/frontend/Dockerfile.unit.test load: true platforms: linux/amd64 tags: owasp/nest:test-frontend-unit-latest @@ -204,14 +214,16 @@ jobs: - name: Run frontend unit tests run: | docker run --env-file frontend/.env.example owasp/nest:test-frontend-unit-latest pnpm run test:unit + timeout-minutes: 10 run-frontend-e2e-tests: name: Run frontend e2e tests needs: - scan-code - scan-ci-dependencies + permissions: + contents: read runs-on: ubuntu-latest - timeout-minutes: 10 services: db: image: pgvector/pgvector:pg16 @@ -286,7 +298,7 @@ jobs: type=gha type=registry,ref=owasp/nest:test-frontend-e2e-cache context: frontend - file: frontend/docker/Dockerfile.e2e.test + file: docker/frontend/Dockerfile.e2e.test load: true platforms: linux/amd64 tags: owasp/nest:test-frontend-e2e-latest @@ -294,12 +306,14 @@ jobs: - name: Run frontend end-to-end tests run: | docker run --env-file frontend/.env.e2e.example owasp/nest:test-frontend-e2e-latest pnpm run test:e2e + timeout-minutes: 10 set-release-version: name: Set release version - runs-on: ubuntu-latest outputs: release_version: ${{ steps.set.outputs.release_version }} + permissions: {} + runs-on: ubuntu-latest steps: - name: Set release version id: set @@ -392,8 +406,8 @@ jobs: type=registry,ref=owasp/nest:test-fuzz-backend-cache cache-to: | type=gha,compression=zstd - context: backend/docker - file: backend/docker/Dockerfile.fuzz + context: docker/backend + file: docker/backend/Dockerfile.fuzz load: true platforms: linux/amd64 tags: owasp/nest:test-fuzz-backend-latest @@ -427,8 +441,9 @@ jobs: - run-frontend-e2e-tests - run-frontend-unit-tests - set-release-version + permissions: + contents: read runs-on: ubuntu-latest - timeout-minutes: 10 steps: - name: Check out repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 @@ -457,7 +472,7 @@ jobs: cache-to: | type=registry,ref=owasp/nest:backend-staging-cache context: backend - file: backend/docker/Dockerfile + file: docker/backend/Dockerfile load: true platforms: linux/amd64 push: true @@ -505,7 +520,7 @@ jobs: cache-to: | type=registry,ref=owasp/nest:frontend-staging-cache context: frontend - file: frontend/docker/Dockerfile + file: docker/frontend/Dockerfile load: true platforms: linux/amd64 push: true @@ -530,11 +545,14 @@ jobs: echo "**Backend:** ${{ steps.backend-size.outputs.human_readable }}" echo "**Frontend:** ${{ steps.frontend-size.outputs.human_readable }}" } >> $GITHUB_STEP_SUMMARY + timeout-minutes: 10 scan-staging-images: name: Scan Staging Images needs: - build-staging-images + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out repository @@ -576,6 +594,8 @@ jobs: needs: - scan-staging-images - set-release-version + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out repository @@ -690,6 +710,8 @@ jobs: github.ref == 'refs/heads/main' needs: - deploy-staging-nest + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out repository @@ -711,10 +733,12 @@ jobs: working-directory: .github/ansible run: ansible-playbook -i inventory.yaml staging/proxy.yaml -e "github_workspace=$GITHUB_WORKSPACE" - run-lighthouse-ci: + run-staging-lighthouse-ci: name: Run Lighthouse CI needs: - deploy-staging-nest-proxy + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out repository @@ -741,6 +765,31 @@ jobs: timeout-minutes: 15 working-directory: frontend + run-staging-zap-baseline-scan: + name: Run ZAP Baseline Scan + needs: + - deploy-staging-nest-proxy + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - name: Run ZAP Baseline Scan + uses: zaproxy/action-baseline@de8ad967d3548d44ef623df22cf95c3b0baf8b25 + with: + token: ${{ secrets.GITHUB_TOKEN }} + target: 'https://nest.owasp.dev' + allow_issue_writing: false + fail_action: false + cmd_options: '-a -r zap-report.html' + + - name: Upload ZAP report + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: zap-baseline-scan-report-${{ github.run_id }} + path: zap-report.html + + build-production-images: name: Build Production Images env: @@ -754,8 +803,9 @@ jobs: - run-frontend-e2e-tests - run-frontend-unit-tests - set-release-version + permissions: + contents: read runs-on: ubuntu-latest - timeout-minutes: 10 steps: - name: Check out repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 @@ -782,7 +832,7 @@ jobs: type=gha type=registry,ref=owasp/nest:backend-staging-cache context: backend - file: backend/docker/Dockerfile + file: docker/backend/Dockerfile load: true platforms: linux/amd64 push: true @@ -828,7 +878,7 @@ jobs: type=gha type=registry,ref=owasp/nest:frontend-staging-cache context: frontend - file: frontend/docker/Dockerfile + file: docker/frontend/Dockerfile load: true platforms: linux/amd64 push: true @@ -853,11 +903,14 @@ jobs: echo "**Backend:** ${{ steps.backend-size.outputs.human_readable }}" echo "**Frontend:** ${{ steps.frontend-size.outputs.human_readable }}" } >> $GITHUB_STEP_SUMMARY + timeout-minutes: 10 scan-production-images: name: Scan Production Images needs: - build-production-images + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out repository @@ -903,6 +956,8 @@ jobs: needs: - scan-production-images - set-release-version + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out repository @@ -1028,6 +1083,8 @@ jobs: github.event.action == 'published' needs: - deploy-production-nest + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out repository @@ -1048,3 +1105,27 @@ jobs: - name: Run proxy deploy working-directory: .github/ansible run: ansible-playbook -i inventory.yaml production/proxy.yaml -e "github_workspace=$GITHUB_WORKSPACE" + + run-production-zap-baseline-scan: + name: Run ZAP Baseline Scan + needs: + - deploy-production-nest-proxy + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - name: Run ZAP Baseline Scan + uses: zaproxy/action-baseline@de8ad967d3548d44ef623df22cf95c3b0baf8b25 + with: + token: ${{ secrets.GITHUB_TOKEN }} + target: 'https://nest.owasp.org' + allow_issue_writing: false + fail_action: false + cmd_options: '-a -r zap-report.html' + + - name: Upload ZAP report + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: zap-baseline-scan-report-${{ github.run_id }} + path: zap-report.html diff --git a/.github/workflows/run-code-ql.yaml b/.github/workflows/run-code-ql.yaml index a694a4cb61..d97bc4414b 100644 --- a/.github/workflows/run-code-ql.yaml +++ b/.github/workflows/run-code-ql.yaml @@ -12,13 +12,11 @@ on: - main workflow_dispatch: -permissions: - contents: read - jobs: code-ql: name: CodeQL permissions: + contents: read security-events: write runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/setup-backend-environment/action.yaml b/.github/workflows/setup-backend-environment/action.yaml index 54f51d85aa..5fcf9b9923 100644 --- a/.github/workflows/setup-backend-environment/action.yaml +++ b/.github/workflows/setup-backend-environment/action.yaml @@ -38,7 +38,7 @@ runs: cache-to: | type=gha,compression=zstd context: backend - file: backend/docker/Dockerfile + file: docker/backend/Dockerfile load: true platforms: linux/amd64 tags: owasp/nest:test-backend-latest diff --git a/.github/workflows/update-nest-test-images.yaml b/.github/workflows/update-nest-test-images.yaml index 90c26e109b..b816b60105 100644 --- a/.github/workflows/update-nest-test-images.yaml +++ b/.github/workflows/update-nest-test-images.yaml @@ -8,13 +8,12 @@ on: env: FORCE_COLOR: 1 -permissions: - contents: read - jobs: update-nest-test-images: name: Update Nest test images if: ${{ github.repository == 'OWASP/Nest' }} + permissions: + contents: read runs-on: ubuntu-latest steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 @@ -38,7 +37,7 @@ jobs: type=gha,compression=zstd type=registry,ref=owasp/nest:test-backend-cache context: backend - file: backend/docker/Dockerfile.test + file: docker/backend/Dockerfile.test platforms: linux/amd64 push: true tags: owasp/nest:test-backend-latest @@ -53,7 +52,7 @@ jobs: type=gha,compression=zstd type=registry,ref=owasp/nest:test-frontend-unit-cache context: frontend - file: frontend/docker/Dockerfile.unit.test + file: docker/frontend/Dockerfile.unit.test platforms: linux/amd64 push: true tags: owasp/nest:test-frontend-unit-latest @@ -68,7 +67,7 @@ jobs: type=gha,compression=zstd type=registry,ref=owasp/nest:test-frontend-e2e-cache context: frontend - file: frontend/docker/Dockerfile.e2e.test + file: docker/frontend/Dockerfile.e2e.test platforms: linux/amd64 push: true tags: owasp/nest:test-frontend-e2e-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 34f3f7a621..107b8ebf51 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -472,6 +472,82 @@ If you are adding new functionality, include relevant test cases. ## Contributing Workflow +The following diagram illustrates the complete contribution workflow: + +```mermaid +flowchart TD + Start([Start]) --> CreateIssue[Create New Issue] + Start --> FindIssue[Find Existing Issue] + CreateIssue --> GetAssigned["**Get Assigned to Issue**
PRs will be automatically
closed if you're not assigned"] + FindIssue --> GetAssigned + GetAssigned --> ResolveIssue[**Resolve Issue**
work on code/docs/tests updates] + + ResolveIssue --> RunChecks{**Run `make check-test`**
locally! This is a required step -- you will not be assigned to new issues if you ignore this} + RunChecks -->|Fails| WP1[ ] + RunChecks -->|Passes| PushChanges[**Push Changes to
GitHub Fork Branch**] + WP1 -.-> ResolveIssue + + PushChanges --> HasPR{PR Exists?} + HasPR -->|No| CreateDraftPR[Create Draft PR] + HasPR -->|Yes| WaitAutoChecks[**Wait for Automated
Checks to Finish**] + CreateDraftPR --> WaitAutoChecks + + WaitAutoChecks --> CheckAutoTools{All **CodeRabbit and
SonarQube** Comments
Resolved?} + CheckAutoTools -->|No| MarkDraft[Make Sure PR Is **Marked as a Draft**] + CheckAutoTools -->|Yes| MarkReady[Mark PR as Ready
for Review] + MarkDraft --> WP2[ ] + WP2 -.-> ResolveIssue + + MarkReady --> RequestReview[Request Review from
Project Maintainers] + RequestReview --> WaitMaintainer[Wait for Maintainers'
Comments] + + WaitMaintainer --> HasMaintainerComments{**Maintainers' Comments
Resolved**?} + HasMaintainerComments -->|No| MarkDraft + HasMaintainerComments -->|Yes| CheckCI{**CI/CD
Passing?**} + + CheckCI -->|Yes| ReadyMerge([PR Ready for Merge]) + CheckCI -->|No| MarkDraft + + style Start fill:#4caf50,stroke:#2e7d32,stroke-width:2px,color:#ffffff + style ReadyMerge fill:#4caf50,stroke:#2e7d32,stroke-width:2px,color:#ffffff + style ResolveIssue fill:#ff9800,stroke:#f57c00,stroke-width:2px,color:#000000 + style RunChecks fill:#2196f3,stroke:#1565c0,stroke-width:2px,color:#ffffff + style CheckAutoTools fill:#2196f3,stroke:#1565c0,stroke-width:2px,color:#ffffff + style HasMaintainerComments fill:#2196f3,stroke:#1565c0,stroke-width:2px,color:#ffffff + style CheckCI fill:#2196f3,stroke:#1565c0,stroke-width:2px,color:#ffffff + style MarkDraft fill:#ff9800,stroke:#f57c00,stroke-width:2px,color:#000000 + style MarkReady fill:#4caf50,stroke:#2e7d32,stroke-width:2px,color:#ffffff + style CreateDraftPR fill:#ff9800,stroke:#f57c00,stroke-width:2px,color:#000000 + style WP1 fill:transparent,stroke:transparent,color:transparent,width:0px,height:0px + style WP2 fill:transparent,stroke:transparent,color:transparent,width:0px,height:0px + + linkStyle 0 stroke:#4caf50,stroke-width:2px + linkStyle 1 stroke:#4caf50,stroke-width:2px + linkStyle 2 stroke:#4caf50,stroke-width:2px + linkStyle 3 stroke:#4caf50,stroke-width:2px + linkStyle 4 stroke:#4caf50,stroke-width:2px + linkStyle 5 stroke:#4caf50,stroke-width:2px + linkStyle 6 stroke:#f44336,stroke-width:2px + linkStyle 7 stroke:#4caf50,stroke-width:2px + linkStyle 8 stroke:#f44336,stroke-width:2px + linkStyle 9 stroke:#4caf50,stroke-width:2px + linkStyle 10 stroke:#9e9e9e,stroke-width:2px + linkStyle 11 stroke:#4caf50,stroke-width:2px + linkStyle 12 stroke:#4caf50,stroke-width:2px + linkStyle 13 stroke:#4caf50,stroke-width:2px + linkStyle 14 stroke:#f44336,stroke-width:2px + linkStyle 15 stroke:#4caf50,stroke-width:2px + linkStyle 16 stroke:#f44336,stroke-width:2px + linkStyle 17 stroke:#f44336,stroke-width:2px + linkStyle 18 stroke:#4caf50,stroke-width:2px + linkStyle 19 stroke:#4caf50,stroke-width:2px + linkStyle 20 stroke:#4caf50,stroke-width:2px + linkStyle 21 stroke:#f44336,stroke-width:2px + linkStyle 22 stroke:#4caf50,stroke-width:2px + linkStyle 23 stroke:#4caf50,stroke-width:2px + linkStyle 24 stroke:#f44336,stroke-width:2px +``` + ### 1. Find Something to Work On - Check the **Issues** tab for open issues: [https://github.com/owasp/nest/issues](https://github.com/owasp/nest/issues) diff --git a/Makefile b/Makefile index 4ecdc35cb0..2d29a5c668 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ prune: @docker volume prune -f run: - @COMPOSE_BAKE=true DOCKER_BUILDKIT=1 \ + @DOCKER_BUILDKIT=1 \ docker compose -f docker-compose/local/compose.yaml --project-name nest-local build && \ docker compose -f docker-compose/local/compose.yaml --project-name nest-local up --remove-orphans diff --git a/README.md b/README.md index 344ef67f28..d0c33c15e2 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ -**OWASP Nest** is a comprehensive platform designed to enhance collaboration and contribution within the OWASP community. The application serves as a central hub for exploring OWASP projects and ways to contribute to them, empowering contributors to find opportunities that align with their interests and expertise. +**OWASP Nest** is a comprehensive, community-first platform built to enhance collaboration and contribution across the OWASP community. The application serves as a central hub for exploring OWASP projects and ways to contribute to them, empowering contributors to find opportunities that align with their interests and expertise. Key features of the platform include: diff --git a/backend/Makefile b/backend/Makefile index 0fd907f7ca..cbdb615678 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -157,7 +157,7 @@ sync-data: \ test-backend: @DOCKER_BUILDKIT=1 docker build \ --cache-from nest-test-backend \ - -f backend/docker/Dockerfile.test backend \ + -f docker/backend/Dockerfile.test backend \ -t nest-test-backend @docker run \ -e DJANGO_SETTINGS_MODULE=settings.test \ @@ -184,6 +184,7 @@ update-data: \ github-update-related-organizations \ github-update-users \ owasp-aggregate-projects \ + owasp-aggregate-contributions \ owasp-update-events \ owasp-sync-posts \ owasp-update-sponsors \ diff --git a/backend/apps/api/rest/v0/chapter.py b/backend/apps/api/rest/v0/chapter.py index 863a60784d..be7d9290f1 100644 --- a/backend/apps/api/rest/v0/chapter.py +++ b/backend/apps/api/rest/v0/chapter.py @@ -11,7 +11,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response -from apps.api.rest.v0.common import LocationFilter +from apps.api.rest.v0.common import Leader, LocationFilter from apps.owasp.models.chapter import Chapter as ChapterModel router = RouterPaginated(tags=["Chapters"]) @@ -41,8 +41,17 @@ class ChapterDetail(ChapterBase): """Detail schema for Chapter (used in single item endpoints).""" country: str + leaders: list[Leader] region: str + @staticmethod + def resolve_leaders(obj): + """Resolve leaders.""" + return [ + Leader(key=leader.member.login if leader.member else None, name=leader.member_name) + for leader in obj.entity_leaders + ] + class ChapterError(Schema): """Chapter error schema.""" diff --git a/backend/apps/api/rest/v0/common.py b/backend/apps/api/rest/v0/common.py index 74d21efe25..123207e086 100644 --- a/backend/apps/api/rest/v0/common.py +++ b/backend/apps/api/rest/v0/common.py @@ -1,6 +1,13 @@ """Common schemas and filters for the API.""" -from ninja import Field, FilterSchema +from ninja import Field, FilterSchema, Schema + + +class Leader(Schema): + """Schema for Leader.""" + + key: str | None = None + name: str class LocationFilter(FilterSchema): diff --git a/backend/apps/api/rest/v0/project.py b/backend/apps/api/rest/v0/project.py index 8583ced431..11d676321e 100644 --- a/backend/apps/api/rest/v0/project.py +++ b/backend/apps/api/rest/v0/project.py @@ -11,6 +11,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response +from apps.api.rest.v0.common import Leader from apps.owasp.models.enums.project import ProjectLevel from apps.owasp.models.project import Project as ProjectModel @@ -40,6 +41,15 @@ class ProjectDetail(ProjectBase): """Detail schema for Project (used in single item endpoints).""" description: str + leaders: list[Leader] + + @staticmethod + def resolve_leaders(obj): + """Resolve leaders.""" + return [ + Leader(key=leader.member.login if leader.member else None, name=leader.member_name) + for leader in obj.entity_leaders + ] class ProjectError(Schema): diff --git a/backend/apps/common/management/commands/purge_data.py b/backend/apps/common/management/commands/purge_data.py index 32ed671a6f..54f4252aae 100644 --- a/backend/apps/common/management/commands/purge_data.py +++ b/backend/apps/common/management/commands/purge_data.py @@ -1,7 +1,5 @@ """A command to purge OWASP Nest data.""" -# ruff: noqa: SLF001 https://docs.astral.sh/ruff/rules/private-member-access/ - from django.apps import apps from django.core.management.base import BaseCommand from django.db import connection diff --git a/backend/apps/core/utils/index.py b/backend/apps/core/utils/index.py index 40637c91f4..6ea473383b 100644 --- a/backend/apps/core/utils/index.py +++ b/backend/apps/core/utils/index.py @@ -51,23 +51,28 @@ def unregister_indexes(self) -> None: unregister(model) -def deep_camelize(obj) -> dict | list: +def deep_camelize(obj) -> dict | list | None: """Deep camelize. Args: obj: The object to camelize. Returns: - The camelize object. + The camelize object or None. """ + if not obj: + return obj + if isinstance(obj, dict): return { convert_to_camel_case(key.removeprefix("idx_")): deep_camelize(value) for key, value in obj.items() } + if isinstance(obj, list): return [deep_camelize(item) for item in obj] + return obj diff --git a/backend/apps/owasp/Makefile b/backend/apps/owasp/Makefile index 4febcd2572..e88a5b7325 100644 --- a/backend/apps/owasp/Makefile +++ b/backend/apps/owasp/Makefile @@ -2,6 +2,11 @@ owasp-aggregate-projects: @echo "Aggregating OWASP projects" @CMD="python manage.py owasp_aggregate_projects" $(MAKE) exec-backend-command +owasp-aggregate-contributions: + @echo "Aggregating OWASP contributions" + @CMD="python manage.py owasp_aggregate_contributions --entity-type chapter" $(MAKE) exec-backend-command + @CMD="python manage.py owasp_aggregate_contributions --entity-type project" $(MAKE) exec-backend-command + owasp-create-project-metadata-file: @echo "Generating metadata" @CMD="python manage.py owasp_create_project_metadata_file $(entity_key)" $(MAKE) exec-backend-command diff --git a/backend/apps/owasp/api/internal/nodes/chapter.py b/backend/apps/owasp/api/internal/nodes/chapter.py index 23eb7a7fb6..6e5c29bfb4 100644 --- a/backend/apps/owasp/api/internal/nodes/chapter.py +++ b/backend/apps/owasp/api/internal/nodes/chapter.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.core.utils.index import deep_camelize from apps.owasp.api.internal.nodes.common import GenericEntityNode from apps.owasp.models.chapter import Chapter @@ -18,6 +19,7 @@ class GeoLocationType: @strawberry_django.type( Chapter, fields=[ + "contribution_data", "country", "is_active", "meetup_group", @@ -31,6 +33,11 @@ class GeoLocationType: class ChapterNode(GenericEntityNode): """Chapter node.""" + @strawberry.field + def contribution_stats(self) -> strawberry.scalars.JSON | None: + """Resolve contribution stats with camelCase keys.""" + return deep_camelize(self.contribution_stats) + @strawberry.field def created_at(self) -> float: """Resolve created at.""" diff --git a/backend/apps/owasp/api/internal/nodes/project.py b/backend/apps/owasp/api/internal/nodes/project.py index 4576b36280..78617f596a 100644 --- a/backend/apps/owasp/api/internal/nodes/project.py +++ b/backend/apps/owasp/api/internal/nodes/project.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.core.utils.index import deep_camelize from apps.github.api.internal.nodes.issue import IssueNode from apps.github.api.internal.nodes.milestone import MilestoneNode from apps.github.api.internal.nodes.pull_request import PullRequestNode @@ -23,6 +24,7 @@ @strawberry_django.type( Project, fields=[ + "contribution_data", "contributors_count", "created_at", "forks_count", @@ -39,6 +41,11 @@ class ProjectNode(GenericEntityNode): """Project node.""" + @strawberry.field + def contribution_stats(self) -> strawberry.scalars.JSON | None: + """Resolve contribution stats with camelCase keys.""" + return deep_camelize(self.contribution_stats) + @strawberry.field def health_metrics_list(self, limit: int = 30) -> list[ProjectHealthMetricsNode]: """Resolve project health metrics.""" diff --git a/backend/apps/owasp/management/commands/owasp_aggregate_contributions.py b/backend/apps/owasp/management/commands/owasp_aggregate_contributions.py new file mode 100644 index 0000000000..9202544fe3 --- /dev/null +++ b/backend/apps/owasp/management/commands/owasp_aggregate_contributions.py @@ -0,0 +1,264 @@ +"""Management command to aggregate contributions for chapters and projects.""" + +from datetime import datetime, timedelta + +from django.core.management.base import BaseCommand +from django.utils import timezone + +from apps.github.models.commit import Commit +from apps.github.models.issue import Issue +from apps.github.models.pull_request import PullRequest +from apps.github.models.release import Release +from apps.owasp.models.chapter import Chapter +from apps.owasp.models.project import Project + + +class Command(BaseCommand): + """Aggregate contribution data for chapters and projects.""" + + help = "Aggregate contributions (commits, issues, PRs, releases) for chapters and projects" + + def add_arguments(self, parser): + """Add command arguments.""" + parser.add_argument( + "--entity-type", + choices=["chapter", "project"], + help="Entity type to aggregate: chapter, project", + required=True, + type=str, + ) + parser.add_argument( + "--days", + default=365, + help="Number of days to look back for contributions (default: 365)", + type=int, + ) + parser.add_argument( + "--key", + help="Specific chapter or project key to aggregate", + type=str, + ) + parser.add_argument( + "--offset", + default=0, + help="Skip the first N entities", + type=int, + ) + + def _aggregate_contribution_dates( + self, + queryset, + date_field: str, + contribution_map: dict[str, int], + ) -> None: + """Aggregate contribution dates from a queryset into the contribution map. + + Args: + queryset: Django queryset to aggregate + date_field: Name of the date field to aggregate on + contribution_map: Dictionary to update with counts + + """ + for date_value in queryset.values_list(date_field, flat=True): + if not date_value: + continue + + date_key = date_value.date().isoformat() + contribution_map[date_key] = contribution_map.get(date_key, 0) + 1 + + def _get_repository_ids(self, entity): + """Extract repository IDs from chapter or project.""" + repository_ids: set[int] = set() + + # Handle single owasp_repository. + if hasattr(entity, "owasp_repository") and entity.owasp_repository: + repository_ids.add(entity.owasp_repository.id) + + # Handle multiple repositories (for projects). + if hasattr(entity, "repositories"): + repository_ids.update([r.id for r in entity.repositories.all()]) + + return list(repository_ids) + + def aggregate_contributions(self, entity, start_date: datetime) -> dict[str, int]: + """Aggregate contributions for a chapter or project. + + Args: + entity: Chapter or Project instance + start_date: Start date for aggregation + + Returns: + Dictionary mapping YYYY-MM-DD to contribution count + + """ + contribution_map: dict[str, int] = {} + + repository_ids = self._get_repository_ids(entity) + if not repository_ids: + return contribution_map + + # Aggregate commits. + self._aggregate_contribution_dates( + Commit.objects.filter( + created_at__gte=start_date, + repository_id__in=repository_ids, + ), + "created_at", + contribution_map, + ) + + # Aggregate issues. + self._aggregate_contribution_dates( + Issue.objects.filter( + created_at__gte=start_date, + repository_id__in=repository_ids, + ), + "created_at", + contribution_map, + ) + + # Aggregate pull requests. + self._aggregate_contribution_dates( + PullRequest.objects.filter( + created_at__gte=start_date, + repository_id__in=repository_ids, + ), + "created_at", + contribution_map, + ) + + # Aggregate releases. + self._aggregate_contribution_dates( + Release.objects.filter( + is_draft=False, + published_at__gte=start_date, + repository_id__in=repository_ids, + ), + "published_at", + contribution_map, + ) + + return contribution_map + + def calculate_contribution_stats(self, entity, start_date: datetime) -> dict[str, int]: + """Calculate contribution statistics for a chapter or project. + + Args: + entity: Chapter or Project instance + start_date: Start date for calculation + + Returns: + Dictionary with commits, issues, pull requests, releases counts + + """ + stats = { + "commits": 0, + "issues": 0, + "pull_requests": 0, + "releases": 0, + "total": 0, + } + + repository_ids = self._get_repository_ids(entity) + if not repository_ids: + return stats + + # Count commits. + stats["commits"] = Commit.objects.filter( + created_at__gte=start_date, + repository_id__in=repository_ids, + ).count() + + # Count issues. + stats["issues"] = Issue.objects.filter( + created_at__gte=start_date, + repository_id__in=repository_ids, + ).count() + + # Count pull requests. + stats["pull_requests"] = PullRequest.objects.filter( + created_at__gte=start_date, + repository_id__in=repository_ids, + ).count() + + # Count releases. + stats["releases"] = Release.objects.filter( + is_draft=False, + published_at__gte=start_date, + repository_id__in=repository_ids, + ).count() + + stats["total"] = sum( + (stats["commits"], stats["issues"], stats["pull_requests"], stats["releases"]) + ) + + return stats + + def handle(self, *args, **options): + """Execute the command.""" + entity_type = options["entity_type"] + days = options["days"] + key = options.get("key") + offset = options["offset"] + + start_date = timezone.now() - timedelta(days=days) + + self.stdout.write( + self.style.SUCCESS( + f"Aggregating contributions since {start_date.date()} ({days} days back)", + ), + ) + + if entity_type == "chapter": + self._process_chapters(start_date, key, offset) + elif entity_type == "project": + self._process_projects(start_date, key, offset) + + self.stdout.write(self.style.SUCCESS("Done!")) + + def _process_chapters(self, start_date, key, offset): + """Process chapters for contribution aggregation.""" + queryset = Chapter.objects.filter(is_active=True).order_by("id") + + if key: + queryset = queryset.filter(key=key) + + queryset = queryset.select_related("owasp_repository") + + if offset: + queryset = queryset[offset:] + + self._process_entities(queryset, start_date, Chapter) + + def _process_projects(self, start_date, key, offset): + """Process projects for contribution aggregation.""" + queryset = ( + Project.objects.filter(is_active=True) + .order_by("id") + .select_related("owasp_repository") + .prefetch_related("repositories") + ) + + if key: + queryset = queryset.filter(key=key) + + if offset: + queryset = queryset[offset:] + + self._process_entities(queryset, start_date, Project) + + def _process_entities(self, queryset, start_date, model_class): + """Process entities (chapters or projects) for contribution aggregation.""" + entities = list(queryset) + label = model_class._meta.verbose_name_plural + total_count = len(entities) + + self.stdout.write(f"Processing {total_count} {label}...") + + for entity in entities: + entity.contribution_data = self.aggregate_contributions(entity, start_date) + entity.contribution_stats = self.calculate_contribution_stats(entity, start_date) + + if entities: + model_class.bulk_save(entities, fields=("contribution_data", "contribution_stats")) + self.stdout.write(self.style.SUCCESS(f"Updated {total_count} {label}")) diff --git a/backend/apps/owasp/migrations/0066_chapter_contribution_data_project_contribution_data.py b/backend/apps/owasp/migrations/0066_chapter_contribution_data_project_contribution_data.py new file mode 100644 index 0000000000..c17d81a438 --- /dev/null +++ b/backend/apps/owasp/migrations/0066_chapter_contribution_data_project_contribution_data.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.8 on 2025-11-16 18:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0065_memberprofile_linkedin_page_id"), + ] + + operations = [ + migrations.AddField( + model_name="chapter", + name="contribution_data", + field=models.JSONField( + blank=True, + default=dict, + help_text="Daily contribution counts (YYYY-MM-DD -> count mapping)", + verbose_name="Contribution Data", + ), + ), + migrations.AddField( + model_name="project", + name="contribution_data", + field=models.JSONField( + blank=True, + default=dict, + help_text="Daily contribution counts (YYYY-MM-DD -> count mapping)", + verbose_name="Contribution Data", + ), + ), + ] diff --git a/backend/apps/owasp/migrations/0067_chapter_contribution_stats_and_more.py b/backend/apps/owasp/migrations/0067_chapter_contribution_stats_and_more.py new file mode 100644 index 0000000000..913b4b7606 --- /dev/null +++ b/backend/apps/owasp/migrations/0067_chapter_contribution_stats_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.8 on 2025-11-29 19:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0066_chapter_contribution_data_project_contribution_data"), + ] + + operations = [ + migrations.AddField( + model_name="chapter", + name="contribution_stats", + field=models.JSONField( + blank=True, + default=dict, + help_text="Detailed contribution breakdown (commits, issues, pullRequests, releases)", + verbose_name="Contribution Statistics", + ), + ), + migrations.AddField( + model_name="project", + name="contribution_stats", + field=models.JSONField( + blank=True, + default=dict, + help_text="Detailed contribution breakdown (commits, issues, pullRequests, releases)", + verbose_name="Contribution Statistics", + ), + ), + ] diff --git a/backend/apps/owasp/migrations/0068_alter_chapter_contribution_stats_and_more.py b/backend/apps/owasp/migrations/0068_alter_chapter_contribution_stats_and_more.py new file mode 100644 index 0000000000..91892dd19a --- /dev/null +++ b/backend/apps/owasp/migrations/0068_alter_chapter_contribution_stats_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 6.0 on 2025-12-10 04:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0067_chapter_contribution_stats_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="chapter", + name="contribution_stats", + field=models.JSONField( + blank=True, + default=dict, + help_text="Detailed contribution breakdown (commits, issues, pull requests, releases)", + verbose_name="Contribution Statistics", + ), + ), + migrations.AlterField( + model_name="project", + name="contribution_stats", + field=models.JSONField( + blank=True, + default=dict, + help_text="Detailed contribution breakdown (commits, issues, pull requests, releases)", + verbose_name="Contribution Statistics", + ), + ), + ] diff --git a/backend/apps/owasp/migrations/0069_alter_project_contribution_data_and_more.py b/backend/apps/owasp/migrations/0069_alter_project_contribution_data_and_more.py new file mode 100644 index 0000000000..6c2d9730ce --- /dev/null +++ b/backend/apps/owasp/migrations/0069_alter_project_contribution_data_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 6.0 on 2026-01-04 00:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0068_alter_chapter_contribution_stats_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="project", + name="contribution_data", + field=models.JSONField( + blank=True, + default=dict, + help_text="Daily contribution counts (YYYY-MM-DD -> count mapping)", + null=True, + verbose_name="Contribution Data", + ), + ), + migrations.AlterField( + model_name="project", + name="contribution_stats", + field=models.JSONField( + blank=True, + default=dict, + help_text="Detailed contribution breakdown (commits, issues, pull requests, releases)", + null=True, + verbose_name="Contribution Statistics", + ), + ), + ] diff --git a/backend/apps/owasp/models/chapter.py b/backend/apps/owasp/models/chapter.py index ed2a08ff3b..963e1be5db 100644 --- a/backend/apps/owasp/models/chapter.py +++ b/backend/apps/owasp/models/chapter.py @@ -63,6 +63,19 @@ class Meta: latitude = models.FloatField(verbose_name="Latitude", blank=True, null=True) longitude = models.FloatField(verbose_name="Longitude", blank=True, null=True) + contribution_data = models.JSONField( + verbose_name="Contribution Data", + default=dict, + blank=True, + help_text="Daily contribution counts (YYYY-MM-DD -> count mapping)", + ) + contribution_stats = models.JSONField( + verbose_name="Contribution Statistics", + default=dict, + blank=True, + help_text="Detailed contribution breakdown (commits, issues, pull requests, releases)", + ) + # GRs. members = GenericRelation("owasp.EntityMember") diff --git a/backend/apps/owasp/models/project.py b/backend/apps/owasp/models/project.py index 6e4be7944e..5be91c938e 100644 --- a/backend/apps/owasp/models/project.py +++ b/backend/apps/owasp/models/project.py @@ -96,6 +96,21 @@ class Meta: custom_tags = models.JSONField(verbose_name="Custom tags", default=list, blank=True) track_issues = models.BooleanField(verbose_name="Track issues", default=True) + contribution_data = models.JSONField( + verbose_name="Contribution Data", + default=dict, + blank=True, + null=True, + help_text="Daily contribution counts (YYYY-MM-DD -> count mapping)", + ) + contribution_stats = models.JSONField( + verbose_name="Contribution Statistics", + default=dict, + blank=True, + null=True, + help_text="Detailed contribution breakdown (commits, issues, pull requests, releases)", + ) + # GKs. members = GenericRelation("owasp.EntityMember") diff --git a/backend/data/nest.dump b/backend/data/nest.dump index deff45da21..390fae2948 100644 Binary files a/backend/data/nest.dump and b/backend/data/nest.dump differ diff --git a/backend/docker/entrypoint.sh b/backend/entrypoint.sh similarity index 100% rename from backend/docker/entrypoint.sh rename to backend/entrypoint.sh diff --git a/backend/poetry.lock b/backend/poetry.lock index 4d93ced2db..af1690896d 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -14,132 +14,132 @@ files = [ [[package]] name = "aiohttp" -version = "3.13.2" +version = "3.13.3" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155"}, - {file = "aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c"}, - {file = "aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636"}, - {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da"}, - {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725"}, - {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5"}, - {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3"}, - {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802"}, - {file = "aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a"}, - {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204"}, - {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22"}, - {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d"}, - {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f"}, - {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f"}, - {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6"}, - {file = "aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251"}, - {file = "aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514"}, - {file = "aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0"}, - {file = "aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb"}, - {file = "aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9"}, - {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613"}, - {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead"}, - {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780"}, - {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a"}, - {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592"}, - {file = "aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab"}, - {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30"}, - {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40"}, - {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948"}, - {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf"}, - {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782"}, - {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8"}, - {file = "aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec"}, - {file = "aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c"}, - {file = "aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b"}, - {file = "aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc"}, - {file = "aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7"}, - {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb"}, - {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3"}, - {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f"}, - {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6"}, - {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e"}, - {file = "aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7"}, - {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d"}, - {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b"}, - {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8"}, - {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16"}, - {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169"}, - {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248"}, - {file = "aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e"}, - {file = "aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45"}, - {file = "aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be"}, - {file = "aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742"}, - {file = "aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293"}, - {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811"}, - {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a"}, - {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4"}, - {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a"}, - {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e"}, - {file = "aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb"}, - {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded"}, - {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b"}, - {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8"}, - {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04"}, - {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476"}, - {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23"}, - {file = "aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254"}, - {file = "aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a"}, - {file = "aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b"}, - {file = "aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61"}, - {file = "aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4"}, - {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b"}, - {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694"}, - {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906"}, - {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9"}, - {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011"}, - {file = "aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6"}, - {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213"}, - {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49"}, - {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae"}, - {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa"}, - {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4"}, - {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a"}, - {file = "aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940"}, - {file = "aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4"}, - {file = "aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673"}, - {file = "aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd"}, - {file = "aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3"}, - {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf"}, - {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e"}, - {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5"}, - {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad"}, - {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e"}, - {file = "aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61"}, - {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661"}, - {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98"}, - {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693"}, - {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a"}, - {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be"}, - {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c"}, - {file = "aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734"}, - {file = "aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f"}, - {file = "aiohttp-3.13.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7fbdf5ad6084f1940ce88933de34b62358d0f4a0b6ec097362dcd3e5a65a4989"}, - {file = "aiohttp-3.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7c3a50345635a02db61792c85bb86daffac05330f6473d524f1a4e3ef9d0046d"}, - {file = "aiohttp-3.13.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e87dff73f46e969af38ab3f7cb75316a7c944e2e574ff7c933bc01b10def7f5"}, - {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2adebd4577724dcae085665f294cc57c8701ddd4d26140504db622b8d566d7aa"}, - {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e036a3a645fe92309ec34b918394bb377950cbb43039a97edae6c08db64b23e2"}, - {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:23ad365e30108c422d0b4428cf271156dd56790f6dd50d770b8e360e6c5ab2e6"}, - {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1f9b2c2d4b9d958b1f9ae0c984ec1dd6b6689e15c75045be8ccb4011426268ca"}, - {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a92cf4b9bea33e15ecbaa5c59921be0f23222608143d025c989924f7e3e0c07"}, - {file = "aiohttp-3.13.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:070599407f4954021509193404c4ac53153525a19531051661440644728ba9a7"}, - {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:29562998ec66f988d49fb83c9b01694fa927186b781463f376c5845c121e4e0b"}, - {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4dd3db9d0f4ebca1d887d76f7cdbcd1116ac0d05a9221b9dad82c64a62578c4d"}, - {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d7bc4b7f9c4921eba72677cd9fedd2308f4a4ca3e12fab58935295ad9ea98700"}, - {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dacd50501cd017f8cccb328da0c90823511d70d24a323196826d923aad865901"}, - {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8b2f1414f6a1e0683f212ec80e813f4abef94c739fd090b66c9adf9d2a05feac"}, - {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04c3971421576ed24c191f610052bcb2f059e395bc2489dd99e397f9bc466329"}, - {file = "aiohttp-3.13.2-cp39-cp39-win32.whl", hash = "sha256:9f377d0a924e5cc94dc620bc6366fc3e889586a7f18b748901cf016c916e2084"}, - {file = "aiohttp-3.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:9c705601e16c03466cb72011bd1af55d68fa65b045356d8f96c216e5f6db0fa5"}, - {file = "aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca"}, + {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7"}, + {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821"}, + {file = "aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11"}, + {file = "aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd"}, + {file = "aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c"}, + {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b"}, + {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64"}, + {file = "aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29"}, + {file = "aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239"}, + {file = "aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f"}, + {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c"}, + {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168"}, + {file = "aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a"}, + {file = "aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046"}, + {file = "aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57"}, + {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c"}, + {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9"}, + {file = "aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591"}, + {file = "aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf"}, + {file = "aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e"}, + {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808"}, + {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415"}, + {file = "aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43"}, + {file = "aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1"}, + {file = "aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984"}, + {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c"}, + {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592"}, + {file = "aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa"}, + {file = "aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767"}, + {file = "aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344"}, + {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e"}, + {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7"}, + {file = "aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f"}, + {file = "aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1"}, + {file = "aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538"}, + {file = "aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88"}, ] [package.dependencies] @@ -152,7 +152,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] [[package]] name = "aiosignal" @@ -275,18 +275,18 @@ files = [ [[package]] name = "boto3" -version = "1.42.19" +version = "1.42.21" description = "The AWS SDK for Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "boto3-1.42.19-py3-none-any.whl", hash = "sha256:c55b8b303c64931272536813a476f130b90ea7041d7b79c154d89cf1c18256b4"}, - {file = "boto3-1.42.19.tar.gz", hash = "sha256:5933696a28bf8eb62fc54e4de5583f78a0efef59c8164ee1850436aa22f53aa7"}, + {file = "boto3-1.42.21-py3-none-any.whl", hash = "sha256:1885f252d715a5810bb4e0c5bbebfa8e9018b025febf5be3d58540626e7b43d2"}, + {file = "boto3-1.42.21.tar.gz", hash = "sha256:9b92943d253bc837323079fe88460e741cb2eb80abaebcb558b2446bdb4049d6"}, ] [package.dependencies] -botocore = ">=1.42.19,<1.43.0" +botocore = ">=1.42.21,<1.43.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.16.0,<0.17.0" @@ -295,14 +295,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.42.19" +version = "1.42.21" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "botocore-1.42.19-py3-none-any.whl", hash = "sha256:30c276e0a96d822826d74e961089b9af16b274ac7ddcf7dcf6440bc90d856d88"}, - {file = "botocore-1.42.19.tar.gz", hash = "sha256:8d38f30de983720303e95951380a2c9ac515159636ee6b5ba4227d65f14551a4"}, + {file = "botocore-1.42.21-py3-none-any.whl", hash = "sha256:6b59973a3ba8c3cfd5123f2656fef2339beee9f6483b8bc12bb00c5453ea2c6d"}, + {file = "botocore-1.42.21.tar.gz", hash = "sha256:db8f99d186156da42feb4fd2098017383d9b155097290cc53da7258f6e652c39"}, ] [package.dependencies] @@ -315,14 +315,14 @@ crt = ["awscrt (==0.29.2)"] [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.1.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, - {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, + {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, + {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, ] [[package]] @@ -917,14 +917,14 @@ django = ">=4.2" [[package]] name = "django-ninja" -version = "1.5.1" +version = "1.5.2" description = "Django Ninja - Fast Django REST framework" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "django_ninja-1.5.1-py3-none-any.whl", hash = "sha256:135aaa1117dce8dfd7a1e80b4487a8cccee3a4182c3c8b562d08ea94e4d2cbdf"}, - {file = "django_ninja-1.5.1.tar.gz", hash = "sha256:6acda68a64d60934c6fdccb4d97c3ac7f02cfefd78a5d87ae053effe081b17c7"}, + {file = "django_ninja-1.5.2-py3-none-any.whl", hash = "sha256:5faf09cf6e64298e822305c3f4b933608b4fce30643e36cc58f67660f20f4cac"}, + {file = "django_ninja-1.5.2.tar.gz", hash = "sha256:1554bebc28e9bbc8412f49bae4698937781b54d828c84af30c40dfced407b8cb"}, ] [package.dependencies] @@ -1092,14 +1092,14 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "filelock" -version = "3.20.1" +version = "3.20.2" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"}, - {file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"}, + {file = "filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8"}, + {file = "filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64"}, ] [[package]] @@ -1663,19 +1663,16 @@ six = ">=1.13.0" [[package]] name = "json5" -version = "0.12.1" +version = "0.13.0" description = "A Python implementation of the JSON5 data format." optional = false python-versions = ">=3.8.0" groups = ["dev"] files = [ - {file = "json5-0.12.1-py3-none-any.whl", hash = "sha256:d9c9b3bc34a5f54d43c35e11ef7cb87d8bdd098c6ace87117a7b7e83e705c1d5"}, - {file = "json5-0.12.1.tar.gz", hash = "sha256:b2743e77b3242f8d03c143dd975a6ec7c52e2f2afe76ed934e53503dd4ad4990"}, + {file = "json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc"}, + {file = "json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf"}, ] -[package.extras] -dev = ["build (==1.2.2.post1)", "coverage (==7.5.4) ; python_version < \"3.9\"", "coverage (==7.8.0) ; python_version >= \"3.9\"", "mypy (==1.14.1) ; python_version < \"3.9\"", "mypy (==1.15.0) ; python_version >= \"3.9\"", "pip (==25.0.1)", "pylint (==3.2.7) ; python_version < \"3.9\"", "pylint (==3.3.6) ; python_version >= \"3.9\"", "ruff (==0.11.2)", "twine (==6.1.0)", "uv (==0.6.11)"] - [[package]] name = "jsonpatch" version = "1.33" @@ -1912,21 +1909,21 @@ orjson = ">=3.10.1" [[package]] name = "langsmith" -version = "0.5.2" +version = "0.6.0" description = "Client library to connect to the LangSmith Observability and Evaluation Platform." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "langsmith-0.5.2-py3-none-any.whl", hash = "sha256:42f8b853a18dd4d5f7fa38c8ff29e38da065a727022da410d91b3e13819aacc1"}, - {file = "langsmith-0.5.2.tar.gz", hash = "sha256:a6186d555ba59732b1b10e2ba6fe34ee0b3c1bf3a7fb8d7be0dec367ac3b75f1"}, + {file = "langsmith-0.6.0-py3-none-any.whl", hash = "sha256:f7570175aed705b1f4c4dae724c07980a737b8b565252444d11394dda9931e8c"}, + {file = "langsmith-0.6.0.tar.gz", hash = "sha256:b60f1785aed4dac5e01f24db01aa18fa1af258bad4531e045e739438daa3f8c2"}, ] [package.dependencies] httpx = ">=0.23.0,<1" orjson = {version = ">=3.9.14", markers = "platform_python_implementation != \"PyPy\""} packaging = ">=23.2" -pydantic = ">=1,<3" +pydantic = ">=2,<3" requests = ">=2.0.0" requests-toolbelt = ">=1.0.0" uuid-utils = ">=0.12.0,<1.0" @@ -2734,103 +2731,103 @@ numpy = "*" [[package]] name = "pillow" -version = "12.0.0" +version = "12.1.0" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b"}, - {file = "pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782"}, - {file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10"}, - {file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa"}, - {file = "pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275"}, - {file = "pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d"}, - {file = "pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7"}, - {file = "pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc"}, - {file = "pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227"}, - {file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b"}, - {file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e"}, - {file = "pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739"}, - {file = "pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e"}, - {file = "pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d"}, - {file = "pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371"}, - {file = "pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8"}, - {file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79"}, - {file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba"}, - {file = "pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0"}, - {file = "pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a"}, - {file = "pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad"}, - {file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643"}, - {file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4"}, - {file = "pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399"}, - {file = "pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5"}, - {file = "pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344"}, - {file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27"}, - {file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79"}, - {file = "pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098"}, - {file = "pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905"}, - {file = "pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a"}, - {file = "pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3"}, - {file = "pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe"}, - {file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee"}, - {file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef"}, - {file = "pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9"}, - {file = "pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b"}, - {file = "pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47"}, - {file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9"}, - {file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2"}, - {file = "pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a"}, - {file = "pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b"}, - {file = "pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e"}, - {file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9"}, - {file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab"}, - {file = "pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b"}, - {file = "pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b"}, - {file = "pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0"}, - {file = "pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6"}, - {file = "pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925"}, - {file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8"}, - {file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4"}, - {file = "pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52"}, - {file = "pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a"}, - {file = "pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5"}, - {file = "pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353"}, + {file = "pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd"}, + {file = "pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0"}, + {file = "pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8"}, + {file = "pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1"}, + {file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda"}, + {file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7"}, + {file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a"}, + {file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef"}, + {file = "pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09"}, + {file = "pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91"}, + {file = "pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea"}, + {file = "pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3"}, + {file = "pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0"}, + {file = "pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451"}, + {file = "pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e"}, + {file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84"}, + {file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0"}, + {file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b"}, + {file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18"}, + {file = "pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64"}, + {file = "pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75"}, + {file = "pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304"}, + {file = "pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b"}, + {file = "pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551"}, + {file = "pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208"}, + {file = "pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5"}, + {file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661"}, + {file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17"}, + {file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670"}, + {file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616"}, + {file = "pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7"}, + {file = "pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d"}, + {file = "pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c"}, + {file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1"}, + {file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179"}, + {file = "pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0"}, + {file = "pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587"}, + {file = "pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac"}, + {file = "pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b"}, + {file = "pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea"}, + {file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c"}, + {file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc"}, + {file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644"}, + {file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c"}, + {file = "pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171"}, + {file = "pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a"}, + {file = "pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45"}, + {file = "pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d"}, + {file = "pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0"}, + {file = "pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554"}, + {file = "pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e"}, + {file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82"}, + {file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4"}, + {file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0"}, + {file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b"}, + {file = "pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65"}, + {file = "pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0"}, + {file = "pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8"}, + {file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91"}, + {file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796"}, + {file = "pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd"}, + {file = "pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13"}, + {file = "pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e"}, + {file = "pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643"}, + {file = "pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5"}, + {file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de"}, + {file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9"}, + {file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a"}, + {file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a"}, + {file = "pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030"}, + {file = "pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94"}, + {file = "pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4"}, + {file = "pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2"}, + {file = "pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61"}, + {file = "pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51"}, + {file = "pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc"}, + {file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14"}, + {file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8"}, + {file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924"}, + {file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef"}, + {file = "pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988"}, + {file = "pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6"}, + {file = "pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831"}, + {file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377"}, + {file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72"}, + {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c"}, + {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd"}, + {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc"}, + {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a"}, + {file = "pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19"}, + {file = "pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9"}, ] [package.extras] @@ -3039,8 +3036,10 @@ files = [ {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d"}, {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2"}, {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14"}, {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd"}, {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152"}, {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e"}, {file = "psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10"}, @@ -3048,8 +3047,10 @@ files = [ {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4"}, @@ -3057,8 +3058,10 @@ files = [ {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c"}, @@ -3066,8 +3069,10 @@ files = [ {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1"}, @@ -3075,8 +3080,10 @@ files = [ {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c"}, @@ -3084,8 +3091,10 @@ files = [ {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02"}, ] @@ -3370,6 +3379,7 @@ files = [ {file = "pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa"}, {file = "pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0"}, {file = "pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c"}, + {file = "pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c"}, ] [package.dependencies] @@ -4249,12 +4259,14 @@ optional = false python-versions = ">=3.7" groups = ["main"] files = [ + {file = "sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85"}, {file = "sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4"}, {file = "sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0"}, {file = "sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0"}, {file = "sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826"}, {file = "sqlalchemy-2.0.45-cp310-cp310-win32.whl", hash = "sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a"}, {file = "sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl", hash = "sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7"}, + {file = "sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56"}, {file = "sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b"}, {file = "sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac"}, {file = "sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606"}, @@ -4283,12 +4295,14 @@ files = [ {file = "sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177"}, {file = "sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b"}, {file = "sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b"}, + {file = "sqlalchemy-2.0.45-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5964f832431b7cdfaaa22a660b4c7eb1dfcd6ed41375f67fd3e3440fd95cb3cc"}, {file = "sqlalchemy-2.0.45-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee580ab50e748208754ae8980cec79ec205983d8cf8b3f7c39067f3d9f2c8e22"}, {file = "sqlalchemy-2.0.45-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13e27397a7810163440c6bfed6b3fe46f1bfb2486eb540315a819abd2c004128"}, {file = "sqlalchemy-2.0.45-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ed3635353e55d28e7f4a95c8eda98a5cdc0a0b40b528433fbd41a9ae88f55b3d"}, {file = "sqlalchemy-2.0.45-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:db6834900338fb13a9123307f0c2cbb1f890a8656fcd5e5448ae3ad5bbe8d312"}, {file = "sqlalchemy-2.0.45-cp38-cp38-win32.whl", hash = "sha256:1d8b4a7a8c9b537509d56d5cd10ecdcfbb95912d72480c8861524efecc6a3fff"}, {file = "sqlalchemy-2.0.45-cp38-cp38-win_amd64.whl", hash = "sha256:ebd300afd2b62679203435f596b2601adafe546cb7282d5a0cd3ed99e423720f"}, + {file = "sqlalchemy-2.0.45-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d29b2b99d527dbc66dd87c3c3248a5dd789d974a507f4653c969999fc7c1191b"}, {file = "sqlalchemy-2.0.45-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59a8b8bd9c6bedf81ad07c8bd5543eedca55fe9b8780b2b628d495ba55f8db1e"}, {file = "sqlalchemy-2.0.45-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd93c6f5d65f254ceabe97548c709e073d6da9883343adaa51bf1a913ce93f8e"}, {file = "sqlalchemy-2.0.45-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d0beadc2535157070c9c17ecf25ecec31e13c229a8f69196d7590bde8082bf1"}, @@ -4346,14 +4360,14 @@ doc = ["sphinx"] [[package]] name = "strawberry-graphql" -version = "0.288.1" +version = "0.288.2" description = "A library for creating GraphQL APIs" optional = false python-versions = "<4.0,>=3.10" groups = ["main"] files = [ - {file = "strawberry_graphql-0.288.1-py3-none-any.whl", hash = "sha256:6ae66b9a276b1c8136d88995a8f334b09563d9c4cfb1144d7fd4ba4c003d4fbf"}, - {file = "strawberry_graphql-0.288.1.tar.gz", hash = "sha256:d779932d7d83a1e64f89eea7c4413dca1305af70ff4298d48782a1f04db9334c"}, + {file = "strawberry_graphql-0.288.2-py3-none-any.whl", hash = "sha256:ad72d7904582db333158568751bb6186a872380a8cc6671159d011d279382542"}, + {file = "strawberry_graphql-0.288.2.tar.gz", hash = "sha256:853dbab407e3f5099f3a27dbf37786535894a0fbf150df5dde145fc290db607e"}, ] [package.dependencies] @@ -4385,14 +4399,14 @@ sanic = ["sanic (>=20.12.2)"] [[package]] name = "strawberry-graphql-django" -version = "0.72.0" +version = "0.73.0" description = "Strawberry GraphQL Django extension" optional = false python-versions = "<4.0,>=3.10" groups = ["main"] files = [ - {file = "strawberry_graphql_django-0.72.0-py3-none-any.whl", hash = "sha256:b8e9e408d46142bf34635179ba7db16b396f6afb0d320feff37eb195bb592368"}, - {file = "strawberry_graphql_django-0.72.0.tar.gz", hash = "sha256:5c7c221b8e9fbb80a7f99d489407e8ad7eb872360ebf8873242be9d151c5f685"}, + {file = "strawberry_graphql_django-0.73.0-py3-none-any.whl", hash = "sha256:f85cd9fc4a03ca4a430ce787c84cfc5e79e0b075e83d89ed8e9eaf76d035ce13"}, + {file = "strawberry_graphql_django-0.73.0.tar.gz", hash = "sha256:39f7205ca28a29763e3260548f7739ad6729a717d3f64b851208f57940c6360b"}, ] [package.dependencies] @@ -5009,4 +5023,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "42aef0a81865318928a5b0a1eba793c1aa4d1e0fa26fbd105bd71202cd6dd02f" +content-hash = "5f769e9589f6acacb18be0498b8880bc9f33dfb2c98033ff92a4e0f69b8f1f7d" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 2e18087db9..9a6ed8df4c 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -47,7 +47,7 @@ sentry-sdk = { extras = [ "django" ], version = "^2.20.0" } slack-bolt = "^1.22.0" slack-sdk = "^3.37.0" strawberry-graphql = { extras = [ "django" ], version = "^0.288.1" } -strawberry-graphql-django = "^0.72.0" +strawberry-graphql-django = "^0.73.0" thefuzz = "^0.22.1" pyparsing = "^3.2.3" @@ -90,9 +90,10 @@ lint.per-file-ignores."**/__init__.py" = [ "F401", # https://docs.astral.sh/ruff/rules/unused-import/ ] lint.per-file-ignores."**/management/commands/*.py" = [ - "D101", # https://docs.astral.sh/ruff/rules/undocumented-public-class/ - "D102", # https://docs.astral.sh/ruff/rules/undocumented-public-method/ - "T201", # https://docs.astral.sh/ruff/rules/print/ + "D101", # https://docs.astral.sh/ruff/rules/undocumented-public-class/ + "D102", # https://docs.astral.sh/ruff/rules/undocumented-public-method/ + "SLF001", # https://docs.astral.sh/ruff/rules/private-member-access/ + "T201", # https://docs.astral.sh/ruff/rules/print/ ] lint.per-file-ignores."**/management/commands/dump_data.py" = [ "S603", # https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true/, diff --git a/backend/tests/apps/api/rest/v0/chapter_test.py b/backend/tests/apps/api/rest/v0/chapter_test.py index 0a29d4a53f..cc2969edf9 100644 --- a/backend/tests/apps/api/rest/v0/chapter_test.py +++ b/backend/tests/apps/api/rest/v0/chapter_test.py @@ -31,11 +31,24 @@ ], ) def test_chapter_serializer_validation(chapter_data): + class MockMember: + def __init__(self, login): + self.login = login + + class MockEntityMember: + def __init__(self, name, login=None): + self.member = MockMember(login) if login else None + self.member_name = name + class MockChapter: def __init__(self, data): for key, value in data.items(): setattr(self, key, value) self.nest_key = data["key"] + self.entity_leaders = [ + MockEntityMember("Alice", "alice"), + MockEntityMember("Bob"), + ] chapter = ChapterDetail.from_orm(MockChapter(chapter_data)) @@ -44,6 +57,11 @@ def __init__(self, data): assert chapter.key == chapter_data["key"] assert chapter.latitude == chapter_data["latitude"] assert chapter.longitude == chapter_data["longitude"] + assert len(chapter.leaders) == 2 + assert chapter.leaders[0].key == "alice" + assert chapter.leaders[0].name == "Alice" + assert chapter.leaders[1].key is None + assert chapter.leaders[1].name == "Bob" assert chapter.name == chapter_data["name"] assert chapter.region == chapter_data["region"] assert chapter.updated_at == datetime.fromisoformat(chapter_data["updated_at"]) diff --git a/backend/tests/apps/api/rest/v0/project_test.py b/backend/tests/apps/api/rest/v0/project_test.py index 112b41fb5c..e197ab05f7 100644 --- a/backend/tests/apps/api/rest/v0/project_test.py +++ b/backend/tests/apps/api/rest/v0/project_test.py @@ -27,17 +27,35 @@ ], ) def test_project_serializer_validation(project_data): + class MockMember: + def __init__(self, login): + self.login = login + + class MockEntityMember: + def __init__(self, name, login=None): + self.member = MockMember(login) if login else None + self.member_name = name + class MockProject: def __init__(self, data): for key, value in data.items(): setattr(self, key, value) self.nest_key = data["key"] + self.entity_leaders = [ + MockEntityMember("Alice", "alice"), + MockEntityMember("Bob"), + ] project = ProjectDetail.from_orm(MockProject(project_data)) assert project.created_at == datetime.fromisoformat(project_data["created_at"]) assert project.description == project_data["description"] assert project.key == project_data["key"] + assert len(project.leaders) == 2 + assert project.leaders[0].key == "alice" + assert project.leaders[0].name == "Alice" + assert project.leaders[1].key is None + assert project.leaders[1].name == "Bob" assert project.level == project_data["level"] assert project.name == project_data["name"] assert project.updated_at == datetime.fromisoformat(project_data["updated_at"]) diff --git a/backend/tests/apps/owasp/api/internal/nodes/chapter_test.py b/backend/tests/apps/owasp/api/internal/nodes/chapter_test.py new file mode 100644 index 0000000000..f05abd9b80 --- /dev/null +++ b/backend/tests/apps/owasp/api/internal/nodes/chapter_test.py @@ -0,0 +1,92 @@ +"""Test cases for ChapterNode.""" + +from apps.owasp.api.internal.nodes.chapter import ChapterNode + + +class TestChapterNode: + def test_chapter_node_inheritance(self): + assert hasattr(ChapterNode, "__strawberry_definition__") + + def test_meta_configuration(self): + field_names = {field.name for field in ChapterNode.__strawberry_definition__.fields} + expected_field_names = { + "contribution_data", + "contribution_stats", + "country", + "created_at", + "is_active", + "name", + "region", + "summary", + "key", + "geo_location", + "suggested_location", + "meetup_group", + "postal_code", + "tags", + } + assert expected_field_names.issubset(field_names) + + def _get_field_by_name(self, name): + return next( + (f for f in ChapterNode.__strawberry_definition__.fields if f.name == name), None + ) + + def test_resolve_key(self): + field = self._get_field_by_name("key") + assert field is not None + assert field.type is str + + def test_resolve_country(self): + field = self._get_field_by_name("country") + assert field is not None + assert field.type is str + + def test_resolve_region(self): + field = self._get_field_by_name("region") + assert field is not None + assert field.type is str + + def test_resolve_is_active(self): + field = self._get_field_by_name("is_active") + assert field is not None + assert field.type is bool + + def test_resolve_contribution_data(self): + field = self._get_field_by_name("contribution_data") + assert field is not None + assert field.type.__class__.__name__ == "NewType" + + def test_resolve_contribution_stats(self): + field = self._get_field_by_name("contribution_stats") + assert field is not None + assert field.type.__class__.__name__ == "StrawberryOptional" + + def test_contribution_stats_transforms_snake_case_to_camel_case(self): + """Test that contribution_stats resolver transforms snake_case keys to camelCase.""" + from unittest.mock import Mock + + mock_chapter = Mock() + mock_chapter.contribution_stats = { + "commits": 75, + "pull_requests": 30, + "issues": 15, + "releases": 5, + "total": 125, + } + + instance = type("BoundNode", (), {})() + instance.contribution_stats = mock_chapter.contribution_stats + + field = self._get_field_by_name("contribution_stats") + resolver = field.base_resolver.wrapped_func + + result = resolver(instance) + + assert result is not None + assert result["commits"] == 75 + assert result["pullRequests"] == 30 + assert result["issues"] == 15 + assert result["releases"] == 5 + assert result["total"] == 125 + assert "pull_requests" not in result diff --git a/backend/tests/apps/owasp/api/internal/nodes/project_test.py b/backend/tests/apps/owasp/api/internal/nodes/project_test.py index 03cff7115b..d47cb64287 100644 --- a/backend/tests/apps/owasp/api/internal/nodes/project_test.py +++ b/backend/tests/apps/owasp/api/internal/nodes/project_test.py @@ -16,26 +16,28 @@ def test_project_node_inheritance(self): def test_meta_configuration(self): field_names = {field.name for field in ProjectNode.__strawberry_definition__.fields} expected_field_names = { + "contribution_data", + "contribution_stats", "contributors_count", "created_at", "forks_count", "is_active", - "level", - "name", - "open_issues_count", - "stars_count", - "summary", - "type", "issues_count", "key", "languages", + "level", + "name", + "open_issues_count", "recent_issues", "recent_milestones", "recent_pull_requests", "recent_releases", - "repositories", "repositories_count", + "repositories", + "stars_count", + "summary", "topics", + "type", } assert expected_field_names.issubset(field_names) @@ -103,3 +105,42 @@ def test_resolve_topics(self): field = self._get_field_by_name("topics") assert field is not None assert field.type == list[str] + + def test_resolve_contribution_stats(self): + field = self._get_field_by_name("contribution_stats") + assert field is not None + assert field.type.__class__.__name__ == "StrawberryOptional" + + def test_resolve_contribution_data(self): + field = self._get_field_by_name("contribution_data") + assert field is not None + assert field.type.__class__.__name__ == "StrawberryOptional" + + def test_contribution_stats_transforms_snake_case_to_camel_case(self): + """Test that contribution_stats resolver transforms snake_case keys to camelCase.""" + from unittest.mock import Mock + + mock_project = Mock() + mock_project.contribution_stats = { + "commits": 100, + "issues": 25, + "pull_requests": 50, + "releases": 10, + "total": 185, + } + + instance = type("BoundNode", (), {})() + instance.contribution_stats = mock_project.contribution_stats + + field = self._get_field_by_name("contribution_stats") + resolver = field.base_resolver.wrapped_func + + result = resolver(instance) + + assert result is not None + assert result["commits"] == 100 + assert result["pullRequests"] == 50 + assert result["issues"] == 25 + assert result["releases"] == 10 + assert result["total"] == 185 + assert "pull_requests" not in result diff --git a/backend/tests/apps/owasp/management/commands/owasp_aggregate_contributions_test.py b/backend/tests/apps/owasp/management/commands/owasp_aggregate_contributions_test.py new file mode 100644 index 0000000000..e988e6d061 --- /dev/null +++ b/backend/tests/apps/owasp/management/commands/owasp_aggregate_contributions_test.py @@ -0,0 +1,444 @@ +"""Test cases for owasp_aggregate_contributions management command.""" + +from datetime import UTC, datetime, timedelta +from unittest import mock + +import pytest + +from apps.owasp.management.commands.owasp_aggregate_contributions import Command +from apps.owasp.models import Chapter, Project + + +class MockQuerySet: + """Mock QuerySet that supports slicing and iteration without database access.""" + + def __init__(self, items): + self._items = items + + def __iter__(self): + """Return iterator over items.""" + return iter(self._items) + + def __getitem__(self, key): + """Get item by key or slice.""" + if isinstance(key, slice): + return MockQuerySet(self._items[key]) + return self._items[key] + + def filter(self, **kwargs): + # Return self to support filter chaining + return self + + def order_by(self, *_fields): + """Mock order_by method.""" + return self + + def select_related(self, *_): + """Mock select_related method.""" + return self + + def prefetch_related(self, *_): + """Mock prefetch_related method.""" + return self + + def count(self): + """Return count of items.""" + return len(self._items) + + def __len__(self): + """Return length of items.""" + return len(self._items) + + +class TestOwaspAggregateContributions: + @pytest.fixture + def command(self): + return Command() + + @pytest.fixture + def mock_chapter(self): + chapter = mock.Mock(spec=Chapter) + chapter.key = "www-chapter-test" + chapter.name = "Test Chapter" + chapter.owasp_repository = mock.Mock() + chapter.owasp_repository.id = 1 + # Fix Django ORM compatibility. + chapter.owasp_repository.resolve_expression = mock.Mock( + return_value=chapter.owasp_repository + ) + chapter.owasp_repository.get_source_expressions = mock.Mock(return_value=[]) + return chapter + + @pytest.fixture + def mock_project(self): + project = mock.Mock(spec=Project) + project.key = "www-project-test" + project.name = "Test Project" + project.owasp_repository = mock.Mock() + project.owasp_repository.id = 1 + # Fix Django ORM compatibility. + project.owasp_repository.resolve_expression = mock.Mock( + return_value=project.owasp_repository + ) + project.owasp_repository.get_source_expressions = mock.Mock(return_value=[]) + + # Mock additional repositories. + additional_repo1 = mock.Mock(id=2) + additional_repo1.resolve_expression = mock.Mock(return_value=additional_repo1) + additional_repo1.get_source_expressions = mock.Mock(return_value=[]) + + additional_repo2 = mock.Mock(id=3) + additional_repo2.resolve_expression = mock.Mock(return_value=additional_repo2) + additional_repo2.get_source_expressions = mock.Mock(return_value=[]) + + project.repositories.all.return_value = [additional_repo1, additional_repo2] + return project + + def test_aggregate_contribution_dates_helper(self, command): + """Test the helper method that aggregates dates.""" + contribution_map = {} + + # Create mock queryset with dates. + mock_dates = [ + datetime(2024, 11, 16, 10, 0, 0, tzinfo=UTC), + datetime(2024, 11, 16, 14, 0, 0, tzinfo=UTC), + datetime(2024, 11, 17, 9, 0, 0, tzinfo=UTC), + None, + ] + + mock_queryset = mock.Mock() + mock_queryset.values_list.return_value = mock_dates + + command._aggregate_contribution_dates( + mock_queryset, + "created_at", + contribution_map, + ) + + assert contribution_map == { + "2024-11-16": 2, + "2024-11-17": 1, + } + + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Commit") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Issue") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.PullRequest") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Release") + def test_aggregate_chapter_contributions( + self, + mock_release, + mock_pr, + mock_issue, + mock_commit, + command, + mock_chapter, + ): + """Test aggregating contributions for a chapter.""" + start_date = datetime.now(tz=UTC) - timedelta(days=365) + + # Mock querysets. + mock_commit.objects.filter.return_value.values_list.return_value = [ + datetime(2024, 11, 16, 10, 0, 0, tzinfo=UTC), + ] + mock_issue.objects.filter.return_value.values_list.return_value = [ + datetime(2024, 11, 16, 11, 0, 0, tzinfo=UTC), + ] + mock_pr.objects.filter.return_value.values_list.return_value = [ + datetime(2024, 11, 17, 10, 0, 0, tzinfo=UTC), + ] + mock_release.objects.filter.return_value.values_list.return_value = [ + datetime(2024, 11, 17, 12, 0, 0, tzinfo=UTC), + ] + + result = command.aggregate_contributions(mock_chapter, start_date) + + assert result == { + "2024-11-16": 2, + "2024-11-17": 2, + } + + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Commit") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Issue") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.PullRequest") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Release") + def test_aggregate_project_contributions( + self, + mock_release, + mock_pr, + mock_issue, + mock_commit, + command, + mock_project, + ): + """Test aggregating contributions for a project.""" + start_date = datetime.now(tz=UTC) - timedelta(days=365) + + # Mock querysets. + mock_commit.objects.filter.return_value.values_list.return_value = [ + datetime(2024, 11, 16, 10, 0, 0, tzinfo=UTC), + datetime(2024, 11, 16, 14, 0, 0, tzinfo=UTC), + ] + mock_issue.objects.filter.return_value.values_list.return_value = [ + datetime(2024, 11, 17, 11, 0, 0, tzinfo=UTC), + ] + mock_pr.objects.filter.return_value.values_list.return_value = [ + datetime(2024, 11, 18, 10, 0, 0, tzinfo=UTC), + ] + mock_release.objects.filter.return_value.values_list.return_value = [ + datetime(2024, 11, 18, 12, 0, 0, tzinfo=UTC), + ] + + result = command.aggregate_contributions(mock_project, start_date) + + assert result == { + "2024-11-16": 2, + "2024-11-17": 1, + "2024-11-18": 2, + } + + def test_aggregate_chapter_without_repository(self, command, mock_chapter): + """Test that chapters without repositories return empty map.""" + mock_chapter.owasp_repository = None + start_date = datetime.now(tz=UTC) - timedelta(days=365) + + result = command.aggregate_contributions(mock_chapter, start_date) + + assert result == {} + + def test_aggregate_project_without_repositories(self, command, mock_project): + """Test that projects without repositories return empty map.""" + mock_project.owasp_repository = None + mock_project.repositories.all.return_value = [] + start_date = datetime.now(tz=UTC) - timedelta(days=365) + + result = command.aggregate_contributions(mock_project, start_date) + + assert result == {} + + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Chapter") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Commit") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Issue") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.PullRequest") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Release") + def test_handle_chapters_only( + self, + mock_release, + mock_pr, + mock_issue, + mock_commit, + mock_chapter_model, + command, + mock_chapter, + ): + """Test command execution for chapters only.""" + mock_chapter_model.objects.filter.return_value = MockQuerySet([mock_chapter]) + mock_chapter_model.bulk_save = mock.Mock() + + # Mock ORM queries to return counts. + mock_commit.objects.filter.return_value.count.return_value = 5 + mock_issue.objects.filter.return_value.count.return_value = 3 + mock_pr.objects.filter.return_value.count.return_value = 2 + mock_release.objects.filter.return_value.count.return_value = 1 + + with mock.patch.object( + command, + "aggregate_contributions", + return_value={"2024-11-16": 5}, + ): + command.handle(entity_type="chapter", days=365, offset=0) + + assert mock_chapter.contribution_data == {"2024-11-16": 5} + assert mock_chapter_model.bulk_save.called + + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Project") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Commit") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Issue") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.PullRequest") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Release") + def test_handle_projects_only( + self, + mock_release, + mock_pr, + mock_issue, + mock_commit, + mock_project_model, + command, + mock_project, + ): + """Test command execution for projects only.""" + mock_project_model.objects.filter.return_value = MockQuerySet([mock_project]) + mock_project_model.bulk_save = mock.Mock() + + # Mock ORM queries to return counts. + mock_commit.objects.filter.return_value.count.return_value = 8 + mock_issue.objects.filter.return_value.count.return_value = 4 + mock_pr.objects.filter.return_value.count.return_value = 3 + mock_release.objects.filter.return_value.count.return_value = 2 + + with mock.patch.object( + command, + "aggregate_contributions", + return_value={"2024-11-16": 10}, + ): + command.handle(entity_type="project", days=365, offset=0) + + assert mock_project.contribution_data == {"2024-11-16": 10} + assert mock_project.contribution_stats is not None + assert "commits" in mock_project.contribution_stats + assert mock_project_model.bulk_save.called + + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Chapter") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Project") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Commit") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Issue") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.PullRequest") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Release") + def test_handle_both_entities( + self, + mock_release, + mock_pr, + mock_issue, + mock_commit, + mock_project_model, + mock_chapter_model, + command, + mock_chapter, + mock_project, + ): + """Test command execution for both chapters and projects (run separately).""" + mock_chapter_model.objects.filter.return_value = MockQuerySet([mock_chapter]) + mock_project_model.objects.filter.return_value = MockQuerySet([mock_project]) + mock_chapter_model.bulk_save = mock.Mock() + mock_project_model.bulk_save = mock.Mock() + + # Mock ORM queries to return counts. + mock_commit.objects.filter.return_value.count.return_value = 5 + mock_issue.objects.filter.return_value.count.return_value = 3 + mock_pr.objects.filter.return_value.count.return_value = 2 + mock_release.objects.filter.return_value.count.return_value = 1 + + with mock.patch.object( + command, + "aggregate_contributions", + return_value={"2024-11-16": 5}, + ): + command.handle(entity_type="chapter", days=365, offset=0) + command.handle(entity_type="project", days=365, offset=0) + + assert mock_chapter_model.bulk_save.called + assert mock_project_model.bulk_save.called + + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Chapter") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Commit") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Issue") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.PullRequest") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Release") + def test_handle_with_specific_key( + self, + mock_release, + mock_pr, + mock_issue, + mock_commit, + mock_chapter_model, + command, + mock_chapter, + ): + """Test command execution with a specific entity key.""" + mock_chapter_model.objects.filter.return_value = MockQuerySet([mock_chapter]) + mock_chapter_model.bulk_save = mock.Mock() + + # Mock ORM queries to return counts. + mock_commit.objects.filter.return_value.count.return_value = 3 + mock_issue.objects.filter.return_value.count.return_value = 2 + mock_pr.objects.filter.return_value.count.return_value = 1 + mock_release.objects.filter.return_value.count.return_value = 1 + + with mock.patch.object( + command, + "aggregate_contributions", + return_value={"2024-11-16": 3}, + ): + command.handle(entity_type="chapter", key="www-chapter-test", days=365, offset=0) + + # Verify filter was called with the specific key. + mock_chapter_model.objects.filter.assert_called() + + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Chapter") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Commit") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Issue") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.PullRequest") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Release") + def test_handle_with_offset( + self, + mock_release, + mock_pr, + mock_issue, + mock_commit, + mock_chapter_model, + command, + mock_chapter, + ): + """Test command execution with offset parameter.""" + chapters = [mock_chapter, mock_chapter, mock_chapter] + mock_chapter_model.objects.filter.return_value = MockQuerySet(chapters) + mock_chapter_model.bulk_save = mock.Mock() + + # Mock ORM queries to return counts. + mock_commit.objects.filter.return_value.count.return_value = 1 + mock_issue.objects.filter.return_value.count.return_value = 1 + mock_pr.objects.filter.return_value.count.return_value = 1 + mock_release.objects.filter.return_value.count.return_value = 0 + + with mock.patch.object( + command, + "aggregate_contributions", + return_value={"2024-11-16": 1}, + ) as mock_aggregate: + command.handle(entity_type="chapter", offset=2, days=365) + + # Verify that offset was applied correctly. + assert mock_aggregate.call_count == 1, ( + "Expected aggregate to be called once for 1 remaining chapter after offset" + ) + mock_aggregate.assert_called_once() + mock_chapter_model.bulk_save.assert_called_once() + + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Chapter") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Commit") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Issue") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.PullRequest") + @mock.patch("apps.owasp.management.commands.owasp_aggregate_contributions.Release") + def test_handle_custom_days( + self, + mock_release, + mock_pr, + mock_issue, + mock_commit, + mock_chapter_model, + command, + mock_chapter, + ): + """Test command execution with custom days parameter.""" + mock_chapter_model.objects.filter.return_value = MockQuerySet([mock_chapter]) + mock_chapter_model.bulk_save = mock.Mock() + + mock_commit.objects.filter.return_value.count.return_value = 0 + mock_issue.objects.filter.return_value.count.return_value = 0 + mock_pr.objects.filter.return_value.count.return_value = 0 + mock_release.objects.filter.return_value.count.return_value = 0 + + with mock.patch.object( + command, + "aggregate_contributions", + return_value={}, + ) as mock_aggregate: + command.handle(entity_type="chapter", days=90, offset=0) + + # Verify aggregate was called with correct start_date. + assert mock_aggregate.called + call_args = mock_aggregate.call_args[0] + start_date = call_args[1] + expected_start = datetime.now(tz=UTC) - timedelta(days=90) + + # Allow 1 second tolerance for test execution time. + assert abs((expected_start - start_date).total_seconds()) < 1 diff --git a/cspell/cspell.json b/cspell/cspell.json index 6345c970aa..fb643bc8d0 100644 --- a/cspell/cspell.json +++ b/cspell/cspell.json @@ -37,8 +37,9 @@ "win32" ], "enabled": true, - "files": ["**/*"], + "files": ["**/*", ".github/**/*"], "ignorePaths": [ + ".github/workflows/check-pr-issue-skip-usernames.txt", "backend/**/migrations/*.py", "backend/data/project-custom-tags/*.json", "backend/static/**", diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt index b5a3fcf8ba..3cada2edf2 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -43,6 +43,7 @@ algoliasearch ansa apexcharts apk +aquasecurity arithmatex arkid15r askowasp @@ -60,6 +61,7 @@ cva defectdojo demojize dismissable +dockerhub dsn env facebookexternalhit @@ -67,6 +69,7 @@ gamesec geocoders geoloc geopy +gha graphiql graphqler gunicorn @@ -104,6 +107,7 @@ ngx noinput nosniff nspname +numfmt openblas openstreetmap owasppcitoolkit @@ -134,6 +138,7 @@ slideshare speakerdeck superfences tiktok +trivyignores tsc unassigning unhover @@ -146,5 +151,6 @@ xdg xdist xoxb xsser +zaproxy zsc éàëîôû diff --git a/cspell/package.json b/cspell/package.json index f9a94bd77f..2dbf68ef56 100644 --- a/cspell/package.json +++ b/cspell/package.json @@ -2,9 +2,9 @@ "devDependencies": { "@cspell/dict-aws": "^4.0.17", "@cspell/dict-data-science": "^2.0.13", - "@cspell/dict-en_us": "^4.4.26", + "@cspell/dict-en_us": "^4.4.27", "@cspell/dict-fullstack": "^3.2.7", - "@cspell/dict-golang": "^6.0.25", + "@cspell/dict-golang": "^6.0.26", "@cspell/dict-k8s": "^1.0.12", "@cspell/dict-people-names": "^1.1.16", "@cspell/dict-software-terms": "^4.2.5", diff --git a/cspell/pnpm-lock.yaml b/cspell/pnpm-lock.yaml index c241282533..de581a5916 100644 --- a/cspell/pnpm-lock.yaml +++ b/cspell/pnpm-lock.yaml @@ -15,14 +15,14 @@ importers: specifier: ^2.0.13 version: 2.0.13 '@cspell/dict-en_us': - specifier: ^4.4.26 - version: 4.4.26 + specifier: ^4.4.27 + version: 4.4.27 '@cspell/dict-fullstack': specifier: ^3.2.7 version: 3.2.7 '@cspell/dict-golang': - specifier: ^6.0.25 - version: 6.0.25 + specifier: ^6.0.26 + version: 6.0.26 '@cspell/dict-k8s': specifier: ^1.0.12 version: 1.0.12 @@ -77,8 +77,8 @@ packages: '@cspell/dict-bash@4.2.2': resolution: {integrity: sha512-kyWbwtX3TsCf5l49gGQIZkRLaB/P8g73GDRm41Zu8Mv51kjl2H7Au0TsEvHv7jzcsRLS6aUYaZv6Zsvk1fOz+Q==} - '@cspell/dict-companies@3.2.9': - resolution: {integrity: sha512-y5GdU+LnuMhUE/WYwOYt7GcJdrpmV4KXE1oFb5toEsnGa2KzffUbS6lwPpeRBocQoqZj8jJYFtxoQ+2KVg++/A==} + '@cspell/dict-companies@3.2.10': + resolution: {integrity: sha512-bJ1qnO1DkTn7JYGXvxp8FRQc4yq6tRXnrII+jbP8hHmq5TX5o1Wu+rdfpoUQaMWTl6balRvcMYiINDesnpR9Bw==} '@cspell/dict-cpp@6.0.15': resolution: {integrity: sha512-N7MKK3llRNoBncygvrnLaGvmjo4xzVr5FbtAc9+MFGHK6/LeSySBupr1FM72XDaVSIsmBEe7sDYCHHwlI9Jb2w==} @@ -104,20 +104,20 @@ packages: '@cspell/dict-docker@1.1.17': resolution: {integrity: sha512-OcnVTIpHIYYKhztNTyK8ShAnXTfnqs43hVH6p0py0wlcwRIXe5uj4f12n7zPf2CeBI7JAlPjEsV0Rlf4hbz/xQ==} - '@cspell/dict-dotnet@5.0.10': - resolution: {integrity: sha512-ooar8BP/RBNP1gzYfJPStKEmpWy4uv/7JCq6FOnJLeD1yyfG3d/LFMVMwiJo+XWz025cxtkM3wuaikBWzCqkmg==} + '@cspell/dict-dotnet@5.0.11': + resolution: {integrity: sha512-LSVKhpFf/ASTWJcfYeS0Sykcl1gVMsv2Z5Eo0TnTMSTLV3738HH+66pIsjUTChqU6SF3gKPuCe6EOaRYqb/evA==} '@cspell/dict-elixir@4.0.8': resolution: {integrity: sha512-CyfphrbMyl4Ms55Vzuj+mNmd693HjBFr9hvU+B2YbFEZprE5AG+EXLYTMRWrXbpds4AuZcvN3deM2XVB80BN/Q==} - '@cspell/dict-en-common-misspellings@2.1.10': - resolution: {integrity: sha512-+S10oo15G3Axz1W4FGmYNq9u0xxS6OhNl9dXY3qjYBOqhzfF3l1oM/TpkfH/1NH31r3GneuPVXKXT7y16qwJYA==} + '@cspell/dict-en-common-misspellings@2.1.11': + resolution: {integrity: sha512-2jcY494If1udvzd7MT2z/QH/RACUo/I02vIY4ttNdZhgYvUmRKhg8OBdrbzYo0lJOcc7XUb8rhIFQRHzxOSVeA==} '@cspell/dict-en-gb@1.1.33': resolution: {integrity: sha512-tKSSUf9BJEV+GJQAYGw5e+ouhEe2ZXE620S7BLKe3ZmpnjlNG9JqlnaBhkIMxKnNFkLY2BP/EARzw31AZnOv4g==} - '@cspell/dict-en_us@4.4.26': - resolution: {integrity: sha512-rpjM87n2e3PN3mx9SbzQOIniEWUKewZj0xFA796Pzeu3gJlYsHsSkZZC6Jxdea2992EfrzJZYwJb+mjxa3gWGg==} + '@cspell/dict-en_us@4.4.27': + resolution: {integrity: sha512-0y4vH2i5cFmi8sxkc4OlD2IlnqDznOtKczm4h6jA288g5VVrm3bhkYK6vcB8b0CoRKtYWKet4VEmHBP1yI+Qfw==} '@cspell/dict-filetypes@3.0.15': resolution: {integrity: sha512-uDMeqYlLlK476w/muEFQGBy9BdQWS0mQ7BJiy/iQv5XUWZxE2O54ZQd9nW8GyQMzAgoyg5SG4hf9l039Qt66oA==} @@ -140,8 +140,8 @@ packages: '@cspell/dict-git@3.0.7': resolution: {integrity: sha512-odOwVKgfxCQfiSb+nblQZc4ErXmnWEnv8XwkaI4sNJ7cNmojnvogYVeMqkXPjvfrgEcizEEA4URRD2Ms5PDk1w==} - '@cspell/dict-golang@6.0.25': - resolution: {integrity: sha512-Q0mkUj1mFN1P5LZoKBeTLOQehlHMYv62K0Px9FS7qykSvZjBz44bhCezJuepTPCiCFqmwQgT2fc3Ixw+fhO6pQ==} + '@cspell/dict-golang@6.0.26': + resolution: {integrity: sha512-YKA7Xm5KeOd14v5SQ4ll6afe9VSy3a2DWM7L9uBq4u3lXToRBQ1W5PRa+/Q9udd+DTURyVVnQ+7b9cnOlNxaRg==} '@cspell/dict-google@1.0.9': resolution: {integrity: sha512-biL65POqialY0i4g6crj7pR6JnBkbsPovB2WDYkj3H4TuC/QXv7Pu5pdPxeUJA6TSCHI7T5twsO4VSVyRxD9CA==} @@ -193,14 +193,14 @@ packages: '@cspell/dict-node@5.0.8': resolution: {integrity: sha512-AirZcN2i84ynev3p2/1NCPEhnNsHKMz9zciTngGoqpdItUb2bDt1nJBjwlsrFI78GZRph/VaqTVFwYikmncpXg==} - '@cspell/dict-npm@5.2.27': - resolution: {integrity: sha512-REy2vRQ9BJkjoW8cEr8ewoJAZ0DsTh+TimJ58KgIG1d81caanNgdvKLSgDkPd8OlGxPfLKHe7o2TJuk/l7VqhA==} + '@cspell/dict-npm@5.2.28': + resolution: {integrity: sha512-tjnBjpIJsgYMTqNSrL5YlvFcXdtc7gkrL1ZI+MPSJSYOoJ78yeegS5UrIIbH3VrQtbNYSS8YhlEVF+xN0G4E8Q==} '@cspell/dict-people-names@1.1.16': resolution: {integrity: sha512-jiV+V32DVdaMqpznnqqNNMNaKFtyaHnZvak7HrVLWulGgobilQk+8NzFO9mtkyDs7Pde7CEGSExBAvc+xZxgeA==} - '@cspell/dict-php@4.1.0': - resolution: {integrity: sha512-dTDeabyOj7eFvn2Q4Za3uVXM2+SzeFMqX8ly2P0XTo4AzbCmI2hulFD/QIADwWmwiRrInbbf8cxwFHNIYrXl4w==} + '@cspell/dict-php@4.1.1': + resolution: {integrity: sha512-EXelI+4AftmdIGtA8HL8kr4WlUE11OqCSVlnIgZekmTkEGSZdYnkFdiJ5IANSALtlQ1mghKjz+OFqVs6yowgWA==} '@cspell/dict-powershell@5.0.15': resolution: {integrity: sha512-l4S5PAcvCFcVDMJShrYD0X6Huv9dcsQPlsVsBGbH38wvuN7gS7+GxZFAjTNxDmTY1wrNi1cCatSg6Pu2BW4rgg==} @@ -208,20 +208,20 @@ packages: '@cspell/dict-public-licenses@2.0.15': resolution: {integrity: sha512-cJEOs901H13Pfy0fl4dCD1U+xpWIMaEPq8MeYU83FfDZvellAuSo4GqWCripfIqlhns/L6+UZEIJSOZnjgy7Wg==} - '@cspell/dict-python@4.2.24': - resolution: {integrity: sha512-B1oXYTa0+3sKOvx/svwxFaT3MrkHJ7ZLWpA1N7ZyHoET7IJhLCwcfAu7DCTq1f24Wnd4t+ARJvPEmFbMx65VBw==} + '@cspell/dict-python@4.2.25': + resolution: {integrity: sha512-hDdN0YhKgpbtZVRjQ2c8jk+n0wQdidAKj1Fk8w7KEHb3YlY5uPJ0mAKJk7AJKPNLOlILoUmN+HAVJz+cfSbWYg==} '@cspell/dict-r@2.1.1': resolution: {integrity: sha512-71Ka+yKfG4ZHEMEmDxc6+blFkeTTvgKbKAbwiwQAuKl3zpqs1Y0vUtwW2N4b3LgmSPhV3ODVY0y4m5ofqDuKMw==} - '@cspell/dict-ruby@5.0.9': - resolution: {integrity: sha512-H2vMcERMcANvQshAdrVx0XoWaNX8zmmiQN11dZZTQAZaNJ0xatdJoSqY8C8uhEMW89bfgpN+NQgGuDXW2vmXEw==} + '@cspell/dict-ruby@5.1.0': + resolution: {integrity: sha512-9PJQB3cfkBULrMLp5kSAcFPpzf8oz9vFN+QYZABhQwWkGbuzCIXSorHrmWSASlx4yejt3brjaWS57zZ/YL5ZQQ==} '@cspell/dict-rust@4.1.0': resolution: {integrity: sha512-ysFxxKc3QjPWtPacbwxzz8sDOACHNShlhQpnBsDXAHN3LogmuBsQtfyuU30APqFjCOg9KwGciKYC/hcGxJCbiA==} - '@cspell/dict-scala@5.0.8': - resolution: {integrity: sha512-YdftVmumv8IZq9zu1gn2U7A4bfM2yj9Vaupydotyjuc+EEZZSqAafTpvW/jKLWji2TgybM1L2IhmV0s/Iv9BTw==} + '@cspell/dict-scala@5.0.9': + resolution: {integrity: sha512-AjVcVAELgllybr1zk93CJ5wSUNu/Zb5kIubymR/GAYkMyBdYFCZ3Zbwn4Zz8GJlFFAbazABGOu0JPVbeY59vGg==} '@cspell/dict-shell@1.1.2': resolution: {integrity: sha512-WqOUvnwcHK1X61wAfwyXq04cn7KYyskg90j4lLg3sGGKMW9Sq13hs91pqrjC44Q+lQLgCobrTkMDw9Wyl9nRFA==} @@ -229,8 +229,8 @@ packages: '@cspell/dict-software-terms@4.2.5': resolution: {integrity: sha512-CaRzkWti3AgcXoxuRcMijaNG7YUk/MH1rHjB8VX34v3UdCxXXeqvRyElRKnxhFeVLB/robb2UdShqh/CpskxRg==} - '@cspell/dict-software-terms@5.1.18': - resolution: {integrity: sha512-+RUM+DnRnGzDjnJrAEiEQnopPGBXQ5kUY9t38WdTVYVgkpIE0/dcMX+s5uAp7vvKezhU6gW+CGW5K5xdF2KKiw==} + '@cspell/dict-software-terms@5.1.19': + resolution: {integrity: sha512-3leJLYvibbOnPsIUV/60WcSPxzRmgrx6/0QkqRi8cSsEuRY5/cbUU8Jc0/hKYCIhWJlnIWh5yx34Ep2s8QSIBw==} '@cspell/dict-sql@2.2.1': resolution: {integrity: sha512-qDHF8MpAYCf4pWU8NKbnVGzkoxMNrFqBHyG/dgrlic5EQiKANCLELYtGlX5auIMDLmTf1inA0eNtv74tyRJ/vg==} @@ -451,7 +451,7 @@ snapshots: '@cspell/dict-al': 1.1.1 '@cspell/dict-aws': 4.0.17 '@cspell/dict-bash': 4.2.2 - '@cspell/dict-companies': 3.2.9 + '@cspell/dict-companies': 3.2.10 '@cspell/dict-cpp': 6.0.15 '@cspell/dict-cryptocurrencies': 5.0.5 '@cspell/dict-csharp': 4.0.8 @@ -460,11 +460,11 @@ snapshots: '@cspell/dict-data-science': 2.0.13 '@cspell/dict-django': 4.1.6 '@cspell/dict-docker': 1.1.17 - '@cspell/dict-dotnet': 5.0.10 + '@cspell/dict-dotnet': 5.0.11 '@cspell/dict-elixir': 4.0.8 - '@cspell/dict-en-common-misspellings': 2.1.10 + '@cspell/dict-en-common-misspellings': 2.1.11 '@cspell/dict-en-gb': 1.1.33 - '@cspell/dict-en_us': 4.4.26 + '@cspell/dict-en_us': 4.4.27 '@cspell/dict-filetypes': 3.0.15 '@cspell/dict-flutter': 1.1.1 '@cspell/dict-fonts': 4.0.5 @@ -472,7 +472,7 @@ snapshots: '@cspell/dict-fullstack': 3.2.7 '@cspell/dict-gaming-terms': 1.1.2 '@cspell/dict-git': 3.0.7 - '@cspell/dict-golang': 6.0.25 + '@cspell/dict-golang': 6.0.26 '@cspell/dict-google': 1.0.9 '@cspell/dict-haskell': 4.0.6 '@cspell/dict-html': 4.0.14 @@ -488,17 +488,17 @@ snapshots: '@cspell/dict-markdown': 2.0.14(@cspell/dict-css@4.0.19)(@cspell/dict-html-symbol-entities@4.0.5)(@cspell/dict-html@4.0.14)(@cspell/dict-typescript@3.2.3) '@cspell/dict-monkeyc': 1.0.12 '@cspell/dict-node': 5.0.8 - '@cspell/dict-npm': 5.2.27 - '@cspell/dict-php': 4.1.0 + '@cspell/dict-npm': 5.2.28 + '@cspell/dict-php': 4.1.1 '@cspell/dict-powershell': 5.0.15 '@cspell/dict-public-licenses': 2.0.15 - '@cspell/dict-python': 4.2.24 + '@cspell/dict-python': 4.2.25 '@cspell/dict-r': 2.1.1 - '@cspell/dict-ruby': 5.0.9 + '@cspell/dict-ruby': 5.1.0 '@cspell/dict-rust': 4.1.0 - '@cspell/dict-scala': 5.0.8 + '@cspell/dict-scala': 5.0.9 '@cspell/dict-shell': 1.1.2 - '@cspell/dict-software-terms': 5.1.18 + '@cspell/dict-software-terms': 5.1.19 '@cspell/dict-sql': 2.2.1 '@cspell/dict-svelte': 1.0.7 '@cspell/dict-swift': 2.0.6 @@ -530,7 +530,7 @@ snapshots: dependencies: '@cspell/dict-shell': 1.1.2 - '@cspell/dict-companies@3.2.9': {} + '@cspell/dict-companies@3.2.10': {} '@cspell/dict-cpp@6.0.15': {} @@ -548,15 +548,15 @@ snapshots: '@cspell/dict-docker@1.1.17': {} - '@cspell/dict-dotnet@5.0.10': {} + '@cspell/dict-dotnet@5.0.11': {} '@cspell/dict-elixir@4.0.8': {} - '@cspell/dict-en-common-misspellings@2.1.10': {} + '@cspell/dict-en-common-misspellings@2.1.11': {} '@cspell/dict-en-gb@1.1.33': {} - '@cspell/dict-en_us@4.4.26': {} + '@cspell/dict-en_us@4.4.27': {} '@cspell/dict-filetypes@3.0.15': {} @@ -572,7 +572,7 @@ snapshots: '@cspell/dict-git@3.0.7': {} - '@cspell/dict-golang@6.0.25': {} + '@cspell/dict-golang@6.0.26': {} '@cspell/dict-google@1.0.9': {} @@ -609,33 +609,33 @@ snapshots: '@cspell/dict-node@5.0.8': {} - '@cspell/dict-npm@5.2.27': {} + '@cspell/dict-npm@5.2.28': {} '@cspell/dict-people-names@1.1.16': {} - '@cspell/dict-php@4.1.0': {} + '@cspell/dict-php@4.1.1': {} '@cspell/dict-powershell@5.0.15': {} '@cspell/dict-public-licenses@2.0.15': {} - '@cspell/dict-python@4.2.24': + '@cspell/dict-python@4.2.25': dependencies: '@cspell/dict-data-science': 2.0.13 '@cspell/dict-r@2.1.1': {} - '@cspell/dict-ruby@5.0.9': {} + '@cspell/dict-ruby@5.1.0': {} '@cspell/dict-rust@4.1.0': {} - '@cspell/dict-scala@5.0.8': {} + '@cspell/dict-scala@5.0.9': {} '@cspell/dict-shell@1.1.2': {} '@cspell/dict-software-terms@4.2.5': {} - '@cspell/dict-software-terms@5.1.18': {} + '@cspell/dict-software-terms@5.1.19': {} '@cspell/dict-sql@2.2.1': {} diff --git a/docker-compose/e2e/compose.yaml b/docker-compose/e2e/compose.yaml index 7e8b402cb3..e64bcf45d8 100644 --- a/docker-compose/e2e/compose.yaml +++ b/docker-compose/e2e/compose.yaml @@ -8,7 +8,7 @@ services: ' build: context: ../../backend - dockerfile: docker/Dockerfile + dockerfile: ../docker/backend/Dockerfile depends_on: db: condition: service_healthy @@ -93,7 +93,7 @@ services: container_name: e2e-nest-tests build: context: ../../frontend - dockerfile: docker/Dockerfile.e2e.test + dockerfile: ../docker/frontend/Dockerfile.e2e.test command: > sh -c ' pnpm run test:e2e diff --git a/docker-compose/fuzz/compose.yaml b/docker-compose/fuzz/compose.yaml index 0db02c1060..466874757e 100644 --- a/docker-compose/fuzz/compose.yaml +++ b/docker-compose/fuzz/compose.yaml @@ -8,7 +8,7 @@ services: ' build: context: ../../backend - dockerfile: docker/Dockerfile + dockerfile: ../docker/backend/Dockerfile depends_on: db: condition: service_healthy @@ -92,7 +92,7 @@ services: graphql: container_name: fuzz-nest-graphql build: - context: ../../backend/docker + context: ../../docker/backend dockerfile: Dockerfile.fuzz environment: BASE_URL: http://backend:9500 diff --git a/docker-compose/local/compose.yaml b/docker-compose/local/compose.yaml index 1fd6f05c5a..50210b9611 100644 --- a/docker-compose/local/compose.yaml +++ b/docker-compose/local/compose.yaml @@ -9,7 +9,7 @@ services: image: nest-local-backend build: context: ../../backend - dockerfile: docker/Dockerfile.local + dockerfile: ../docker/backend/Dockerfile.local depends_on: cache: condition: service_healthy @@ -76,7 +76,7 @@ services: ' build: context: ../../ - dockerfile: docs/docker/Dockerfile.local + dockerfile: docker/docs/Dockerfile.local networks: - nest-network ports: @@ -93,7 +93,7 @@ services: ' build: context: ../../frontend - dockerfile: docker/Dockerfile.local + dockerfile: ../docker/frontend/Dockerfile.local depends_on: - backend environment: diff --git a/backend/docker/Dockerfile b/docker/backend/Dockerfile similarity index 96% rename from backend/docker/Dockerfile rename to docker/backend/Dockerfile index 30ecbac340..92c011820a 100644 --- a/backend/docker/Dockerfile +++ b/docker/backend/Dockerfile @@ -36,8 +36,7 @@ RUN --mount=type=cache,target=${POETRY_CACHE_DIR},uid=${OWASP_UID},gid=${OWASP_G poetry install --no-root --without dev --without test COPY apps apps -COPY docker/entrypoint.sh entrypoint.sh -COPY manage.py wsgi.py ./ +COPY entrypoint.sh manage.py wsgi.py ./ COPY settings settings COPY static static COPY templates templates diff --git a/backend/docker/Dockerfile.fuzz b/docker/backend/Dockerfile.fuzz similarity index 100% rename from backend/docker/Dockerfile.fuzz rename to docker/backend/Dockerfile.fuzz diff --git a/backend/docker/Dockerfile.local b/docker/backend/Dockerfile.local similarity index 100% rename from backend/docker/Dockerfile.local rename to docker/backend/Dockerfile.local diff --git a/backend/docker/Dockerfile.test b/docker/backend/Dockerfile.test similarity index 100% rename from backend/docker/Dockerfile.test rename to docker/backend/Dockerfile.test diff --git a/backend/docker/entrypoint.fuzz.sh b/docker/backend/entrypoint.fuzz.sh similarity index 100% rename from backend/docker/entrypoint.fuzz.sh rename to docker/backend/entrypoint.fuzz.sh diff --git a/docker/cspell/Dockerfile b/docker/cspell/Dockerfile new file mode 100644 index 0000000000..1e0069ca51 --- /dev/null +++ b/docker/cspell/Dockerfile @@ -0,0 +1,23 @@ +FROM node:24-alpine + +WORKDIR /opt/node + +ENV PNPM_HOME="/pnpm" +ENV NPM_CONFIG_RETRY=5 \ + NPM_CACHE="/nest/.npm" \ + NPM_CONFIG_TIMEOUT=30000 \ + PATH="$PNPM_HOME:$PATH" + +RUN --mount=type=cache,target=${NPM_CACHE} \ + npm install --ignore-scripts -g pnpm --cache ${NPM_CACHE} + +COPY package.json pnpm-lock.yaml ./ + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --frozen-lockfile --ignore-scripts + +WORKDIR /nest + +ENTRYPOINT ["/opt/node/node_modules/.bin/cspell"] + +USER node diff --git a/docs/docker/Dockerfile.local b/docker/docs/Dockerfile.local similarity index 100% rename from docs/docker/Dockerfile.local rename to docker/docs/Dockerfile.local diff --git a/frontend/docker/Dockerfile b/docker/frontend/Dockerfile similarity index 100% rename from frontend/docker/Dockerfile rename to docker/frontend/Dockerfile diff --git a/frontend/docker/Dockerfile.e2e.test b/docker/frontend/Dockerfile.e2e.test similarity index 100% rename from frontend/docker/Dockerfile.e2e.test rename to docker/frontend/Dockerfile.e2e.test diff --git a/frontend/docker/Dockerfile.local b/docker/frontend/Dockerfile.local similarity index 100% rename from frontend/docker/Dockerfile.local rename to docker/frontend/Dockerfile.local diff --git a/frontend/docker/Dockerfile.unit.test b/docker/frontend/Dockerfile.unit.test similarity index 100% rename from frontend/docker/Dockerfile.unit.test rename to docker/frontend/Dockerfile.unit.test diff --git a/docs/index.md b/docs/index.md index bdb29448bf..0baf6c94a1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,7 +14,7 @@ title: OWASP Nest ## What is OWASP Nest? -**OWASP Nest** is a comprehensive platform designed to enhance collaboration and contribution within the OWASP community. It serves as a central hub for exploring OWASP projects, finding contribution opportunities, and fostering community engagement in software security. +**OWASP Nest** is a comprehensive, community-first platform built to enhance collaboration and contribution across the OWASP community. It serves as a central hub for exploring OWASP projects, finding contribution opportunities, and fostering community engagement in software security. ### **Key Features** diff --git a/docs/poetry.lock b/docs/poetry.lock index 9c2feb9f64..4d97b445fd 100644 --- a/docs/poetry.lock +++ b/docs/poetry.lock @@ -37,14 +37,14 @@ extras = ["regex"] [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.1.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, - {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, + {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, + {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, ] [[package]] diff --git a/frontend/Makefile b/frontend/Makefile index d4acd8176a..3051cc250d 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -68,7 +68,7 @@ test-frontend-e2e: test-frontend-unit: @DOCKER_BUILDKIT=1 NEXT_PUBLIC_ENVIRONMENT=local docker build \ --cache-from nest-test-frontend-unit \ - -f frontend/docker/Dockerfile.unit.test frontend \ + -f docker/frontend/Dockerfile.unit.test frontend \ -t nest-test-frontend-unit @docker run --env-file frontend/.env.example --rm nest-test-frontend-unit pnpm run test:unit diff --git a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx index 0537290d53..78acfe216c 100644 --- a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx +++ b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx @@ -3,7 +3,7 @@ import React from 'react' import '@testing-library/jest-dom' import { FaCode, FaTags } from 'react-icons/fa6' import type { DetailsCardProps } from 'types/card' -import CardDetailsPage from 'components/CardDetailsPage' +import CardDetailsPage, { type CardType } from 'components/CardDetailsPage' jest.mock('next/link', () => { const MockLink = ({ children, @@ -117,6 +117,50 @@ jest.mock('components/HealthMetrics', () => ({ ), })) +jest.mock('components/ContributionHeatmap', () => ({ + __esModule: true, + default: ({ + contributionData, + startDate, + endDate, + ...props + }: { + contributionData: Record + startDate: string + endDate: string + [key: string]: unknown + }) => ( +
+ Heatmap: {Object.keys(contributionData).length} days from {startDate} to {endDate} +
+ ), +})) + +jest.mock('components/ContributionStats', () => ({ + __esModule: true, + default: ({ + title, + stats, + ...props + }: { + title: string + stats?: { commits: number; pullRequests: number; issues: number; total: number } + [key: string]: unknown + }) => ( +
+

{title}

+ {stats && ( + <> +

{stats.commits}

+

{stats.pullRequests}

+

{stats.issues}

+

{stats.total}

+ + )} +
+ ), +})) + jest.mock('components/InfoBlock', () => ({ __esModule: true, default: ({ @@ -694,7 +738,13 @@ describe('CardDetailsPage', () => { expect(detailsCard).toHaveClass('md:col-span-5') }) - const supportedTypes = ['project', 'repository', 'committee', 'user', 'organization'] + const supportedTypes: CardType[] = [ + 'project', + 'repository', + 'committee', + 'user', + 'organization', + ] test.each(supportedTypes)('renders statistics section for %s type', (entityType) => { render() @@ -950,7 +1000,14 @@ describe('CardDetailsPage', () => { expect(screen.getByTestId('recent-releases')).toBeInTheDocument() }) - const entityTypes = ['project', 'repository', 'user', 'organization', 'committee', 'chapter'] + const entityTypes: CardType[] = [ + 'project', + 'repository', + 'user', + 'organization', + 'committee', + 'chapter', + ] test.each(entityTypes)('renders all expected sections for %s type', (entityType) => { render( @@ -969,7 +1026,13 @@ describe('CardDetailsPage', () => { ).toBeInTheDocument() }) - const supportedTypes = ['project', 'repository', 'committee', 'user', 'organization'] + const supportedTypes: CardType[] = [ + 'project', + 'repository', + 'committee', + 'user', + 'organization', + ] test.each(supportedTypes)('renders statistics section for supported %s type', (entityType) => { render() @@ -1152,8 +1215,8 @@ describe('CardDetailsPage', () => { }) it('handles unsupported entity types gracefully', () => { - render() - + // eslint-disable-next-line @typescript-eslint/no-explicit-any + render() expect(screen.getByText('Unsupported-type Details')).toBeInTheDocument() }) @@ -1194,7 +1257,7 @@ describe('CardDetailsPage', () => { it('validates required vs optional props correctly', () => { const minimalValidProps: DetailsCardProps = { - type: 'project', + type: 'project' as const, stats: [], healthMetricsData: [], languages: [], @@ -1247,15 +1310,15 @@ describe('CardDetailsPage', () => { describe('Advanced Integration Tests', () => { it('handles multiple rapid prop changes', () => { - const { rerender } = render() + const { rerender } = render() - rerender() + rerender() expect(screen.getByText('Chapter Details')).toBeInTheDocument() - rerender() + rerender() expect(screen.getByText('User Details')).toBeInTheDocument() - rerender() + rerender() expect(screen.getByText('Organization Details')).toBeInTheDocument() }) @@ -1293,9 +1356,9 @@ describe('CardDetailsPage', () => { }) it('renders correctly with all optional sections enabled', () => { - const fullPropsAllSections = { + const fullPropsAllSections: DetailsCardProps = { ...defaultProps, - type: 'project', + type: 'project' as const, summary: 'Project summary text', userSummary:
User summary content
, socialLinks: ['https://github.com/test', 'https://twitter.com/test'], @@ -1396,9 +1459,9 @@ describe('CardDetailsPage', () => { describe('Archived Badge Functionality', () => { it('displays archived badge for archived repository', () => { - const archivedProps = { + const archivedProps: DetailsCardProps = { ...defaultProps, - type: 'repository', + type: 'repository' as const, isArchived: true, } @@ -1408,9 +1471,9 @@ describe('CardDetailsPage', () => { }) it('does not display archived badge for non-archived repository', () => { - const activeProps = { + const activeProps: DetailsCardProps = { ...defaultProps, - type: 'repository', + type: 'repository' as const, isArchived: false, } @@ -1420,9 +1483,9 @@ describe('CardDetailsPage', () => { }) it('does not display archived badge when isArchived is undefined', () => { - const undefinedProps = { + const undefinedProps: DetailsCardProps = { ...defaultProps, - type: 'repository', + type: 'repository' as const, } render() @@ -1431,9 +1494,9 @@ describe('CardDetailsPage', () => { }) it('does not display archived badge for non-repository types', () => { - const projectProps = { + const projectProps: DetailsCardProps = { ...defaultProps, - type: 'project', + type: 'project' as const, isArchived: true, } @@ -1443,9 +1506,9 @@ describe('CardDetailsPage', () => { }) it('displays archived badge alongside inactive badge', () => { - const bothBadgesProps = { + const bothBadgesProps: DetailsCardProps = { ...defaultProps, - type: 'repository', + type: 'repository' as const, isArchived: true, isActive: false, } @@ -1457,9 +1520,9 @@ describe('CardDetailsPage', () => { }) it('displays archived badge independently of active status', () => { - const archivedAndActiveProps = { + const archivedAndActiveProps: DetailsCardProps = { ...defaultProps, - type: 'repository', + type: 'repository' as const, isArchived: true, isActive: true, } @@ -1471,9 +1534,9 @@ describe('CardDetailsPage', () => { }) it('archived badge has correct positioning with flex container', () => { - const archivedProps = { + const archivedProps: DetailsCardProps = { ...defaultProps, - type: 'repository', + type: 'repository' as const, isArchived: true, } @@ -1485,9 +1548,9 @@ describe('CardDetailsPage', () => { }) it('archived badge renders with medium size', () => { - const archivedProps = { + const archivedProps: DetailsCardProps = { ...defaultProps, - type: 'repository', + type: 'repository' as const, isArchived: true, } @@ -1498,9 +1561,9 @@ describe('CardDetailsPage', () => { }) it('handles null isArchived gracefully', () => { - const nullArchivedProps = { + const nullArchivedProps: DetailsCardProps = { ...defaultProps, - type: 'repository', + type: 'repository' as const, isArchived: null, } @@ -1509,4 +1572,136 @@ describe('CardDetailsPage', () => { expect(screen.queryByText('Archived')).not.toBeInTheDocument() }) }) + + describe('Contribution Stats and Heatmap', () => { + const contributionData = { + '2024-01-01': 5, + '2024-01-02': 10, + '2024-01-03': 3, + } + + const contributionStats = { + commits: 100, + pullRequests: 50, + issues: 25, + total: 175, + } + + it('renders contribution stats and heatmap when data is provided', () => { + const propsWithContributions: DetailsCardProps = { + ...defaultProps, + type: 'project' as const, + contributionData, + contributionStats, + startDate: '2024-01-01', + endDate: '2024-12-31', + } + + render() + + expect(screen.getByText('Project Contribution Activity')).toBeInTheDocument() + expect(screen.getByText('100')).toBeInTheDocument() + expect(screen.getByText('50')).toBeInTheDocument() + expect(screen.getByText('25')).toBeInTheDocument() + expect(screen.getByText('175')).toBeInTheDocument() + }) + + it('uses correct title for chapter type', () => { + const chapterPropsWithContributions: DetailsCardProps = { + ...defaultProps, + type: 'chapter' as const, + contributionStats, + } + + render() + + expect(screen.getByText('Chapter Contribution Activity')).toBeInTheDocument() + }) + + it('does not render contribution section when no data is provided', () => { + render() + + expect(screen.queryByText('Project Contribution Activity')).not.toBeInTheDocument() + expect(screen.queryByText('Chapter Contribution Activity')).not.toBeInTheDocument() + }) + + it('renders only stats when contributionData is missing', () => { + const statsOnlyProps: DetailsCardProps = { + ...defaultProps, + type: 'project' as const, + contributionStats, + } + + render() + + expect(screen.getByText('Project Contribution Activity')).toBeInTheDocument() + expect(screen.getByText('100')).toBeInTheDocument() + }) + + it('renders heatmap when contributionData and dates are provided', () => { + const heatmapProps: DetailsCardProps = { + ...defaultProps, + type: 'project' as const, + contributionData, + startDate: '2024-01-01', + endDate: '2024-12-31', + } + + render() + + // Heatmap should be rendered (mocked in jest setup) + expect(screen.getByTestId('mock-heatmap-chart')).toBeInTheDocument() + }) + + it('does not render heatmap when dates are missing', () => { + const noDateProps: DetailsCardProps = { + ...defaultProps, + type: 'project' as const, + contributionData, + } + + render() + + expect(screen.queryByTestId('mock-heatmap-chart')).not.toBeInTheDocument() + }) + + it('does not render heatmap when contributionData is empty', () => { + const emptyDataProps: DetailsCardProps = { + ...defaultProps, + type: 'project' as const, + contributionData: {}, + startDate: '2024-01-01', + endDate: '2024-12-31', + } + + render() + + expect(screen.queryByTestId('mock-heatmap-chart')).not.toBeInTheDocument() + }) + + it('renders contribution section before top contributors', () => { + const fullProps: DetailsCardProps = { + ...defaultProps, + type: 'project' as const, + contributionStats, + topContributors: [ + { + login: 'user1', + name: 'User One', + avatarUrl: 'https://example.com/avatar1.png', + }, + ], + } + + render() + + const contributionSection = screen.getByText('Project Contribution Activity') + const contributorsSection = screen.getByText(/Top Contributors/i) + + // Check that contribution section appears before contributors + expect(contributionSection.compareDocumentPosition(contributorsSection)).toBe( + Node.DOCUMENT_POSITION_FOLLOWING + ) + }) + }) }) diff --git a/frontend/__tests__/unit/components/ContributionHeatmap.test.tsx b/frontend/__tests__/unit/components/ContributionHeatmap.test.tsx index 84b2d3ec72..dbff39b822 100644 --- a/frontend/__tests__/unit/components/ContributionHeatmap.test.tsx +++ b/frontend/__tests__/unit/components/ContributionHeatmap.test.tsx @@ -178,28 +178,26 @@ describe('ContributionHeatmap', () => { , 'light' ) - expect(screen.getByText('Light').parentElement).toHaveClass('text-gray-700') + expect(screen.getByText('Light').parentElement).toHaveClass('w-full') ;(useTheme as jest.Mock).mockReturnValue({ theme: 'dark', setTheme: jest.fn() }) rerender( ) - expect(screen.getByText('Dark').parentElement).toHaveClass('text-gray-700') + expect(screen.getByText('Dark').parentElement).toHaveClass('w-full') }) it('applies correct container and style classes', () => { const { container } = renderWithTheme() - expect(container.querySelector('.heatmap-container')).toBeInTheDocument() - expect(container.querySelector('style')).toBeInTheDocument() expect(container.querySelector('.w-full')).toBeInTheDocument() + expect(container.querySelector('style')).toBeInTheDocument() }) it('includes responsive media queries', () => { const { container } = renderWithTheme() const styleContent = container.querySelector('style')?.textContent - expect(styleContent).toContain('@media (max-width: 768px)') - expect(styleContent).toContain('@media (max-width: 480px)') + expect(styleContent).toContain('apexcharts-tooltip') }) }) @@ -210,13 +208,13 @@ describe('ContributionHeatmap', () => { ) const title = screen.getByText('Activity') expect(title).toHaveClass('font-semibold') - expect(title.parentElement).toHaveClass('mb-1', 'text-sm') - expect(container.querySelector('h4')).toBeInTheDocument() + expect(title.parentElement).toHaveClass('w-full') + expect(container.querySelector('h3')).toBeInTheDocument() }) it('has accessible heading structure', () => { renderWithTheme() - expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Accessible') + expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Accessible') }) }) @@ -224,7 +222,7 @@ describe('ContributionHeatmap', () => { it('sets correct dimensions and series count', () => { renderWithTheme() const chart = screen.getByTestId('mock-heatmap-chart') - expect(chart).toHaveAttribute('data-height', '100%') + expect(chart).toHaveAttribute('data-height', '195') expect(chart).toHaveAttribute('data-series-length', '7') }) @@ -540,15 +538,14 @@ describe('ContributionHeatmap', () => { describe('Responsive Design', () => { it('applies responsive container classes', () => { const { container } = renderWithTheme() - const heatmapContainer = container.querySelector('.heatmap-container') + const heatmapContainer = container.querySelector('.w-full') expect(heatmapContainer).toBeInTheDocument() }) it('maintains aspect ratio on different screen sizes', () => { const { container } = renderWithTheme() const styleContent = container.querySelector('style')?.textContent - expect(styleContent).toContain('aspect-ratio: 4 / 1') - expect(styleContent).toContain('min-height: 132px') + expect(styleContent).toContain('apexcharts-tooltip') }) }) @@ -620,4 +617,61 @@ describe('ContributionHeatmap', () => { expect(screen.getByTestId('mock-heatmap-chart')).toBeInTheDocument() }) }) + + describe('Variants', () => { + it('renders default variant with full dimensions', () => { + renderWithTheme() + const chart = screen.getByTestId('mock-heatmap-chart') + // Verify full-size dimensions (195px height for default variant) + expect(chart).toHaveAttribute('data-height', '195') + }) + + it('renders compact variant with smaller dimensions', () => { + renderWithTheme() + const chart = screen.getByTestId('mock-heatmap-chart') + // Verify compact dimensions (150px height for compact variant) + expect(chart).toHaveAttribute('data-height', '150') + }) + + it('applies compact-specific container styling when variant is compact', () => { + const { container } = renderWithTheme( + + ) + // Verify compact variant uses inline-block class + const chartContainer = container.querySelector('.inline-block') + expect(chartContainer).toBeInTheDocument() + }) + + it('applies default variant container styling when variant is default', () => { + const { container } = renderWithTheme( + + ) + // Verify default variant uses inline-block class + const chartContainer = container.querySelector('.inline-block') + expect(chartContainer).toBeInTheDocument() + }) + + it('defaults to default variant when no variant is specified', () => { + renderWithTheme() + const chart = screen.getByTestId('mock-heatmap-chart') + // Should render with default variant dimensions + expect(chart).toHaveAttribute('data-height', '195') + }) + + it('renders title with same styling regardless of variant', () => { + const { rerender } = renderWithTheme( + + ) + let title = screen.getByText('Test Title') + expect(title).toHaveClass('mb-4', 'text-sm', 'font-semibold') + + rerender( + + + + ) + title = screen.getByText('Test Title') + expect(title).toHaveClass('mb-4', 'text-sm', 'font-semibold') + }) + }) }) diff --git a/frontend/__tests__/unit/components/ContributionStats.test.tsx b/frontend/__tests__/unit/components/ContributionStats.test.tsx new file mode 100644 index 0000000000..a2512b3f8b --- /dev/null +++ b/frontend/__tests__/unit/components/ContributionStats.test.tsx @@ -0,0 +1,326 @@ +import { render, screen } from '@testing-library/react' +import ContributionStats from 'components/ContributionStats' + +describe('ContributionStats', () => { + const mockStats = { + commits: 150, + pullRequests: 25, + issues: 42, + total: 217, + } + + const defaultProps = { + title: 'Test Contribution Activity', + stats: mockStats, + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('Rendering', () => { + it('renders the component with title and stats', () => { + render() + + expect(screen.getByText('Test Contribution Activity')).toBeInTheDocument() + expect(screen.getByText('Commits')).toBeInTheDocument() + expect(screen.getByText('PRs')).toBeInTheDocument() + expect(screen.getByText('Issues')).toBeInTheDocument() + expect(screen.getByText('Total')).toBeInTheDocument() + }) + + it('displays formatted numbers correctly', () => { + render() + + expect(screen.getByText('150')).toBeInTheDocument() + expect(screen.getByText('25')).toBeInTheDocument() + expect(screen.getByText('42')).toBeInTheDocument() + expect(screen.getByText('217')).toBeInTheDocument() + }) + + it('displays large numbers with locale formatting', () => { + const largeStats = { + commits: 1500, + pullRequests: 2500, + issues: 4200, + total: 8200, + } + + render() + + expect(screen.getByText('1,500')).toBeInTheDocument() + expect(screen.getByText('2,500')).toBeInTheDocument() + expect(screen.getByText('4,200')).toBeInTheDocument() + expect(screen.getByText('8,200')).toBeInTheDocument() + }) + + it('renders all react-icons correctly', () => { + render() + + const container = screen.getByTestId('contribution-stats') + const icons = container.querySelectorAll('svg') + expect(icons).toHaveLength(5) // Title icon + 4 stat icons + + // Verify specific icon data attributes + expect(screen.getByTestId('contribution-stats')).toBeInTheDocument() + expect(screen.getByText('Test Contribution Activity')).toBeInTheDocument() + }) + + it('formats extremely large numbers correctly', () => { + const extremeStats = { + commits: 1234567, + pullRequests: 987654, + issues: 456789, + total: 2679010, + } + + render() + + expect(screen.getByText('1,234,567')).toBeInTheDocument() + expect(screen.getByText('987,654')).toBeInTheDocument() + expect(screen.getByText('456,789')).toBeInTheDocument() + expect(screen.getByText('2,679,010')).toBeInTheDocument() + }) + }) + + describe('Edge Cases - No Data', () => { + it('handles undefined stats gracefully', () => { + render() + + expect(screen.getByText('No Stats')).toBeInTheDocument() + expect(screen.getAllByText('0')).toHaveLength(4) // All stats should show 0 + }) + + it('handles null stats gracefully', () => { + render() + + expect(screen.getByText('Null Stats')).toBeInTheDocument() + expect(screen.getAllByText('0')).toHaveLength(4) // All stats should show 0 + }) + + it('handles empty object stats', () => { + render() + + expect(screen.getByText('Empty Stats')).toBeInTheDocument() + expect(screen.getAllByText('0')).toHaveLength(4) // All stats should show 0 + }) + }) + + describe('Edge Cases - Partial Data', () => { + it('handles partial stats data - only commits', () => { + const partialStats = { + commits: 100, + } + + render() + + // Verify commits value + expect(screen.getByText('100')).toBeInTheDocument() + + // Verify PRs, issues, and total are 0 + expect(screen.getAllByText('0')).toHaveLength(3) // pullRequests, issues, total should be 0 + }) + + it('handles partial stats data - mixed values', () => { + const partialStats = { + commits: 50, + issues: 25, + total: 75, + } + + render() + + expect(screen.getByText('50')).toBeInTheDocument() // commits + expect(screen.getByText('25')).toBeInTheDocument() // issues + expect(screen.getByText('75')).toBeInTheDocument() // total + expect(screen.getByText('0')).toBeInTheDocument() // pullRequests should be 0 + }) + + it('handles zero values correctly', () => { + const zeroStats = { + commits: 0, + pullRequests: 0, + issues: 0, + total: 0, + } + + render() + + expect(screen.getAllByText('0')).toHaveLength(4) + }) + }) + + describe('Edge Cases - Invalid Values', () => { + it('handles negative values gracefully', () => { + const negativeStats = { + commits: -5, + pullRequests: -3, + issues: -2, + total: -10, + } + + render() + + // Component should still render, showing the negative values or handling them gracefully + expect(screen.getByText('Negative Stats')).toBeInTheDocument() + }) + + it('handles non-numeric values', () => { + const invalidStats = { + commits: 'invalid' as unknown as number, + pullRequests: null as unknown as number, + issues: undefined as unknown as number, + total: 42, + } + + render() + + expect(screen.getByText('Invalid Stats')).toBeInTheDocument() + expect(screen.getByText('42')).toBeInTheDocument() // total should still work + }) + + it('handles very large numbers without breaking', () => { + const largeStats = { + commits: Number.MAX_SAFE_INTEGER, + pullRequests: 999999999, + issues: 888888888, + total: Number.MAX_SAFE_INTEGER, + } + + render() + + expect(screen.getByText('Large Stats')).toBeInTheDocument() + // Should not crash, even with very large numbers + }) + }) + + describe('Loading States', () => { + it('renders with loading-like undefined stats', () => { + render() + + expect(screen.getByText('Loading...')).toBeInTheDocument() + expect(screen.getAllByText('0')).toHaveLength(4) // Should show zeros while loading + }) + + it('handles transitioning from undefined to actual data', () => { + const { rerender } = render() + + expect(screen.getAllByText('0')).toHaveLength(4) + + // Simulate data loading + rerender() + + expect(screen.getByText('150')).toBeInTheDocument() + expect(screen.getByText('25')).toBeInTheDocument() + expect(screen.getByText('42')).toBeInTheDocument() + expect(screen.getByText('217')).toBeInTheDocument() + }) + }) + + describe('Accessibility', () => { + it('has proper heading structure', () => { + render() + + const heading = screen.getByRole('heading', { level: 2 }) + expect(heading).toHaveTextContent('Test Contribution Activity') + }) + + it('has proper semantic structure', () => { + render() + + // Check that the container exists and the grid has proper classes + const container = screen.getByTestId('contribution-stats') + expect(container).toBeInTheDocument() + + // The mb-6 class is on the grid div, not the container + const grid = container.querySelector('.grid') + expect(grid).toHaveClass('mb-6', 'grid', 'grid-cols-2', 'gap-4', 'sm:grid-cols-4') + }) + + it('provides meaningful labels for screen readers', () => { + render() + + expect(screen.getByText('Commits')).toBeInTheDocument() + expect(screen.getByText('PRs')).toBeInTheDocument() + expect(screen.getByText('Issues')).toBeInTheDocument() + expect(screen.getByText('Total')).toBeInTheDocument() + }) + }) + + describe('Different Use Cases', () => { + it('renders project-specific title correctly', () => { + render() + + expect(screen.getByText('Project Contribution Activity')).toBeInTheDocument() + }) + + it('renders chapter-specific title correctly', () => { + render() + + expect(screen.getByText('Chapter Contribution Activity')).toBeInTheDocument() + }) + + it('renders board candidate context correctly', () => { + render() + + expect(screen.getByText('Board Candidate Contributions')).toBeInTheDocument() + }) + }) + + describe('Type Safety and Props', () => { + it('accepts readonly props without issues', () => { + const readonlyProps = { + title: 'Readonly Test' as const, + stats: mockStats, + } + + expect(() => render()).not.toThrow() + }) + + it('handles dynamic title changes', () => { + const { rerender } = render() + + expect(screen.getByText('Initial Title')).toBeInTheDocument() + + rerender() + + expect(screen.getByText('Updated Title')).toBeInTheDocument() + expect(screen.queryByText('Initial Title')).not.toBeInTheDocument() + }) + }) + + describe('Visual Elements', () => { + it('renders with proper CSS classes for styling', () => { + render() + + const container = screen.getByTestId('contribution-stats') + expect(container).toBeInTheDocument() + + const heading = container.querySelector('h2') + expect(heading).toHaveClass('mb-4', 'flex', 'items-center', 'gap-2') + + // The mb-6 class is on the grid div + const grid = container.querySelector('.grid') + expect(grid).toHaveClass('mb-6', 'grid', 'grid-cols-2', 'gap-4', 'sm:grid-cols-4') + }) + + it('renders all required icons with proper attributes', () => { + render() + + const container = screen.getByTestId('contribution-stats') + const icons = container.querySelectorAll('svg') + expect(icons).toHaveLength(5) + + // Verify icons have proper styling classes + icons.forEach((icon) => { + expect(icon).toHaveClass('text-gray-600', 'dark:text-gray-400') + }) + + // Verify specific viewBox attributes for different react-icons + const viewBoxes = Array.from(icons).map((icon) => icon.getAttribute('viewBox')) + expect(viewBoxes).toContain('0 0 512 512') // chart-line and exclamation-circle + expect(viewBoxes).toContain('0 0 640 512') // code + expect(viewBoxes).toContain('0 0 384 512') // code-branch + }) + }) +}) diff --git a/frontend/graphql-codegen.ts b/frontend/graphql-codegen.ts index 0142730f6a..41bd209fb7 100644 --- a/frontend/graphql-codegen.ts +++ b/frontend/graphql-codegen.ts @@ -56,6 +56,7 @@ export default (async (): Promise => { Date: 'string | number', // eslint-disable-next-line @typescript-eslint/naming-convention DateTime: 'string | number', + JSON: 'Record', }, }, plugins: ['typescript'], diff --git a/frontend/package.json b/frontend/package.json index b375e02a71..3d9b45c29f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -58,7 +58,6 @@ "react-dom": "^19.2.3", "react-icons": "^5.5.0", "react-router-dom": "^7.11.0", - "rxjs": "^7.8.2", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7" }, @@ -85,19 +84,18 @@ "@types/node": "^25.0.3", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "@types/react-gtm-module": "^2.0.4", "@typescript-eslint/eslint-plugin": "^8.51.0", "@typescript-eslint/parser": "^8.51.0", "eslint": "^9.39.2", "eslint-config-next": "^16.1.1", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-alias": "^1.1.2", - "eslint-plugin-jest": "^29.12.0", + "eslint-plugin-jest": "^29.12.1", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", - "globals": "^16.5.0", + "globals": "^17.0.0", "identity-obj-proxy": "^3.0.0", "import-in-the-middle": "^2.0.1", "jest": "^30.2.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2168179980..9cdafa2de4 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -131,9 +131,6 @@ importers: react-router-dom: specifier: ^7.11.0 version: 7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - rxjs: - specifier: ^7.8.2 - version: 7.8.2 tailwind-merge: specifier: ^3.4.0 version: 3.4.0 @@ -207,9 +204,6 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.7) - '@types/react-gtm-module': - specifier: ^2.0.4 - version: 2.0.4 '@typescript-eslint/eslint-plugin': specifier: ^8.51.0 version: 8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) @@ -229,8 +223,8 @@ importers: specifier: ^1.1.2 version: 1.1.2(eslint-plugin-import@2.32.0) eslint-plugin-jest: - specifier: ^29.12.0 - version: 29.12.0(@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(jest@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.3)(typescript@5.9.3)))(typescript@5.9.3) + specifier: ^29.12.1 + version: 29.12.1(@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(jest@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.3)(typescript@5.9.3)))(typescript@5.9.3) eslint-plugin-jsx-a11y: specifier: ^6.10.2 version: 6.10.2(eslint@9.39.2(jiti@2.6.1)) @@ -244,8 +238,8 @@ importers: specifier: ^7.0.1 version: 7.0.1(eslint@9.39.2(jiti@2.6.1)) globals: - specifier: ^16.5.0 - version: 16.5.0 + specifier: ^17.0.0 + version: 17.0.0 identity-obj-proxy: specifier: ^3.0.0 version: 3.0.0 @@ -545,11 +539,11 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@emnapi/core@1.7.1': - resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} - '@emnapi/runtime@1.7.1': - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -2937,8 +2931,8 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@sinclair/typebox@0.34.45': - resolution: {integrity: sha512-qJcFVfCa5jxBFSuv7S5WYbA8XdeCPmhnaVVfX/2Y6L8WYg8sk3XY2+6W0zH+3mq1Cz+YC7Ki66HfqX6IHAwnkg==} + '@sinclair/typebox@0.34.46': + resolution: {integrity: sha512-kiW7CtS/NkdvTUjkjUJo7d5JsFfbJ14YjdhDk9KoEgK6nFjKNXZPrX0jfLA8ZlET4cFLHxOZ/0vFKOP+bOxIOQ==} '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -3294,9 +3288,6 @@ packages: peerDependencies: '@types/react': ^19.2.0 - '@types/react-gtm-module@2.0.4': - resolution: {integrity: sha512-5wPMWsUE5AI6O0B0K1/zbs0rFHBKu+7NWXQwDXhqvA12ooLD6W1AYiWZqR4UiOd7ixZDV1H5Ys301zEsqyIfNg==} - '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} @@ -3974,8 +3965,8 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} - cjs-module-lexer@2.1.1: - resolution: {integrity: sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -4515,8 +4506,8 @@ packages: '@typescript-eslint/parser': optional: true - eslint-plugin-jest@29.12.0: - resolution: {integrity: sha512-dOMLGkl5vCDZo/KcsmzJkkYJUH+SDLls4PLBj8Aw86x5BHdXkygMGdfnqikJ8RUgEx3MHni09B5cebZF5+4rrQ==} + eslint-plugin-jest@29.12.1: + resolution: {integrity: sha512-Rxo7r4jSANMBkXLICJKS0gjacgyopfNAsoS0e3R9AHnjoKuQOaaPfmsDJPi8UWwygI099OV/K/JhpYRVkxD4AA==} engines: {node: ^20.12.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@typescript-eslint/eslint-plugin': ^8.0.0 @@ -4880,8 +4871,8 @@ packages: resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} engines: {node: '>=18'} - globals@16.5.0: - resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + globals@17.0.0: + resolution: {integrity: sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==} engines: {node: '>=18'} globalthis@1.0.4: @@ -7091,8 +7082,8 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@2.3.0: - resolution: {integrity: sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -7553,8 +7544,8 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.3.4: - resolution: {integrity: sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==} + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} snapshots: @@ -7837,13 +7828,13 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@emnapi/core@1.7.1': + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.7.1': + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 optional: true @@ -9582,7 +9573,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.7.1 + '@emnapi/runtime': 1.8.1 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -9894,7 +9885,7 @@ snapshots: '@jest/schemas@30.0.5': dependencies: - '@sinclair/typebox': 0.34.45 + '@sinclair/typebox': 0.34.46 '@jest/snapshot-utils@30.2.0': dependencies: @@ -10026,8 +10017,8 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.7.1 - '@emnapi/runtime': 1.7.1 + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 '@tybys/wasm-util': 0.10.1 optional: true @@ -11447,7 +11438,7 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@sinclair/typebox@0.34.45': {} + '@sinclair/typebox@0.34.46': {} '@sinonjs/commons@3.0.1': dependencies: @@ -11787,8 +11778,6 @@ snapshots: dependencies: '@types/react': 19.2.7 - '@types/react-gtm-module@2.0.4': {} - '@types/react@19.2.7': dependencies: csstype: 3.2.3 @@ -11830,7 +11819,7 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.3.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -11872,7 +11861,7 @@ snapshots: '@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) - ts-api-utils: 2.3.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -11889,7 +11878,7 @@ snapshots: minimatch: 9.0.5 semver: 7.7.3 tinyglobby: 0.2.15 - ts-api-utils: 2.3.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -12573,7 +12562,7 @@ snapshots: cjs-module-lexer@1.4.3: {} - cjs-module-lexer@2.1.1: {} + cjs-module-lexer@2.2.0: {} class-variance-authority@0.7.1: dependencies: @@ -13138,7 +13127,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest@29.12.0(@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(jest@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.3)(typescript@5.9.3)))(typescript@5.9.3): + eslint-plugin-jest@29.12.1(@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(jest@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.3)(typescript@5.9.3)))(typescript@5.9.3): dependencies: '@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) @@ -13184,8 +13173,8 @@ snapshots: '@babel/parser': 7.28.5 eslint: 9.39.2(jiti@2.6.1) hermes-parser: 0.25.1 - zod: 4.3.4 - zod-validation-error: 4.0.2(zod@4.3.4) + zod: 4.3.5 + zod-validation-error: 4.0.2(zod@4.3.5) transitivePeerDependencies: - supports-color @@ -13616,7 +13605,7 @@ snapshots: globals@16.4.0: {} - globals@16.5.0: {} + globals@17.0.0: {} globalthis@1.0.4: dependencies: @@ -14364,7 +14353,7 @@ snapshots: '@jest/types': 30.2.0 '@types/node': 25.0.3 chalk: 4.1.2 - cjs-module-lexer: 2.1.1 + cjs-module-lexer: 2.2.0 collect-v8-coverage: 1.0.3 glob: 10.5.0 graceful-fs: 4.2.11 @@ -16106,7 +16095,7 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@2.3.0(typescript@5.9.3): + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -16602,10 +16591,10 @@ snapshots: toposort: 2.0.2 type-fest: 2.19.0 - zod-validation-error@4.0.2(zod@4.3.4): + zod-validation-error@4.0.2(zod@4.3.5): dependencies: - zod: 4.3.4 + zod: 4.3.5 zod@3.25.76: {} - zod@4.3.4: {} + zod@4.3.5: {} diff --git a/frontend/src/app/board/[year]/candidates/page.tsx b/frontend/src/app/board/[year]/candidates/page.tsx index 081e6840bc..cbb8c346ac 100644 --- a/frontend/src/app/board/[year]/candidates/page.tsx +++ b/frontend/src/app/board/[year]/candidates/page.tsx @@ -464,6 +464,7 @@ const BoardCandidatesPage = () => { contributionData={snapshot.contributionHeatmapData} startDate={snapshot.startAt} endDate={snapshot.endAt} + variant="compact" /> )} @@ -626,6 +627,7 @@ const BoardCandidatesPage = () => { endDate={snapshot.endAt} title="OWASP Community Engagement" unit="message" + variant="compact" /> )} diff --git a/frontend/src/app/chapters/[chapterKey]/page.tsx b/frontend/src/app/chapters/[chapterKey]/page.tsx index b425c42838..5fddb5e4a8 100644 --- a/frontend/src/app/chapters/[chapterKey]/page.tsx +++ b/frontend/src/app/chapters/[chapterKey]/page.tsx @@ -7,7 +7,8 @@ import { handleAppError, ErrorDisplay } from 'app/global-error' import { GetChapterDataDocument } from 'types/__generated__/chapterQueries.generated' import type { Chapter } from 'types/chapter' import type { Contributor } from 'types/contributor' -import { formatDate } from 'utils/dateFormatter' +import { getContributionStats } from 'utils/contributionDataUtils' +import { formatDate, getDateRange } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' @@ -59,14 +60,26 @@ export default function ChapterDetailsPage() { ), }, ] + + const { startDate, endDate } = getDateRange({ years: 1 }) + + const contributionStats = getContributionStats( + chapter.contributionStats, + chapter.contributionData + ) + return (
{event.suggestedLocation && (
- +
)} @@ -250,8 +250,7 @@ export default function Home() { {chapter.leaders.length > 0 && (
- {' '} - +
)} diff --git a/frontend/src/app/projects/[projectKey]/page.tsx b/frontend/src/app/projects/[projectKey]/page.tsx index f7fc8a1169..f565a7fd41 100644 --- a/frontend/src/app/projects/[projectKey]/page.tsx +++ b/frontend/src/app/projects/[projectKey]/page.tsx @@ -11,7 +11,8 @@ import { ErrorDisplay, handleAppError } from 'app/global-error' import { GetProjectDocument } from 'types/__generated__/projectQueries.generated' import type { Contributor } from 'types/contributor' import type { Project } from 'types/project' -import { formatDate } from 'utils/dateFormatter' +import { getContributionStats } from 'utils/contributionDataUtils' +import { formatDate, getDateRange } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' @@ -85,9 +86,19 @@ const ProjectDetailsPage = () => { }, ] + const { startDate, endDate } = getDateRange({ years: 1 }) + + const contributionStats = getContributionStats( + project.contributionStats, + project.contributionData + ) + return ( { recentMilestones={project.recentMilestones} recentReleases={project.recentReleases} repositories={project.repositories} + startDate={startDate} stats={projectStats} summary={project.summary} title={project.name} diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index fae64ca9ac..9d71515b9b 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -16,6 +16,8 @@ import { scrollToAnchor } from 'utils/scrollToAnchor' import { getSocialIcon } from 'utils/urlIconMappings' import AnchorTitle from 'components/AnchorTitle' import ChapterMapWrapper from 'components/ChapterMapWrapper' +import ContributionHeatmap from 'components/ContributionHeatmap' +import ContributionStats from 'components/ContributionStats' import EntityActions from 'components/EntityActions' import HealthMetrics from 'components/HealthMetrics' import InfoBlock from 'components/InfoBlock' @@ -36,10 +38,33 @@ import StatusBadge from 'components/StatusBadge' import ToggleableList from 'components/ToggleableList' import TopContributorsList from 'components/TopContributorsList' +export type CardType = + | 'chapter' + | 'committee' + | 'module' + | 'organization' + | 'program' + | 'project' + | 'repository' + | 'user' + +const showStatistics = (type: CardType): boolean => + ['committee', 'organization', 'project', 'repository', 'user'].includes(type) + +const showIssuesAndMilestones = (type: CardType): boolean => + ['organization', 'project', 'repository', 'user'].includes(type) + +const showPullRequestsAndReleases = (type: CardType): boolean => + ['organization', 'project', 'repository', 'user'].includes(type) + const DetailsCard = ({ description, details, accessLevel, + contributionData, + contributionStats, + endDate, + startDate, status, setStatus, canUpdateStatus, @@ -146,11 +171,7 @@ const DetailsCard = ({ )} - {(type === 'project' || - type === 'repository' || - type === 'committee' || - type === 'user' || - type === 'organization') && ( + {showStatistics(type) && ( } @@ -239,6 +260,33 @@ const DetailsCard = ({ )} {entityLeaders && entityLeaders.length > 0 && } + {(type === 'project' || type === 'chapter') && (contributionData || contributionStats) && ( +
+
+ {contributionStats && ( + + )} + {contributionData && + Object.keys(contributionData).length > 0 && + startDate && + endDate && ( +
+
+ +
+
+ )} +
+
+ )} {topContributors && ( )} - {(type === 'project' || - type === 'repository' || - type === 'user' || - type === 'organization') && ( + {showIssuesAndMilestones(type) && (
- {type === 'user' || - type === 'organization' || - type === 'repository' || - type === 'project' ? ( - - ) : ( - - )} +
)} - {(type === 'project' || - type === 'repository' || - type === 'organization' || - type === 'user') && ( + {showPullRequestsAndReleases(type) && (
diff --git a/frontend/src/components/ContributionHeatmap.tsx b/frontend/src/components/ContributionHeatmap.tsx index 9f905e7440..2da4804c12 100644 --- a/frontend/src/components/ContributionHeatmap.tsx +++ b/frontend/src/components/ContributionHeatmap.tsx @@ -1,17 +1,255 @@ import dynamic from 'next/dynamic' import { useTheme } from 'next-themes' import React, { useMemo } from 'react' +import { pluralize } from 'utils/pluralize' const Chart = dynamic(() => import('react-apexcharts'), { ssr: false, }) +const generateHeatmapSeries = ( + startDate: string, + endDate: string, + contributionData: Record +) => { + if (!startDate || !endDate) { + const defaultEnd = new Date() + defaultEnd.setUTCHours(0, 0, 0, 0) + const defaultStart = new Date() + defaultStart.setUTCHours(0, 0, 0, 0) + defaultStart.setUTCFullYear(defaultEnd.getUTCFullYear() - 1) + return generateHeatmapSeries( + defaultStart.toISOString().split('T')[0], + defaultEnd.toISOString().split('T')[0], + contributionData + ) + } + + const start = new Date(startDate) + const end = new Date(endDate) + + if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) { + const defaultEnd = new Date() + defaultEnd.setUTCHours(0, 0, 0, 0) + const defaultStart = new Date() + defaultStart.setUTCHours(0, 0, 0, 0) + defaultStart.setUTCFullYear(defaultEnd.getUTCFullYear() - 1) + return generateHeatmapSeries( + defaultStart.toISOString().split('T')[0], + defaultEnd.toISOString().split('T')[0], + contributionData + ) + } + + if (start > end) { + const swappedStartDate = endDate + const swappedEndDate = startDate + return generateHeatmapSeries(swappedStartDate, swappedEndDate, contributionData) + } + + const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + + const series = dayNames.map((day) => ({ + name: day, + data: [] as Array<{ x: string; y: number; date: string }>, + })) + + const firstDay = new Date(start) + const daysToMonday = (firstDay.getUTCDay() + 6) % 7 + firstDay.setUTCDate(firstDay.getUTCDate() - daysToMonday) + + const currentDate = new Date(firstDay) + let weekNumber = 1 + + while (currentDate <= end) { + const dayOfWeek = currentDate.getUTCDay() + const adjustedDayIndex = dayOfWeek === 0 ? 6 : dayOfWeek - 1 + const dateStr = currentDate.toISOString().split('T')[0] + const weekLabel = `W${weekNumber}` + + const isInRange = currentDate >= start && currentDate <= end + const contributionCount = isInRange ? contributionData?.[dateStr] || 0 : 0 + + series[adjustedDayIndex].data.push({ + x: weekLabel, + y: contributionCount, + date: dateStr, + }) + + currentDate.setUTCDate(currentDate.getUTCDate() + 1) + + if (currentDate.getUTCDay() === 1 && currentDate <= end) { + weekNumber++ + } + } + + const reversedSeries = series.slice().reverse() + return { heatmapSeries: reversedSeries } +} + +const getChartOptions = (isDarkMode: boolean, unit: string) => ({ + chart: { + type: 'heatmap' as const, + toolbar: { + show: false, + }, + background: 'transparent', + }, + dataLabels: { + enabled: false, + }, + legend: { + show: false, + }, + colors: ['#008FFB'], + plotOptions: { + heatmap: { + colorScale: { + ranges: [ + { + from: 0, + to: 0, + color: isDarkMode ? '#2C3A4D' : '#E7E7E6', + name: 'No activity', + }, + { + from: 1, + to: 4, + color: isDarkMode ? '#4A5F7A' : '#7BA3C0', + name: 'Low', + }, + { + from: 5, + to: 8, + color: isDarkMode ? '#5A6F8A' : '#6C8EAB', + name: 'Medium', + }, + { + from: 9, + to: 12, + color: isDarkMode ? '#6A7F9A' : '#5C7BA2', + name: 'High', + }, + { + from: 13, + to: 1000, + color: isDarkMode ? '#7A8FAA' : '#567498', + name: 'Very High', + }, + ], + }, + radius: 2, + distributed: false, + useFillColorAsStroke: false, + enableShades: false, + }, + }, + states: { + hover: { + filter: { + type: 'none', + }, + }, + active: { + filter: { + type: 'none', + }, + }, + }, + stroke: { + show: true, + width: 2, + colors: [isDarkMode ? '#1F2937' : '#FFFFFF'], + }, + grid: { + show: false, + padding: { + top: 0, + right: 0, + bottom: 0, + left: 0, + }, + }, + tooltip: { + enabled: true, + shared: false, + intersect: true, + followCursor: true, + offsetY: -10, + style: { + fontSize: '12px', + }, + custom: ({ seriesIndex, dataPointIndex, w }) => { + const data = w.config.series[seriesIndex].data[dataPointIndex] + if (!data) return '' + + const count = data.y + const date = data.date + const parsedDate = new Date(date + 'T00:00:00Z') + const formattedDate = parsedDate.toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }) + + const bgColor = isDarkMode ? '#1F2937' : '#FFFFFF' + const textColor = isDarkMode ? '#F3F4F6' : '#111827' + const secondaryColor = isDarkMode ? '#9CA3AF' : '#6B7280' + const unitLabel = pluralize(count, unit) + + return ` +
+
${formattedDate}
+
${count} ${unitLabel}
+
+ ` + }, + }, + xaxis: { + type: 'category' as const, + labels: { + show: false, + }, + axisBorder: { + show: false, + }, + axisTicks: { + show: false, + }, + tooltip: { + enabled: false, + }, + }, + yaxis: { + labels: { + show: false, + }, + axisBorder: { + show: false, + }, + axisTicks: { + show: false, + }, + tooltip: { + enabled: false, + }, + }, +}) + interface ContributionHeatmapProps { contributionData: Record startDate: string endDate: string title?: string unit?: string + variant?: 'default' | 'compact' } const ContributionHeatmap: React.FC = ({ @@ -20,232 +258,45 @@ const ContributionHeatmap: React.FC = ({ endDate, title, unit = 'contribution', + variant = 'default', }) => { const { theme } = useTheme() const isDarkMode = theme === 'dark' + const isCompact = variant === 'compact' - const { heatmapSeries } = useMemo(() => { - const start = new Date(startDate) - const end = new Date(endDate) - const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] - - // Initialize series for each day of week - const series = dayNames.map((day) => ({ - name: day, - data: [] as Array<{ x: string; y: number; date: string }>, - })) - - // Find the first Monday before or on start date - const firstDay = new Date(start) - const daysToMonday = (firstDay.getDay() + 6) % 7 - firstDay.setDate(firstDay.getDate() - daysToMonday) - - const currentDate = new Date(firstDay) - let weekNumber = 1 - - while (currentDate <= end) { - const dayOfWeek = currentDate.getDay() - // Convert Sunday=0 to Sunday=6, Monday=1 to Monday=0, etc. - const adjustedDayIndex = dayOfWeek === 0 ? 6 : dayOfWeek - 1 - // Format date in local time to avoid timezone shift - const year = currentDate.getFullYear() - const month = String(currentDate.getMonth() + 1).padStart(2, '0') - const day = String(currentDate.getDate()).padStart(2, '0') - const dateStr = `${year}-${month}-${day}` - const weekLabel = `W${weekNumber}` - - // Only count contributions within the actual range - const isInRange = currentDate >= start && currentDate <= end - const contributionCount = isInRange ? contributionData[dateStr] || 0 : 0 - - series[adjustedDayIndex].data.push({ - x: weekLabel, - y: contributionCount, - date: dateStr, - }) + const { heatmapSeries } = useMemo( + () => generateHeatmapSeries(startDate, endDate, contributionData), + [contributionData, startDate, endDate] + ) - // Move to next day - currentDate.setDate(currentDate.getDate() + 1) + const options = useMemo(() => getChartOptions(isDarkMode, unit), [isDarkMode, unit]) - // Increment week number when we hit Monday - if (currentDate.getDay() === 1 && currentDate <= end) { - weekNumber++ - } - } + const calculateChartWidth = useMemo(() => { + const weeksCount = heatmapSeries[0]?.data?.length || 0 - // Calculate height based on number of weeks and maintain square cells - const cellSize = 16 - const height = dayNames.length * cellSize + 20 // 7 days * cellSize + padding + if (isCompact) { + const pixelPerWeek = 13.4 + const padding = 40 + const calculatedWidth = weeksCount * pixelPerWeek + padding + return Math.max(400, calculatedWidth) + } - // Reverse the series so Monday is at the top and Sunday at the bottom - return { heatmapSeries: series.reverse(), chartHeight: height } - }, [contributionData, startDate, endDate]) + const pixelPerWeek = 19.5 + const padding = 50 + const calculatedWidth = weeksCount * pixelPerWeek + padding + return Math.max(600, calculatedWidth) + }, [heatmapSeries, isCompact]) - const options = { - chart: { - type: 'heatmap' as const, - toolbar: { - show: false, - }, - background: 'transparent', - }, - dataLabels: { - enabled: false, - }, - legend: { - show: false, - }, - colors: ['#008FFB'], - plotOptions: { - heatmap: { - colorScale: { - ranges: [ - { - from: 0, - to: 0, - color: isDarkMode ? '#2C3A4D' : '#E7E7E6', - name: 'No activity', - }, - { - from: 1, - to: 4, - color: isDarkMode ? '#4A5F7A' : '#7BA3C0', - name: 'Low', - }, - { - from: 5, - to: 8, - color: isDarkMode ? '#5A6F8A' : '#6C8EAB', - name: 'Medium', - }, - { - from: 9, - to: 12, - color: isDarkMode ? '#6A7F9A' : '#5C7BA2', - name: 'High', - }, - { - from: 13, - to: 1000, - color: isDarkMode ? '#7A8FAA' : '#567498', - name: 'Very High', - }, - ], - }, - radius: 2, - distributed: false, - useFillColorAsStroke: false, - enableShades: false, - }, - }, - states: { - hover: { - filter: { - type: 'none', - }, - }, - active: { - filter: { - type: 'none', - }, - }, - }, - stroke: { - show: true, - width: 2, - colors: [isDarkMode ? '#1F2937' : '#FFFFFF'], - }, - grid: { - show: false, - padding: { - top: 0, - right: 0, - bottom: 0, - left: 0, - }, - }, - tooltip: { - enabled: true, - shared: false, - intersect: true, - followCursor: true, - offsetY: -10, - style: { - fontSize: '12px', - }, - custom: ({ seriesIndex, dataPointIndex, w }) => { - const data = w.config.series[seriesIndex].data[dataPointIndex] - if (!data) return '' - - const count = data.y - const date = data.date - // Parse date as UTC to match data format - const formattedDate = new Date(date + 'T00:00:00Z').toLocaleDateString('en-US', { - weekday: 'short', - month: 'short', - day: 'numeric', - timeZone: 'UTC', - }) - - const bgColor = isDarkMode ? '#1F2937' : '#FFFFFF' - const textColor = isDarkMode ? '#F3F4F6' : '#111827' - const secondaryColor = isDarkMode ? '#9CA3AF' : '#6B7280' - - const unitLabel = count !== 1 ? `${unit}s` : unit - - return ` -
-
${formattedDate}
-
${count} ${unitLabel}
-
- ` - }, - }, - xaxis: { - type: 'category' as const, - labels: { - show: false, - }, - axisBorder: { - show: false, - }, - axisTicks: { - show: false, - }, - tooltip: { - enabled: false, - }, - }, - yaxis: { - labels: { - show: false, - }, - axisBorder: { - show: false, - }, - axisTicks: { - show: false, - }, - tooltip: { - enabled: false, - }, - }, - } + const chartWidth = calculateChartWidth return ( -
+
{title && ( -

- {title} -

+

{title}

)} -
+ + {/* scroll wrapper for small screens */} +
-
+ +
diff --git a/frontend/src/components/ContributionStats.tsx b/frontend/src/components/ContributionStats.tsx new file mode 100644 index 0000000000..b3a7b69655 --- /dev/null +++ b/frontend/src/components/ContributionStats.tsx @@ -0,0 +1,64 @@ +import { FaChartLine, FaCode, FaCodeBranch, FaExclamationCircle } from 'react-icons/fa' +import type { ContributionStats as ContributionStatsType } from 'utils/contributionDataUtils' + +interface ContributionStatsProps { + readonly title: string + readonly stats?: ContributionStatsType +} + +export default function ContributionStats({ title, stats }: Readonly) { + const formatNumber = (value?: number) => { + return typeof value === 'number' ? value.toLocaleString() : '0' + } + + return ( +
+

+ + {title} +

+
+
+ +
+

+ Commits +

+

+ {formatNumber(stats?.commits)} +

+
+
+
+ +
+

PRs

+

+ {formatNumber(stats?.pullRequests)} +

+
+
+
+ +
+

+ Issues +

+

+ {formatNumber(stats?.issues)} +

+
+
+
+ +
+

Total

+

+ {formatNumber(stats?.total)} +

+
+
+
+
+ ) +} diff --git a/frontend/src/components/SponsorCard.tsx b/frontend/src/components/SponsorCard.tsx index 3bfcd85574..c334eb4bce 100644 --- a/frontend/src/components/SponsorCard.tsx +++ b/frontend/src/components/SponsorCard.tsx @@ -1,7 +1,7 @@ import Link from 'next/link' const SponsorCard = ({ target, title, type }: { target: string; title: string; type: string }) => ( -
+

Want to become a sponsor?

Support {title} to help grow global cybersecurity community. diff --git a/frontend/src/server/queries/chapterQueries.ts b/frontend/src/server/queries/chapterQueries.ts index 0035194da8..ceeee6a7d2 100644 --- a/frontend/src/server/queries/chapterQueries.ts +++ b/frontend/src/server/queries/chapterQueries.ts @@ -3,6 +3,8 @@ import { gql } from '@apollo/client' export const GET_CHAPTER_DATA = gql` query GetChapterData($key: String!) { chapter(key: $key) { + contributionData + contributionStats id entityLeaders { id diff --git a/frontend/src/server/queries/projectQueries.ts b/frontend/src/server/queries/projectQueries.ts index d2deee4625..95db2bb39f 100644 --- a/frontend/src/server/queries/projectQueries.ts +++ b/frontend/src/server/queries/projectQueries.ts @@ -3,22 +3,24 @@ import { gql } from '@apollo/client' export const GET_PROJECT_DATA = gql` query GetProject($key: String!) { project(key: $key) { - id + contributionData + contributionStats contributorsCount entityLeaders { - id description + id memberName member { + avatarUrl id login name - avatarUrl } } forksCount - issuesCount + id isActive + issuesCount key languages leaders diff --git a/frontend/src/types/__generated__/chapterQueries.generated.ts b/frontend/src/types/__generated__/chapterQueries.generated.ts index e24187a4e3..2f8993c04a 100644 --- a/frontend/src/types/__generated__/chapterQueries.generated.ts +++ b/frontend/src/types/__generated__/chapterQueries.generated.ts @@ -6,7 +6,7 @@ export type GetChapterDataQueryVariables = Types.Exact<{ }>; -export type GetChapterDataQuery = { chapter: { __typename: 'ChapterNode', id: string, isActive: boolean, key: string, name: string, region: string, relatedUrls: Array, suggestedLocation: string | null, summary: string, updatedAt: number, url: string, entityLeaders: Array<{ __typename: 'EntityMemberNode', id: string, description: string, memberName: string, member: { __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string } | null }>, geoLocation: { __typename: 'GeoLocationType', lat: number, lng: number } | null } | null, topContributors: Array<{ __typename: 'RepositoryContributorNode', id: string, avatarUrl: string, login: string, name: string }> }; +export type GetChapterDataQuery = { chapter: { __typename: 'ChapterNode', contributionData: any, contributionStats: any | null, id: string, isActive: boolean, key: string, name: string, region: string, relatedUrls: Array, suggestedLocation: string | null, summary: string, updatedAt: number, url: string, entityLeaders: Array<{ __typename: 'EntityMemberNode', id: string, description: string, memberName: string, member: { __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string } | null }>, geoLocation: { __typename: 'GeoLocationType', lat: number, lng: number } | null } | null, topContributors: Array<{ __typename: 'RepositoryContributorNode', id: string, avatarUrl: string, login: string, name: string }> }; export type GetChapterMetadataQueryVariables = Types.Exact<{ key: Types.Scalars['String']['input']; @@ -16,5 +16,5 @@ export type GetChapterMetadataQueryVariables = Types.Exact<{ export type GetChapterMetadataQuery = { chapter: { __typename: 'ChapterNode', id: string, name: string, summary: string } | null }; -export const GetChapterDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapterData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"entityLeaders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"memberName"}},{"kind":"Field","name":{"kind":"Name","value":"member"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"geoLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lat"}},{"kind":"Field","name":{"kind":"Name","value":"lng"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"relatedUrls"}},{"kind":"Field","name":{"kind":"Name","value":"suggestedLocation"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"topContributors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"chapter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; +export const GetChapterDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapterData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contributionData"}},{"kind":"Field","name":{"kind":"Name","value":"contributionStats"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"entityLeaders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"memberName"}},{"kind":"Field","name":{"kind":"Name","value":"member"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"geoLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lat"}},{"kind":"Field","name":{"kind":"Name","value":"lng"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"relatedUrls"}},{"kind":"Field","name":{"kind":"Name","value":"suggestedLocation"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"topContributors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"chapter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const GetChapterMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapterMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/__generated__/graphql.ts b/frontend/src/types/__generated__/graphql.ts index dcc5582eaf..6667b8a0b3 100644 --- a/frontend/src/types/__generated__/graphql.ts +++ b/frontend/src/types/__generated__/graphql.ts @@ -14,7 +14,7 @@ export type Scalars = { Float: { input: number; output: number; } Date: { input: string | number; output: string | number; } DateTime: { input: string | number; output: string | number; } - JSON: { input: any; output: any; } + JSON: { input: Record; output: Record; } UUID: { input: any; output: any; } }; @@ -60,6 +60,8 @@ export type BoardOfDirectorsNode = Node & { export type ChapterNode = Node & { __typename?: 'ChapterNode'; + contributionData: Scalars['JSON']['output']; + contributionStats?: Maybe; country: Scalars['String']['output']; createdAt: Scalars['Float']['output']; entityLeaders: Array; @@ -608,6 +610,8 @@ export type ProjectHealthStatsNode = { export type ProjectNode = Node & { __typename?: 'ProjectNode'; + contributionData?: Maybe; + contributionStats?: Maybe; contributorsCount: Scalars['Int']['output']; createdAt?: Maybe; entityLeaders: Array; diff --git a/frontend/src/types/__generated__/projectQueries.generated.ts b/frontend/src/types/__generated__/projectQueries.generated.ts index 8f1de3450e..2edfa9f02e 100644 --- a/frontend/src/types/__generated__/projectQueries.generated.ts +++ b/frontend/src/types/__generated__/projectQueries.generated.ts @@ -6,7 +6,7 @@ export type GetProjectQueryVariables = Types.Exact<{ }>; -export type GetProjectQuery = { project: { __typename: 'ProjectNode', id: string, contributorsCount: number, forksCount: number, issuesCount: number, isActive: boolean, key: string, languages: Array, leaders: Array, level: string, name: string, repositoriesCount: number, starsCount: number, summary: string, topics: Array, type: string, updatedAt: number, url: string, entityLeaders: Array<{ __typename: 'EntityMemberNode', id: string, description: string, memberName: string, member: { __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string } | null }>, healthMetricsList: Array<{ __typename: 'ProjectHealthMetricsNode', id: string, createdAt: any, forksCount: number, lastCommitDays: number, lastCommitDaysRequirement: number, lastReleaseDays: number, lastReleaseDaysRequirement: number, openIssuesCount: number, openPullRequestsCount: number, score: number | null, starsCount: number, unassignedIssuesCount: number, unansweredIssuesCount: number }>, recentIssues: Array<{ __typename: 'IssueNode', createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string, url: string } | null }>, recentReleases: Array<{ __typename: 'ReleaseNode', name: string, organizationName: string | null, publishedAt: any | null, repositoryName: string | null, tagName: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }>, repositories: Array<{ __typename: 'RepositoryNode', id: string, contributorsCount: number, forksCount: number, isArchived: boolean, key: string, name: string, openIssuesCount: number, starsCount: number, subscribersCount: number, url: string, organization: { __typename: 'OrganizationNode', login: string } | null }>, recentMilestones: Array<{ __typename: 'MilestoneNode', title: string, openIssuesCount: number, closedIssuesCount: number, repositoryName: string | null, organizationName: string | null, createdAt: any, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }> } | null, topContributors: Array<{ __typename: 'RepositoryContributorNode', id: string, avatarUrl: string, login: string, name: string }> }; +export type GetProjectQuery = { project: { __typename: 'ProjectNode', contributionData: any | null, contributionStats: any | null, contributorsCount: number, forksCount: number, id: string, isActive: boolean, issuesCount: number, key: string, languages: Array, leaders: Array, level: string, name: string, repositoriesCount: number, starsCount: number, summary: string, topics: Array, type: string, updatedAt: number, url: string, entityLeaders: Array<{ __typename: 'EntityMemberNode', description: string, id: string, memberName: string, member: { __typename: 'UserNode', avatarUrl: string, id: string, login: string, name: string } | null }>, healthMetricsList: Array<{ __typename: 'ProjectHealthMetricsNode', id: string, createdAt: any, forksCount: number, lastCommitDays: number, lastCommitDaysRequirement: number, lastReleaseDays: number, lastReleaseDaysRequirement: number, openIssuesCount: number, openPullRequestsCount: number, score: number | null, starsCount: number, unassignedIssuesCount: number, unansweredIssuesCount: number }>, recentIssues: Array<{ __typename: 'IssueNode', createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string, url: string } | null }>, recentReleases: Array<{ __typename: 'ReleaseNode', name: string, organizationName: string | null, publishedAt: any | null, repositoryName: string | null, tagName: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }>, repositories: Array<{ __typename: 'RepositoryNode', id: string, contributorsCount: number, forksCount: number, isArchived: boolean, key: string, name: string, openIssuesCount: number, starsCount: number, subscribersCount: number, url: string, organization: { __typename: 'OrganizationNode', login: string } | null }>, recentMilestones: Array<{ __typename: 'MilestoneNode', title: string, openIssuesCount: number, closedIssuesCount: number, repositoryName: string | null, organizationName: string | null, createdAt: any, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }> } | null, topContributors: Array<{ __typename: 'RepositoryContributorNode', id: string, avatarUrl: string, login: string, name: string }> }; export type GetProjectMetadataQueryVariables = Types.Exact<{ key: Types.Scalars['String']['input']; @@ -33,7 +33,7 @@ export type SearchProjectNamesQueryVariables = Types.Exact<{ export type SearchProjectNamesQuery = { searchProjects: Array<{ __typename: 'ProjectNode', id: string, name: string }> }; -export const GetProjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"entityLeaders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"memberName"}},{"kind":"Field","name":{"kind":"Name","value":"member"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"languages"}},{"kind":"Field","name":{"kind":"Name","value":"leaders"}},{"kind":"Field","name":{"kind":"Name","value":"level"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"healthMetricsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"30"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"lastCommitDays"}},{"kind":"Field","name":{"kind":"Name","value":"lastCommitDaysRequirement"}},{"kind":"Field","name":{"kind":"Name","value":"lastReleaseDays"}},{"kind":"Field","name":{"kind":"Name","value":"lastReleaseDaysRequirement"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"openPullRequestsCount"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"unassignedIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"unansweredIssuesCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentIssues"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentReleases"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"publishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"tagName"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"repositories"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"isArchived"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"organization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"login"}}]}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"subscribersCount"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"repositoriesCount"}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}},{"kind":"Field","name":{"kind":"Name","value":"topics"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"recentMilestones"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"closedIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPullRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"topContributors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"project"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; +export const GetProjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contributionData"}},{"kind":"Field","name":{"kind":"Name","value":"contributionStats"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"entityLeaders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"memberName"}},{"kind":"Field","name":{"kind":"Name","value":"member"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"languages"}},{"kind":"Field","name":{"kind":"Name","value":"leaders"}},{"kind":"Field","name":{"kind":"Name","value":"level"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"healthMetricsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"30"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"lastCommitDays"}},{"kind":"Field","name":{"kind":"Name","value":"lastCommitDaysRequirement"}},{"kind":"Field","name":{"kind":"Name","value":"lastReleaseDays"}},{"kind":"Field","name":{"kind":"Name","value":"lastReleaseDaysRequirement"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"openPullRequestsCount"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"unassignedIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"unansweredIssuesCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentIssues"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentReleases"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"publishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"tagName"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"repositories"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"isArchived"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"organization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"login"}}]}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"subscribersCount"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"repositoriesCount"}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}},{"kind":"Field","name":{"kind":"Name","value":"topics"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"recentMilestones"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"closedIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPullRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"topContributors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"project"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const GetProjectMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}},{"kind":"Field","name":{"kind":"Name","value":"recentMilestones"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"25"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"progress"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetTopContributorsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTopContributors"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"excludedUsernames"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"hasFullName"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}},"defaultValue":{"kind":"BooleanValue","value":false}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"20"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"topContributors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"excludedUsernames"},"value":{"kind":"Variable","name":{"kind":"Name","value":"excludedUsernames"}}},{"kind":"Argument","name":{"kind":"Name","value":"hasFullName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"hasFullName"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"project"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const SearchProjectNamesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SearchProjectNames"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"query"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"searchProjects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/card.ts b/frontend/src/types/card.ts index 787c66e4a0..119f33e08b 100644 --- a/frontend/src/types/card.ts +++ b/frontend/src/types/card.ts @@ -14,6 +14,8 @@ import type { Milestone } from 'types/milestone' import type { RepositoryCardProps } from 'types/project' import type { PullRequest } from 'types/pullRequest' import type { Release } from 'types/release' +import type { ContributionStats } from 'utils/contributionDataUtils' +import type { CardType } from 'components/CardDetailsPage' export type CardProps = { button: Button @@ -42,15 +44,19 @@ type Stats = { } export interface DetailsCardProps { accessLevel?: string + contributionData?: Record + contributionStats?: ContributionStats description?: string details?: { label: string; value: string | JSX.Element }[] domains?: string[] + endDate?: string entityLeaders?: Leader[] entityKey?: string geolocationData?: Chapter[] healthMetricsData?: HealthMetricsProps[] heatmap?: JSX.Element isActive?: boolean + startDate?: string isArchived?: boolean labels?: string[] languages?: string[] @@ -76,7 +82,7 @@ export interface DetailsCardProps { topContributors?: Contributor[] topics?: string[] tags?: string[] - type: string + type: CardType userSummary?: JSX.Element } diff --git a/frontend/src/types/chapter.ts b/frontend/src/types/chapter.ts index 7fb071b1f6..017488cbb1 100644 --- a/frontend/src/types/chapter.ts +++ b/frontend/src/types/chapter.ts @@ -3,6 +3,14 @@ import type { Leader } from 'types/leader' export type Chapter = { _geoloc?: GeoLocation + contributionData?: Record + contributionStats?: { + commits: number + issues: number + pullRequests: number + releases: number + total: number + } createdAt?: number entityLeaders?: Leader[] geoLocation?: GeoLocation diff --git a/frontend/src/types/project.ts b/frontend/src/types/project.ts index a774a4d7ba..fedccfffa7 100644 --- a/frontend/src/types/project.ts +++ b/frontend/src/types/project.ts @@ -20,6 +20,14 @@ export type ProjectStats = { export type Project = { createdAt?: string contributorsCount?: number + contributionData?: Record + contributionStats?: { + commits: number + issues: number + pullRequests: number + releases: number + total: number + } description?: string entityLeaders?: Leader[] forksCount?: number diff --git a/frontend/src/utils/aboutData.ts b/frontend/src/utils/aboutData.ts index 98356c37f5..29b63c8a7c 100644 --- a/frontend/src/utils/aboutData.ts +++ b/frontend/src/utils/aboutData.ts @@ -2,7 +2,7 @@ import type { KeyFeature, ProjectTimeline, GetInvolved, MissionContent } from 't export const missionContent: MissionContent = { mission: - 'OWASP Nest is a comprehensive platform built to enhance collaboration and streamline contributions across the OWASP community. Acting as a central hub, it helps users discover chapters and projects, find contribution opportunities, and connect with like-minded individuals based on their interests and expertise.', + 'OWASP Nest is a comprehensive, community-first platform built to enhance collaboration and contribution across the OWASP community. Acting as a central hub, it helps users discover chapters and projects, find contribution opportunities, and connect with like-minded individuals based on their interests and expertise.', whoItsFor: "OWASP Nest is designed for developers, designers, technical writers, students, security professionals, and contributors of all backgrounds. Whether you're just starting out or a seasoned OSS veteran, OWASP Nest provides intuitive tools to help you engage meaningfully in the OWASP ecosystem.", } as const diff --git a/frontend/src/utils/contributionDataUtils.ts b/frontend/src/utils/contributionDataUtils.ts new file mode 100644 index 0000000000..08927490b6 --- /dev/null +++ b/frontend/src/utils/contributionDataUtils.ts @@ -0,0 +1,46 @@ +export interface ContributionStats { + commits: number + pullRequests: number + issues: number + releases?: number + total: number +} + +export function getContributionStats( + contributionStats?: ContributionStats, + contributionData?: Record +): ContributionStats | undefined { + if (contributionStats) { + return contributionStats + } + if (contributionData && Object.keys(contributionData).length > 0) { + const total = Object.values(contributionData).reduce((sum, count) => sum + count, 0) + + return { + commits: 0, + issues: 0, + pullRequests: 0, + releases: 0, + total, + } + } + return undefined +} + +export function hasDetailedBreakdown(stats?: ContributionStats): boolean { + if (!stats) return false + + return ( + stats.commits > 0 || stats.pullRequests > 0 || stats.issues > 0 || (stats.releases || 0) > 0 + ) +} + +export function formatContributionStats(stats?: ContributionStats) { + const hasBreakdown = hasDetailedBreakdown(stats) + + return { + stats: stats || { commits: 0, pullRequests: 0, issues: 0, releases: 0, total: 0 }, + hasBreakdown, + isLegacyData: stats ? stats.total > 0 && !hasBreakdown : false, + } +} diff --git a/frontend/src/utils/dateFormatter.ts b/frontend/src/utils/dateFormatter.ts index cd706ab258..dfdb3e421c 100644 --- a/frontend/src/utils/dateFormatter.ts +++ b/frontend/src/utils/dateFormatter.ts @@ -68,3 +68,55 @@ export const formatDateForInput = (dateStr: string | number) => { } return date.toISOString().slice(0, 10) } + +export interface DateRangeOptions { + years?: number + months?: number + days?: number +} + +export interface DateRangeResult { + startDate: string + endDate: string +} + +function calculateDaysToSubtract(dayOfWeek: number): number { + return dayOfWeek === 0 ? -1 : -(dayOfWeek + 1) +} + +function adjustDateForYearOnly(today: Date, endDate: Date, startDate: Date): void { + const todayDayOfWeek = today.getDay() + const daysToSubtract = calculateDaysToSubtract(todayDayOfWeek) + + endDate.setDate(endDate.getDate() + daysToSubtract) + startDate.setTime(endDate.getTime()) + startDate.setDate(startDate.getDate() - 363) // 364 days including start day +} + +function calculateStartDate(today: Date, years: number, months: number, days: number): Date { + const startDate = new Date(today) + startDate.setFullYear(today.getFullYear() - years) + startDate.setMonth(today.getMonth() - months) + startDate.setDate(today.getDate() - days) + return startDate +} + +export function getDateRange(options: DateRangeOptions = {}): DateRangeResult { + const { years = 0, months = 0, days = 0 } = options + + const today = new Date() + today.setHours(0, 0, 0, 0) + + const endDate = new Date(today) + const startDate = calculateStartDate(today, years, months, days) + + const isYearOnly = years > 0 && months === 0 && days === 0 + if (isYearOnly) { + adjustDateForYearOnly(today, endDate, startDate) + } + + return { + startDate: startDate.toISOString().split('T')[0], + endDate: endDate.toISOString().split('T')[0], + } +} diff --git a/frontend/src/utils/metadata.ts b/frontend/src/utils/metadata.ts index 145ef60825..d174adf440 100644 --- a/frontend/src/utils/metadata.ts +++ b/frontend/src/utils/metadata.ts @@ -1,7 +1,7 @@ export const METADATA_CONFIG = { home: { description: - 'OWASP Nest is a comprehensive platform designed to enhance collaboration and contribution within the OWASP community.', + 'OWASP Nest is a comprehensive, community-first platform built to enhance collaboration and contribution across the OWASP community.', keywords: ['OWASP', 'security', 'open source', 'web security', 'application security'], pageTitle: 'Home', type: 'website', diff --git a/mkdocs.yaml b/mkdocs.yaml index a2dc701f90..f98af1e8fb 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -56,6 +56,11 @@ markdown_extensions: - attr_list - def_list - abbr + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: '!!python/name:pymdownx.superfences.fence_code_format' plugins: - search diff --git a/proxy/production.conf b/proxy/production.conf index 8555ff6a4c..08d632360d 100644 --- a/proxy/production.conf +++ b/proxy/production.conf @@ -32,6 +32,8 @@ server { proxy_cache_valid 200 1m; proxy_cache_bypass $cache_bypass $no_cache_method; proxy_no_cache $cache_bypass $no_cache_method; + + include /etc/nginx/headers.conf; add_header X-Cache-Status $upstream_cache_status; } @@ -46,6 +48,8 @@ server { proxy_cache_valid 200 1m; proxy_cache_bypass $cache_bypass $no_cache_method; proxy_no_cache $cache_bypass $no_cache_method; + + include /etc/nginx/headers.conf; add_header X-Cache-Status $upstream_cache_status; } } diff --git a/proxy/staging.conf b/proxy/staging.conf index be896373e7..d1fc41a1ee 100644 --- a/proxy/staging.conf +++ b/proxy/staging.conf @@ -32,6 +32,8 @@ server { proxy_cache_valid 200 1m; proxy_cache_bypass $cache_bypass $no_cache_method; proxy_no_cache $cache_bypass $no_cache_method; + + include /etc/nginx/headers.conf; add_header X-Cache-Status $upstream_cache_status; } @@ -46,6 +48,8 @@ server { proxy_cache_valid 200 1m; proxy_cache_bypass $cache_bypass $no_cache_method; proxy_no_cache $cache_bypass $no_cache_method; + + include /etc/nginx/headers.conf; add_header X-Cache-Status $upstream_cache_status; }