diff --git a/.asf.yaml b/.asf.yaml index a99d2462db09c..b8fb1be32036f 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -171,6 +171,7 @@ github: - gyli - jroachgolf84 - Dev-iL + - kacpermuda notifications: jobs: jobs@airflow.apache.org diff --git a/.codespellignorelines b/.codespellignorelines index 1234698be3071..5e8e365086240 100644 --- a/.codespellignorelines +++ b/.codespellignorelines @@ -4,3 +4,4 @@ The platform supports **C**reate, **R**ead, **U**pdate, and **D**elete operations on most resources.
Code block\ndoes not\nrespect\nnewlines\n
"trough", + assert "task_instance_id" in route.dependant.path_param_names, ( diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 16e6a3ca298bb..dc6469419db37 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -116,7 +116,7 @@ airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/ @Lee-W @jason810496 @guan # Dev tools /.github/workflows/ @potiuk @ashb @gopidesupavan @amoghrajesh @jscheffl @bugraoz93 @kaxil @jason810496 -/dev/ @potiuk @ashb @gopidesupavan @amoghrajesh @jscheffl @bugraoz93 @jason810496 @jedcunningham @ephraimbuddy +/dev/ @potiuk @ashb @gopidesupavan @amoghrajesh @jscheffl @bugraoz93 @jason810496 @jedcunningham @ephraimbuddy @choo121600 /dev/react-plugin-tools/ @pierrejeambrun @bbovenzi /docker-tests/ @potiuk @ashb @gopidesupavan @jason810496 /kubernetes-tests/ @potiuk @ashb @gopidesupavan @jason810496 diff --git a/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml b/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml index 08eea11292fa1..40bd08c2ec2d2 100644 --- a/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml @@ -38,7 +38,7 @@ body: for more information. multiple: false options: - - "3.1.7" + - "3.1.8" - "2.11.X" - "main (development)" - "Other Airflow 3 version (please specify below)" diff --git a/.github/actions/breeze/action.yml b/.github/actions/breeze/action.yml index 53d94793c0dc0..bea8ff3a36003 100644 --- a/.github/actions/breeze/action.yml +++ b/.github/actions/breeze/action.yml @@ -24,7 +24,7 @@ inputs: default: "3.10" uv-version: description: 'uv version to use' - default: "0.10.8" # Keep this comment to allow automatic replacement of uv version + default: "0.10.9" # Keep this comment to allow automatic replacement of uv version outputs: host-python-version: description: Python version used in host diff --git a/.github/actions/install-prek/action.yml b/.github/actions/install-prek/action.yml index 00821eb44c797..ffa5cd2fd13b1 100644 --- a/.github/actions/install-prek/action.yml +++ b/.github/actions/install-prek/action.yml @@ -24,10 +24,10 @@ inputs: default: "3.10" uv-version: description: 'uv version to use' - default: "0.10.8" # Keep this comment to allow automatic replacement of uv version + default: "0.10.9" # Keep this comment to allow automatic replacement of uv version prek-version: description: 'prek version to use' - default: "0.3.4" # Keep this comment to allow automatic replacement of prek version + default: "0.3.5" # Keep this comment to allow automatic replacement of prek version save-cache: description: "Whether to save prek cache" required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9c9a16c3cc2b5..1362bc6ae6cc4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,6 +24,10 @@ updates: schedule: # Check for updates to GitHub Actions every week interval: "weekly" + groups: + github-actions-updates: + patterns: + - "*" - package-ecosystem: "github-actions" directory: "/" @@ -33,6 +37,10 @@ updates: # Check for updates to GitHub Actions every week interval: "weekly" target-branch: v3-1-test + groups: + github-actions-updates: + patterns: + - "*" - package-ecosystem: "github-actions" directory: "/" @@ -42,6 +50,10 @@ updates: # Check for updates to GitHub Actions every week interval: "weekly" target-branch: v2-11-test + groups: + github-actions-updates: + patterns: + - "*" - package-ecosystem: pip cooldown: @@ -69,15 +81,35 @@ updates: directories: - /airflow-core/src/airflow/ui schedule: - interval: daily + interval: "weekly" groups: + react: + patterns: + - "react" + - "react-dom" + - "@types/react" + - "@types/react-dom" + chakra-ui: + patterns: + - "@chakra-ui/*" + - "@emotion/*" + - "framer-motion" + eslint: + patterns: + - "eslint*" + - "@eslint/*" + typescript: + patterns: + - "typescript*" + - "@typescript-eslint/*" + - "@types/typescript" core-ui-package-updates: patterns: - "*" update-types: - "minor" - "patch" - major-version-updates: + core-ui-major-version-updates: patterns: - "*" applies-to: security-updates @@ -90,15 +122,15 @@ updates: directories: - /airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui schedule: - interval: daily + interval: "weekly" groups: - core-ui-package-updates: + auth-ui-package-updates: patterns: - "*" update-types: - "minor" - "patch" - major-version-updates: + auth-ui-major-version-updates: patterns: - "*" applies-to: security-updates @@ -111,7 +143,7 @@ updates: directories: - /dev/react-plugin-tools/react_plugin_template schedule: - interval: daily + interval: "weekly" groups: ui-plugin-template-package-updates: patterns: @@ -119,12 +151,9 @@ updates: update-types: - "minor" - "patch" - ui-plugin-template-major-version-updates: - patterns: - - "*" - applies-to: security-updates - update-types: - - "major" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] - package-ecosystem: npm cooldown: @@ -132,7 +161,7 @@ updates: directories: - /providers/edge3/src/airflow/providers/edge3/plugins/www schedule: - interval: daily + interval: "weekly" groups: edge-ui-package-updates: patterns: @@ -150,6 +179,27 @@ updates: patterns: - "*" + - package-ecosystem: npm + cooldown: + default-days: 4 + directories: + - /registry + schedule: + interval: "weekly" + groups: + registry-package-updates: + patterns: + - "*" + update-types: + - "minor" + - "patch" + registry-major-version-updates: + patterns: + - "*" + applies-to: security-updates + update-types: + - "major" + # Repeat dependency updates on v3-1-test branch as well - package-ecosystem: pip cooldown: @@ -178,10 +228,10 @@ updates: directories: - /airflow-core/src/airflow/ui schedule: - interval: daily + interval: "weekly" target-branch: v3-1-test groups: - core-ui-package-updates: + 3-1-core-ui-package-updates: patterns: - "*" update-types: @@ -197,10 +247,10 @@ updates: directories: - /airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui schedule: - interval: daily + interval: "weekly" target-branch: v3-1-test groups: - core-ui-package-updates: + 3-1-auth-ui-package-updates: patterns: - "*" update-types: @@ -220,7 +270,7 @@ updates: - /docker_tests - / schedule: - interval: daily + interval: "weekly" target-branch: v2-11-test groups: pip-dependency-updates: @@ -233,12 +283,15 @@ updates: directories: - /airflow/www/ schedule: - interval: daily + interval: "weekly" target-branch: v2-11-test groups: - core-ui-package-updates: + legacy-ui-package-updates: patterns: - "*" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] - package-ecosystem: "uv" cooldown: diff --git a/.github/skills/airflow-translations/locales/el.md b/.github/skills/airflow-translations/locales/el.md new file mode 100644 index 0000000000000..10d2603d974ce --- /dev/null +++ b/.github/skills/airflow-translations/locales/el.md @@ -0,0 +1,238 @@ + + +# Greek (el) Translation Agent Skill + +**Locale code:** `el` +**Preferred variant:** Standard Modern Greek (el), consistent with existing translations in `airflow-core/src/airflow/ui/public/i18n/locales/el/` + +This file contains locale-specific guidelines so AI translation agents produce +new Greek strings that stay 100% consistent with the existing translations. + +## 1. Core Airflow Terminology + +The following terms **must remain in English unchanged** (case-sensitive): + +- `Dag` / `Dags` — Airflow concept; never write "DAG" +- `XCom` / `XComs` — Airflow cross-communication mechanism +- `Backfill` / `Backfills` — Historical data fill-in; kept as a recognizable technical term +- `Pool` / `Pools` — Resource constraint mechanism +- `Slot` / `Slots` — Pool slot count +- `Map Index` — Task mapping index +- `PID` — Unix process identifier +- `ID` — Universal abbreviation +- `UTC` — Time standard +- `JSON` — Standard technical format name +- `REST API` — Standard technical term +- `URI` — Uniform Resource Identifier +- `Gantt` — Chart type name +- `Catchup` — Dag scheduling catchup setting +- Log levels: `INFO`, `DEBUG` (Note: `CRITICAL`, `ERROR`, `WARNING` are translated — see § 3) + +## 2. Standard Translations + +The following Airflow-specific terms have established Greek translations +that **must be used consistently**: + +| English Term | Greek Translation | Notes | +| ---------------------- | ------------------------------------- | --------------------------------------------------- | +| Task | Εργασία | Plural: "Εργασίες" | +| Task Instance | Εκτέλεση Εργασίας | Plural: "Εκτελέσεις Εργασίας" | +| Task Group | Ομάδα Εργασιών | | +| Dag Run | Εκτέλεση Dag | Plural: "Εκτελέσεις Dag" | +| Run | Εκτέλεση | Plural: "Εκτελέσεις"; used standalone | +| Trigger (noun) | Ενεργοποίηση | | +| Trigger Rule | Κανόνας Ενεργοποίησης | | +| Triggerer | Ενεργοποιητής | Component name | +| Scheduler | Προγραμματιστής | | +| Schedule (noun) | Πρόγραμμα | | +| Executor | Εκτελεστής | | +| Connection | Σύνδεση | Plural: "Συνδέσεις" | +| Variable | Μεταβλητή | Plural: "Μεταβλητές" | +| Audit Log | Καταγραφή Ελέγχου | | +| Log | Καταγραφή | | +| State | Κατάσταση | | +| Queue (noun) | Ουρά | e.g., "Σε Ουρά" for "queued" | +| Config / Configuration | Ρυθμίσεις | | +| Operator | Τελεστής | Plural: "Τελεστές" | +| Asset | Οντότητα | Plural: "Οντότητες" — translated (Greek-specific) | +| Asset Event | Συμβάν Οντότητας | Plural: "Συμβάντα Οντοτήτων" | +| Plugin | Πρόσθετο | Plural: "Πρόσθετα" | +| Provider | Πάροχος | Plural: "Πάροχοι" | +| Dag Processor | Επεξεργαστής Dag | Component name | +| Heartbeat | Παλμός | | +| Map Index | Δείκτης Χάρτη | | +| Upstream (dependency) | Ανάντη | Used in states: "Αποτυχία Ανάντη" | +| Upstream (action) | Άνοδος | Used in clear-task action options | +| Downstream (action) | Κάθοδος | Used in clear-task action options | + +> **Note on `Asset`:** Unlike French, Catalan, and other locales where "Asset" is kept in +> English, Greek translates it as **Οντότητα** ("entity"). Use "Οντότητα" consistently +> across all Greek translations. + +## 3. Task/Run States and Log Levels + +### States + +| English State | Greek Translation | +| ------------------- | ---------------------------- | +| running | Εκτελείται | +| failed | Αποτυχία | +| success | Επιτυχία | +| queued | Σε Ουρά | +| scheduled | Προγραμματισμένο | +| skipped | Παραλείφθηκε | +| deferred | Αναβληθέν | +| removed | Αφαιρέθηκε | +| restarting | Επανεκκίνηση | +| up_for_retry | Προς Επανάληψη | +| up_for_reschedule | Προς Επαναπρογραμματισμό | +| upstream_failed | Αποτυχία Ανάντη | +| no_status / none | Χωρίς Κατάσταση | +| planned | Προγραμματισμένο | + +### Log Levels + +| English Level | Greek Translation | +| ------------- | ------------------ | +| CRITICAL | ΚΡΙΣΙΜΟ | +| ERROR | ΣΦΑΛΜΑ | +| WARNING | ΠΡΟΕΙΔΟΠΟΙΗΣΗ | +| INFO | INFO | +| DEBUG | DEBUG | + +## 4. Greek-Specific Guidelines + +### Tone and Register + +- Use **formal Greek** ("εσείς/σας" form). Do not use the informal "εσύ/σου". +- Use a neutral, professional tone suitable for technical software UIs. +- Keep UI strings concise — they appear in buttons, labels, and tooltips. + +### Grammatical Gender + +Greek nouns have three genders: masculine (αρσενικό), feminine (θηλυκό), and neuter (ουδέτερο). +Match adjectives and articles accordingly: + +- "Dag" is treated as **neuter**: "το Dag", "ένα Dag" +- "Εργασία" (Task) is **feminine**: "η εργασία", "μια εργασία" +- "Εκτέλεση" (Run/Execution) is **feminine**: "η εκτέλεση", "μια εκτέλεση" +- "Σύνδεση" (Connection) is **feminine**: "η σύνδεση" +- "Μεταβλητή" (Variable) is **feminine**: "η μεταβλητή" +- "Οντότητα" (Asset) is **feminine**: "η οντότητα" +- "Καταγραφή" (Log) is **feminine**: "η καταγραφή" +- "Κατάσταση" (State) is **feminine**: "η κατάσταση" + +### Plural Forms + +Greek uses i18next plural suffixes `_one` and `_other`. Use the established +plural forms from the glossary: + +```json +"task_one": "Εργασία", +"task_other": "Εργασίες" +``` + +```json +"dagRun_one": "Εκτέλεση Dag", +"dagRun_other": "Εκτελέσεις Dag" +``` + +```json +"asset_one": "Οντότητα", +"asset_other": "Οντότητες" +``` + +### Genitive Case + +Greek uses the genitive case to express "of X" relationships. This commonly appears +in compound terms where English uses a noun modifier: + +```json +"dagRunId": "ID Εκτέλεσης Dag" // "ID of Run of Dag" +"taskGroup": "Ομάδα Εργασιών" // "Group of Tasks" +"auditLog": "Καταγραφή Ελέγχου" // "Log of Audit" +"assetEvent_one":"Συμβάν Οντότητας" // "Event of Asset" +"triggerRule": "Κανόνας Ενεργοποίησης" // "Rule of Triggering" +``` + +### Capitalization + +- Use **sentence case** for descriptions and longer strings. +- Use **title-like capitalization** for headers, labels, and button text + (match the style of existing Greek translations). +- Capitalize proper terms: "Dag", "XCom", "Backfill", "Pool", etc. + +### Diacritics + +Greek uses the **tonos** accent mark (΄). Always preserve accented characters — +missing diacritics change the meaning or make text unreadable. Never write +"Εκτελεση", "Συνδεση", "Εργασια", "Οντοτητα", "Καταγραφη", "Κατασταση" etc. +Always use the correctly accented forms from §2. + +### Question Mark + +The Greek question mark is the **erotimatiko** (`;`), which looks like a semicolon. +When translating English `?`, use `;` in Greek sentences: + +```json +"confirmation": "Είστε σίγουροι ότι θέλετε να διαγράψετε το {{resourceName}}; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί." +``` + +### Placeholders and Variables + +Preserve all `{{variable}}` placeholders exactly — never translate placeholder names. +Reorder phrases for natural Greek word order when needed. + +## 5. Terminology Reference + +The established Greek translations are defined in the existing locale files. +Before translating, **read the existing el JSON files** to learn the +established terminology: + +``` +airflow-core/src/airflow/ui/public/i18n/locales/el/ +``` + +Use the translations found in these files as the authoritative glossary. When +translating a term, check how it has been translated elsewhere in the locale to +maintain consistency. If a term has not been translated yet, refer to the English +source in `en/` and apply the rules in this document. + +### Action Verbs (Buttons) + +``` +Add → "Προσθήκη" +Delete → "Διαγραφή" +Edit → "Επεξεργασία" +Save → "Αποθήκευση" +Reset → "Επαναφορά" +Cancel → "Ακύρωση" +Confirm → "Επιβεβαίωση" +Clear → "Εκκαθάριση" +Search → "Αναζήτηση" +Copy → "Αντιγραφή" +``` + +## 6. Agent Instructions (DO / DON'T) + +**DO:** + +- Use formal Greek ("εσείς/σας" form) throughout +- Preserve all i18next placeholders: `{{count}}`, `{{dagName}}`, `{{type}}`, etc. +- Apply correct Greek genitive case for "of X" constructions +- Provide `_one` and `_other` suffixes for every plural key +- Translate "Asset" as "Οντότητα" (Greek-specific — not kept in English) +- Translate log levels: "ΚΡΙΣΙΜΟ", "ΣΦΑΛΜΑ", "ΠΡΟΕΙΔΟΠΟΙΗΣΗ" for CRITICAL, ERROR, WARNING +- Preserve Greek diacritics (tonos accent) on all Greek words +- Use `;` (erotimatiko) for question marks in Greek sentences + +**DON'T:** + +- Write "DAG" — always use "Dag" +- Translate `XCom`, `Backfill`, `Pool`, `Slot`, `ID`, `JSON`, `REST API`, `UTC`, `PID` +- Translate `{{variable}}` placeholder names +- Drop Greek diacritics (e.g., never write "Εκτελεση" for "Εκτέλεση") +- Use the informal "εσύ" form +- Translate "INFO" or "DEBUG" log level labels +- Use English `?` for question marks in Greek sentences diff --git a/.github/workflows/additional-ci-image-checks.yml b/.github/workflows/additional-ci-image-checks.yml index 0dfd92b793ee8..6a33cea24a937 100644 --- a/.github/workflows/additional-ci-image-checks.yml +++ b/.github/workflows/additional-ci-image-checks.yml @@ -135,7 +135,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Install Breeze" diff --git a/.github/workflows/additional-prod-image-tests.yml b/.github/workflows/additional-prod-image-tests.yml index 94c6f1d933694..ad3f60042c920 100644 --- a/.github/workflows/additional-prod-image-tests.yml +++ b/.github/workflows/additional-prod-image-tests.yml @@ -119,7 +119,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 2 persist-credentials: false @@ -157,7 +157,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 2 persist-credentials: false @@ -188,7 +188,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 2 persist-credentials: false @@ -226,6 +226,17 @@ jobs: use-uv: ${{ inputs.use-uv }} e2e_test_mode: "remote_log" + test-e2e-integration-tests-xcom-object-storage: + name: "XCom object storage backend tests with PROD image" + uses: ./.github/workflows/airflow-e2e-tests.yml + with: + workflow-name: "XCom object storage backend e2e test" + runners: ${{ inputs.runners }} + platform: ${{ inputs.platform }} + default-python-version: "${{ inputs.default-python-version }}" + use-uv: ${{ inputs.use-uv }} + e2e_test_mode: "xcom_object_storage" + test-ui-e2e-chromium: name: "Chromium UI e2e tests with PROD image" uses: ./.github/workflows/ui-e2e-tests.yml @@ -277,7 +288,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 2 persist-credentials: false diff --git a/.github/workflows/airflow-distributions-tests.yml b/.github/workflows/airflow-distributions-tests.yml index 1b7fd9adc351b..53692b5d87350 100644 --- a/.github/workflows/airflow-distributions-tests.yml +++ b/.github/workflows/airflow-distributions-tests.yml @@ -90,7 +90,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Prepare breeze & CI image: ${{ matrix.python-version }}" diff --git a/.github/workflows/airflow-e2e-tests.yml b/.github/workflows/airflow-e2e-tests.yml index be0c0381ed040..e9d9811ae2977 100644 --- a/.github/workflows/airflow-e2e-tests.yml +++ b/.github/workflows/airflow-e2e-tests.yml @@ -49,7 +49,7 @@ on: # yamllint disable-line rule:truthy type: string required: true e2e_test_mode: - description: "Test mode - basic or remote_log" + description: "Test mode - basic, remote_log, or xcom_object_storage" type: string default: "basic" @@ -80,7 +80,7 @@ on: # yamllint disable-line rule:truthy type: string default: "" e2e_test_mode: - description: "Test mode - quick or full" + description: "Test mode - basic, remote_log, or xcom_object_storage" type: string default: "basic" @@ -100,7 +100,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 2 persist-credentials: false diff --git a/.github/workflows/automatic-backport.yml b/.github/workflows/automatic-backport.yml index 986c31cae4a51..2abe2b8f3824b 100644 --- a/.github/workflows/automatic-backport.yml +++ b/.github/workflows/automatic-backport.yml @@ -45,7 +45,7 @@ jobs: - name: Find PR information id: pr-info - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/backport-cli.yml b/.github/workflows/backport-cli.yml index 42f8178868267..9ed750a5fbc57 100644 --- a/.github/workflows/backport-cli.yml +++ b/.github/workflows/backport-cli.yml @@ -53,7 +53,7 @@ jobs: steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" id: checkout-for-backport - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: true fetch-depth: 0 diff --git a/.github/workflows/basic-tests.yml b/.github/workflows/basic-tests.yml index d197df3e74612..982350dea7e1d 100644 --- a/.github/workflows/basic-tests.yml +++ b/.github/workflows/basic-tests.yml @@ -70,7 +70,7 @@ on: # yamllint disable-line rule:truthy type: string uv-version: description: 'uv version to use' - default: "0.10.8" # Keep this comment to allow automatic replacement of uv version + default: "0.10.9" # Keep this comment to allow automatic replacement of uv version type: string platform: description: 'Platform for the build - linux/amd64 or linux/arm64' @@ -87,7 +87,7 @@ jobs: - name: "Cleanup repo" shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # Need to fetch all history for selective checks tests fetch-depth: 0 @@ -106,7 +106,7 @@ jobs: - name: "Cleanup repo" shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false @@ -115,7 +115,7 @@ jobs: - name: "Install SVN" run: sudo apt-get update && sudo apt-get install -y subversion - name: "Install Java (for Apache RAT)" - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: distribution: 'temurin' java-version: '17' @@ -134,7 +134,7 @@ jobs: runs-on: ${{ fromJSON(inputs.runners) }} steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 persist-credentials: false @@ -155,16 +155,16 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup pnpm - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 9 run_install: false - name: "Setup node" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 21 cache: 'pnpm' @@ -217,7 +217,7 @@ jobs: runs-on: ${{ fromJSON(inputs.runners) }} steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Install Breeze" @@ -238,7 +238,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Install Breeze" @@ -252,7 +252,7 @@ jobs: platform: ${{ inputs.platform }} save-cache: true - name: Fetch incoming commit ${{ github.sha }} with its parent - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.sha }} fetch-depth: 2 @@ -273,7 +273,7 @@ jobs: runs-on: ["windows-2025"] steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 2 persist-credentials: false @@ -290,7 +290,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Install Breeze" @@ -316,7 +316,7 @@ jobs: run: > prek --all-files --show-diff-on-failure --color always --verbose - --hook-stage manual + --stage manual update-chart-dependencies if: always() # For UV we are not failing the upgrade installers check if it is updated because @@ -326,7 +326,7 @@ jobs: run: > prek --all-files --show-diff-on-failure --color always --verbose - --hook-stage manual upgrade-important-versions || true + --stage manual upgrade-important-versions || true if: always() env: UPGRADE_FLIT: "false" @@ -346,7 +346,7 @@ jobs: run: | if ! prek \ --all-files --show-diff-on-failure --color always --verbose \ - --hook-stage manual upgrade-important-versions; then + --stage manual upgrade-important-versions; then echo -e "\n\033[0;31mThere are changes in prek hooks after upgrade check.\033[0m" echo -e "\n\033[0;33mHow to fix:\033[0m Run \`breeze ci upgrade\` locally to fix it!.\n" exit 1 @@ -376,7 +376,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Install Breeze" @@ -423,7 +423,7 @@ jobs: FORCE_COLOR: 1 steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Install uv" diff --git a/.github/workflows/ci-amd-arm.yml b/.github/workflows/ci-amd-arm.yml index 7a29f4b4c91f5..081beea5769ea 100644 --- a/.github/workflows/ci-amd-arm.yml +++ b/.github/workflows/ci-amd-arm.yml @@ -40,7 +40,7 @@ env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_USERNAME: ${{ github.actor }} SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - UV_VERSION: "0.10.8" # Keep this comment to allow automatic replacement of uv version + UV_VERSION: "0.10.9" # Keep this comment to allow automatic replacement of uv version VERBOSE: "true" concurrency: @@ -142,11 +142,11 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Fetch incoming commit ${{ github.sha }} with its parent - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.sha }} fetch-depth: 2 @@ -218,7 +218,7 @@ jobs: timeout-minutes: 10 steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Install uv" @@ -830,12 +830,12 @@ jobs: VERBOSE: "true" steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # keep this in sync with go.mod in go-sdk/ - name: Setup Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.24 cache-dependency-path: go-sdk/go.sum @@ -971,19 +971,19 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Free up disk space" shell: bash run: ./scripts/tools/free_up_disk_space.sh - name: "Download all test warning artifacts from the current build" - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: path: ./artifacts pattern: test-warnings-* - name: "Setup python" - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "${{ inputs.default-python-version }}" - name: "Summarize all warnings" diff --git a/.github/workflows/ci-image-build.yml b/.github/workflows/ci-image-build.yml index 2083f7bfa28c7..d1415ed53fcf8 100644 --- a/.github/workflows/ci-image-build.yml +++ b/.github/workflows/ci-image-build.yml @@ -118,7 +118,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout target branch" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Free up disk space" diff --git a/.github/workflows/ci-image-checks.yml b/.github/workflows/ci-image-checks.yml index b24995e032c65..126bf9fd1f748 100644 --- a/.github/workflows/ci-image-checks.yml +++ b/.github/workflows/ci-image-checks.yml @@ -136,7 +136,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Prepare breeze & CI image: ${{ inputs.default-python-version }}" @@ -184,7 +184,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Free up disk space" @@ -206,7 +206,7 @@ jobs: platform: ${{ inputs.platform }} save-cache: false - name: "MyPy checks for ${{ matrix.mypy-check }}" - run: prek --color always --verbose --hook-stage manual "$MYPY_CHECK" --all-files + run: prek --color always --verbose --stage manual "$MYPY_CHECK" --all-files env: VERBOSE: "false" COLUMNS: "202" @@ -238,7 +238,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Prepare breeze & CI image: ${{ inputs.default-python-version }}" @@ -301,7 +301,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Prepare breeze & CI image: ${{ inputs.default-python-version }}" @@ -312,7 +312,7 @@ jobs: use-uv: ${{ inputs.use-uv }} make-mnt-writeable-and-cleanup: true - name: "Download docs prepared as artifacts" - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: airflow-docs path: './generated/_build' @@ -358,7 +358,7 @@ jobs: inputs.canary-run == 'true' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@010d0da01d0b5a38af31e9c3470dbfdabdecca3a # v4.0.1 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }} @@ -393,12 +393,12 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 2 persist-credentials: false - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: "apache/airflow-client-python" fetch-depth: 1 diff --git a/.github/workflows/ci-notification.yml b/.github/workflows/ci-notification.yml index 44582376fe718..e009e380e9277 100644 --- a/.github/workflows/ci-notification.yml +++ b/.github/workflows/ci-notification.yml @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4e2b1c1a68571..0c21c6e43d652 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,20 +47,20 @@ jobs: security-events: write steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: # Provide more context to the SARIF output (shows up in run.automationDetails.id field) category: "/language:${{matrix.language}}" diff --git a/.github/workflows/finalize-tests.yml b/.github/workflows/finalize-tests.yml index 6a6ae7d6b2e99..e814751f1cc79 100644 --- a/.github/workflows/finalize-tests.yml +++ b/.github/workflows/finalize-tests.yml @@ -99,7 +99,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # Needed to perform push action persist-credentials: false @@ -107,14 +107,14 @@ jobs: id: constraints-branch run: ./scripts/ci/constraints/ci_branch_constraints.sh >> ${GITHUB_OUTPUT} - name: Checkout ${{ steps.constraints-branch.outputs.branch }} - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: path: "constraints" ref: ${{ steps.constraints-branch.outputs.branch }} persist-credentials: true fetch-depth: 0 - name: "Download constraints from the constraints generated by build CI image" - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: pattern: constraints-* path: ./files @@ -146,7 +146,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Prepare breeze & CI image: ${{ matrix.python-version }}" diff --git a/.github/workflows/generate-constraints.yml b/.github/workflows/generate-constraints.yml index dad454419b962..83ba64e296738 100644 --- a/.github/workflows/generate-constraints.yml +++ b/.github/workflows/generate-constraints.yml @@ -77,7 +77,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Install prek" diff --git a/.github/workflows/helm-tests.yml b/.github/workflows/helm-tests.yml index 38725b59e244f..33eac7c0ed536 100644 --- a/.github/workflows/helm-tests.yml +++ b/.github/workflows/helm-tests.yml @@ -76,7 +76,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Prepare breeze & CI image: ${{ inputs.default-python-version }}" @@ -107,7 +107,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Install Breeze" diff --git a/.github/workflows/integration-system-tests.yml b/.github/workflows/integration-system-tests.yml index 8d117636402b0..1772ecfac6bc3 100644 --- a/.github/workflows/integration-system-tests.yml +++ b/.github/workflows/integration-system-tests.yml @@ -98,7 +98,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Prepare breeze & CI image: ${{ inputs.default-python-version }}" @@ -149,7 +149,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Prepare breeze & CI image: ${{ inputs.default-python-version }}" @@ -194,7 +194,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Prepare breeze & CI image: ${{ inputs.default-python-version }}" diff --git a/.github/workflows/k8s-tests.yml b/.github/workflows/k8s-tests.yml index bdcfdf79a9f71..ac86ac2ee4497 100644 --- a/.github/workflows/k8s-tests.yml +++ b/.github/workflows/k8s-tests.yml @@ -81,7 +81,7 @@ jobs: echo "PYTHON_MAJOR_MINOR_VERSION=${KUBERNETES_COMBO}" | sed 's/-.*//' >> $GITHUB_ENV echo "KUBERNETES_VERSION=${KUBERNETES_COMBO}" | sed 's/=[^-]*-/=/' >> $GITHUB_ENV - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Free up disk space" diff --git a/.github/workflows/milestone-tag-assistant.yml b/.github/workflows/milestone-tag-assistant.yml index 18fd030e9b98a..dd902b8e17da8 100644 --- a/.github/workflows/milestone-tag-assistant.yml +++ b/.github/workflows/milestone-tag-assistant.yml @@ -50,7 +50,7 @@ jobs: - name: Find PR information id: pr-info - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -99,7 +99,7 @@ jobs: steps: - name: "Checkout repository" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # Always checkout main to ensure Breeze with set-milestone command is available diff --git a/.github/workflows/prod-image-build.yml b/.github/workflows/prod-image-build.yml index e66caf80a1ce7..3e5666b313947 100644 --- a/.github/workflows/prod-image-build.yml +++ b/.github/workflows/prod-image-build.yml @@ -125,7 +125,7 @@ jobs: run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" if: inputs.upload-package-artifact == 'true' - name: "Checkout target branch" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Make /mnt writeable" @@ -220,7 +220,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout target branch" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Make /mnt writeable" @@ -231,17 +231,17 @@ jobs: shell: bash run: rm -fv ./dist/* ./docker-context-files/* - name: "Download packages prepared as artifacts" - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: prod-packages path: ./docker-context-files - - name: "Show downloaded packages" - run: ls -la ./docker-context-files - name: "Download constraints" - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: - pattern: constraints-* - path: ./docker-context-files + name: constraints-${{ matrix.python-version }} + path: ./docker-context-files/constraints-${{ matrix.python-version }} + - name: "Show downloaded files" + run: ls -R ./docker-context-files - name: "Show constraints" run: | for file in ./docker-context-files/constraints*/constraints*.txt diff --git a/.github/workflows/publish-docs-to-s3.yml b/.github/workflows/publish-docs-to-s3.yml index 56e9f7e21b70b..f70995ef5d30f 100644 --- a/.github/workflows/publish-docs-to-s3.yml +++ b/.github/workflows/publish-docs-to-s3.yml @@ -193,7 +193,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout current version first to clean-up stuff" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false path: current-version @@ -210,7 +210,7 @@ jobs: # This will take longer as we need to rebuild CI image and it will not use cache # but it will build the CI image from the version of Airflow that is used to check out things - name: "Checkout ${{ inputs.ref }} " - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false ref: ${{ inputs.ref }} @@ -304,7 +304,7 @@ jobs: # but it will build the CI image from the version of Airflow that is used to check out things # We also fetch the whole history to be able to prepare SBOM files - name: "Checkout current version with all history for SBOM" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false fetch-depth: 0 @@ -314,7 +314,7 @@ jobs: - name: "Install Breeze" uses: ./.github/actions/breeze - name: "Download docs prepared as artifacts" - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: airflow-docs path: /mnt/_build @@ -376,7 +376,7 @@ jobs: sudo /tmp/aws/install --update rm -rf /tmp/aws/ - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@010d0da01d0b5a38af31e9c3470dbfdabdecca3a # v4.0.1 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/push-image-cache.yml b/.github/workflows/push-image-cache.yml index 1ac5253a0305a..a89fe782f275b 100644 --- a/.github/workflows/push-image-cache.yml +++ b/.github/workflows/push-image-cache.yml @@ -114,7 +114,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Free up disk space" @@ -184,7 +184,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Free up disk space" @@ -194,7 +194,7 @@ jobs: - name: "Cleanup dist and context file" run: rm -fv ./dist/* ./docker-context-files/* - name: "Download packages prepared as artifacts" - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: prod-packages path: ./docker-context-files diff --git a/.github/workflows/registry-backfill.yml b/.github/workflows/registry-backfill.yml new file mode 100644 index 0000000000000..fa4f7924df3b4 --- /dev/null +++ b/.github/workflows/registry-backfill.yml @@ -0,0 +1,266 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +--- +name: Registry Backfill +on: # yamllint disable-line rule:truthy + workflow_dispatch: + inputs: + destination: + description: > + Publish to live or staging S3 bucket + required: true + type: choice + options: + - staging + - live + default: staging + providers: + description: > + Space-separated provider IDs + (e.g. 'amazon google databricks') + required: true + type: string + versions: + description: > + Space-separated versions to backfill + (e.g. '9.15.0 9.14.0'). Applied to ALL providers. + required: true + type: string + +permissions: + contents: read + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + bucket: ${{ steps.destination.outputs.bucket }} + steps: + - name: "Build provider matrix" + id: matrix + env: + PROVIDERS: ${{ inputs.providers }} + run: | + MATRIX=$(echo "${PROVIDERS}" \ + | tr ' ' '\n' | jq -R . \ + | jq -cs '{"provider": .}') + echo "matrix=${MATRIX}" >> "${GITHUB_OUTPUT}" + + - name: "Determine S3 destination" + id: destination + env: + DESTINATION: ${{ inputs.destination }} + run: | + if [[ "${DESTINATION}" == "live" ]]; then + URL="s3://live-docs-airflow-apache-org" + else + URL="s3://staging-docs-airflow-apache-org" + fi + echo "bucket=${URL}/registry/" \ + >> "${GITHUB_OUTPUT}" + + backfill: + needs: prepare + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }} + name: "Backfill ${{ matrix.provider }}" + if: > + contains(fromJSON('[ + "ashb", + "bugraoz93", + "eladkal", + "ephraimbuddy", + "jedcunningham", + "jscheffl", + "kaxil", + "pierrejeambrun", + "shahar1", + "potiuk", + "utkarsharma2", + "vincbeck" + ]'), github.event.sender.login) + steps: + - name: "Checkout repository" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 + + - name: "Fetch provider tags" + env: + VERSIONS: ${{ inputs.versions }} + PROVIDER: ${{ matrix.provider }} + run: | + for VERSION in ${VERSIONS}; do + TAG="providers-${PROVIDER}/${VERSION}" + echo "Fetching tag: ${TAG}" + git fetch origin tag "${TAG}" \ + 2>/dev/null || echo "Tag not found" + done + + - name: "Install uv" + uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v7.4.0 + + - name: "Install Breeze" + uses: ./.github/actions/breeze + with: + python-version: "3.12" + + - name: "Install AWS CLI v2" + run: | + curl -sSf \ + "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" \ + -o /tmp/awscliv2.zip + unzip -q /tmp/awscliv2.zip -d /tmp + rm /tmp/awscliv2.zip + sudo /tmp/aws/install --update + rm -rf /tmp/aws/ + + - name: "Configure AWS credentials" + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 + with: + aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-2 + + - name: "Download existing providers.json" + env: + S3_BUCKET: ${{ needs.prepare.outputs.bucket }} + run: | + aws s3 cp \ + "${S3_BUCKET}api/providers.json" \ + dev/registry/providers.json || true + + - name: "Extract version metadata from git tags" + env: + VERSIONS: ${{ inputs.versions }} + PROVIDER: ${{ matrix.provider }} + run: | + VERSION_ARGS="" + for VERSION in ${VERSIONS}; do + VERSION_ARGS="${VERSION_ARGS} --version ${VERSION}" + done + uv run python dev/registry/extract_versions.py \ + --provider "${PROVIDER}" ${VERSION_ARGS} || true + + - name: "Run breeze registry backfill" + env: + VERSIONS: ${{ inputs.versions }} + PROVIDER: ${{ matrix.provider }} + run: | + VERSION_ARGS="" + for VERSION in ${VERSIONS}; do + VERSION_ARGS="${VERSION_ARGS} --version ${VERSION}" + done + breeze registry backfill \ + --provider "${PROVIDER}" ${VERSION_ARGS} + + - name: "Download data files from S3 for build" + env: + S3_BUCKET: ${{ needs.prepare.outputs.bucket }} + run: | + aws s3 cp \ + "${S3_BUCKET}api/providers.json" \ + registry/src/_data/providers.json + aws s3 cp \ + "${S3_BUCKET}api/modules.json" \ + registry/src/_data/modules.json + + - name: "Setup pnpm" + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + with: + version: 9 + + - name: "Setup Node.js" + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 20 + cache: 'pnpm' + cache-dependency-path: 'registry/pnpm-lock.yaml' + + - name: "Install Node.js dependencies" + working-directory: registry + run: pnpm install --frozen-lockfile + + - name: "Build registry site" + working-directory: registry + env: + REGISTRY_PATH_PREFIX: "/registry/" + run: pnpm build + + - name: "Sync backfilled version pages to S3" + env: + S3_BUCKET: ${{ needs.prepare.outputs.bucket }} + CACHE_CONTROL: "public, max-age=300" + VERSIONS: ${{ inputs.versions }} + PROVIDER: ${{ matrix.provider }} + run: | + for VERSION in ${VERSIONS}; do + echo "Syncing ${PROVIDER}/${VERSION}..." + aws s3 sync \ + "registry/_site/providers/${PROVIDER}/${VERSION}/" \ + "${S3_BUCKET}providers/${PROVIDER}/${VERSION}/" \ + --cache-control "${CACHE_CONTROL}" + aws s3 sync \ + "registry/_site/api/providers/${PROVIDER}/${VERSION}/" \ + "${S3_BUCKET}api/providers/${PROVIDER}/${VERSION}/" \ + --cache-control "${CACHE_CONTROL}" + done + + publish-versions: + needs: [prepare, backfill] + runs-on: ubuntu-latest + name: "Publish versions.json" + steps: + - name: "Checkout repository" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: "Install Breeze" + uses: ./.github/actions/breeze + with: + python-version: "3.12" + + - name: "Install AWS CLI v2" + run: | + curl -sSf \ + "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" \ + -o /tmp/awscliv2.zip + unzip -q /tmp/awscliv2.zip -d /tmp + rm /tmp/awscliv2.zip + sudo /tmp/aws/install --update + rm -rf /tmp/aws/ + + - name: "Configure AWS credentials" + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 + with: + aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-2 + + - name: "Publish version metadata" + env: + S3_BUCKET: ${{ needs.prepare.outputs.bucket }} + run: > + breeze registry publish-versions + --s3-bucket "${S3_BUCKET}" diff --git a/.github/workflows/registry-build.yml b/.github/workflows/registry-build.yml index 1c8b6d367b5cc..0d35d02417b22 100644 --- a/.github/workflows/registry-build.yml +++ b/.github/workflows/registry-build.yml @@ -70,7 +70,6 @@ jobs: REGISTRY_CACHE_CONTROL: public, max-age=300 permissions: contents: read - id-token: write if: > github.event_name == 'workflow_call' || contains(fromJSON('[ @@ -89,7 +88,7 @@ jobs: ]'), github.event.sender.login) steps: - name: "Checkout repository" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -126,7 +125,7 @@ jobs: rm -rf /tmp/aws/ - name: "Configure AWS credentials" - uses: aws-actions/configure-aws-credentials@010d0da01d0b5a38af31e9c3470dbfdabdecca3a # v4.0.1 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }} @@ -206,12 +205,12 @@ jobs: fi - name: "Setup pnpm" - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 9 - name: "Setup Node.js" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 20 cache: 'pnpm' @@ -240,7 +239,14 @@ jobs: S3_BUCKET: ${{ steps.destination.outputs.bucket }} run: | aws s3 sync registry/_site/ "${S3_BUCKET}" \ - --cache-control "${REGISTRY_CACHE_CONTROL}" + --cache-control "${REGISTRY_CACHE_CONTROL}" \ + --exclude "pagefind/*" + # Pagefind generates content-hashed filenames (e.g. en_181da6f.pf_index). + # Each rebuild produces new hashes, so --delete is needed to remove stale + # index files. This is separate from the main sync which intentionally + # omits --delete to preserve files written by other steps (publish-versions). + aws s3 sync registry/_site/pagefind/ "${S3_BUCKET}pagefind/" \ + --cache-control "${REGISTRY_CACHE_CONTROL}" --delete - name: "Publish version metadata" env: diff --git a/.github/workflows/registry-tests.yml b/.github/workflows/registry-tests.yml index e305d088655c9..fb254be34a555 100644 --- a/.github/workflows/registry-tests.yml +++ b/.github/workflows/registry-tests.yml @@ -45,20 +45,14 @@ jobs: timeout-minutes: 5 steps: - name: "Checkout repository" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - name: "Setup Python" - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: "3.12" # 3.11+ required for stdlib tomllib - - name: "Install uv" - run: python -m pip install uv - - - name: "Install test dependencies" - run: uv pip install --system pytest pyyaml + uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v7.4.0 + with: + python-version: "3.12" - name: "Run registry extraction tests" - run: pytest dev/registry/tests/ -v -o "addopts=" + run: cd dev/registry && uv run --group dev pytest tests/ -v diff --git a/.github/workflows/release_dockerhub_image.yml b/.github/workflows/release_dockerhub_image.yml index 7fac56a7d194a..88d3ca1194892 100644 --- a/.github/workflows/release_dockerhub_image.yml +++ b/.github/workflows/release_dockerhub_image.yml @@ -58,7 +58,7 @@ jobs: AIRFLOW_VERSION: ${{ github.event.inputs.airflowVersion }} AMD_ONLY: ${{ github.event.inputs.amdOnly }} LIMIT_PYTHON_VERSIONS: ${{ github.event.inputs.limitPythonVersions }} - UV_VERSION: "0.10.8" # Keep this comment to allow automatic replacement of uv version + UV_VERSION: "0.10.9" # Keep this comment to allow automatic replacement of uv version if: contains(fromJSON('[ "ashb", "bugraoz93", @@ -88,7 +88,7 @@ jobs: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Install Breeze" diff --git a/.github/workflows/release_single_dockerhub_image.yml b/.github/workflows/release_single_dockerhub_image.yml index cc42773396292..efa72bc45fdb5 100644 --- a/.github/workflows/release_single_dockerhub_image.yml +++ b/.github/workflows/release_single_dockerhub_image.yml @@ -77,7 +77,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Install Breeze" @@ -171,7 +171,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Install Breeze" @@ -190,7 +190,7 @@ jobs: ACTOR: ${{ github.actor }} run: echo "${GITHUB_TOKEN}" | docker login ghcr.io -u ${ACTOR} --password-stdin - name: "Download metadata artifacts" - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: path: ./dist pattern: metadata-${{ inputs.pythonVersion }}-* diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index a1f1245509cc3..5b23a3be28fd0 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -183,7 +183,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Make /mnt writeable" diff --git a/.github/workflows/test-providers.yml b/.github/workflows/test-providers.yml index 90058057e524c..d6c268db849e0 100644 --- a/.github/workflows/test-providers.yml +++ b/.github/workflows/test-providers.yml @@ -89,7 +89,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Free up disk space" @@ -198,7 +198,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Free up disk space" diff --git a/.github/workflows/ui-e2e-tests.yml b/.github/workflows/ui-e2e-tests.yml index 7f31fb3efaad0..2f75206aec7ed 100644 --- a/.github/workflows/ui-e2e-tests.yml +++ b/.github/workflows/ui-e2e-tests.yml @@ -103,7 +103,7 @@ jobs: shell: bash run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 2 persist-credentials: false @@ -121,12 +121,12 @@ jobs: uses: ./.github/actions/breeze if: github.event_name == 'workflow_dispatch' - name: "Setup pnpm" - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 9 run_install: false - name: "Setup node" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 21 - name: "Compile UI assets (for image build fallback)" diff --git a/shared/observability/src/airflow_shared/observability/traces/__init__.py b/.github/zizmor.yml similarity index 88% rename from shared/observability/src/airflow_shared/observability/traces/__init__.py rename to .github/zizmor.yml index 7b2f416872e98..ef9fbc6d507d5 100644 --- a/shared/observability/src/airflow_shared/observability/traces/__init__.py +++ b/.github/zizmor.yml @@ -14,8 +14,8 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from __future__ import annotations -TRACEPARENT = "traceparent" -TRACESTATE = "tracestate" -NO_TRACE_ID = 1 +--- +rules: + secrets-outside-env: + disable: true diff --git a/.gitignore b/.gitignore index 44075d793b8f5..0cf23e4e047b7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ airflow.db # Airflow temporary artifacts airflow-core/src/airflow/git_version +# ERD diagrams (generated at doc build time) +airflow-core/docs/img/airflow_erd.svg +providers/fab/docs/img/fab_erd.svg +providers/edge3/docs/img/edge3_erd.svg airflow-core/src/airflow/ui/coverage/ # and legacy ones airflow/git_version diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66c93463168bb..702496555b352 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -165,9 +165,9 @@ repos: (?x) ^\.github/.*\.md$| ^\.claude/| - AGENTS\.md$| - CLAUDE\.md$| - SKILL\.md$| + ^(?:.*/)?AGENTS\.md$| + ^(?:.*/)?CLAUDE\.md$| + ^(?:.*/)?SKILL\.md$| ^scripts/ci/license-templates/ - id: insert-license name: Add short license for agentic Markdown files @@ -181,9 +181,9 @@ repos: (?x) ^\.github/.*\.md$| ^\.claude/| - AGENTS\.md$| - CLAUDE\.md$| - SKILL\.md$ + ^(?:.*/)?AGENTS\.md$| + ^(?:.*/)?CLAUDE\.md$| + ^(?:.*/)?SKILL\.md$ exclude: (?x) ^scripts/ci/license-templates/| @@ -337,7 +337,7 @@ repos: - --line-length - '99999' - repo: https://github.com/codespell-project/codespell - rev: 63c8f8312b7559622c0d82815639671ae42132ac # frozen: v2.4.1 + rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2 hooks: - id: codespell name: Run codespell @@ -363,7 +363,7 @@ repos: - --skip=providers/.*/src/airflow/providers/*/*.rst,providers/*/docs/changelog.rst,docs/*/commits.rst,providers/*/docs/commits.rst,providers/*/*/docs/commits.rst,docs/apache-airflow/tutorial/pipeline_example.csv,*.min.js,*.lock,INTHEWILD.md,*.svg - --exclude-file=.codespellignorelines - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: b546b77c44c466a54a42af5499dcc0dcc1a3193f # frozen: v1.22.0 + rev: ea2eb407b4cbce87cf0d502f36578950494f5ac9 # frozen: v1.23.1 hooks: - id: zizmor name: Run zizmor to check for github workflow syntax errors @@ -422,6 +422,12 @@ repos: language: python pass_filenames: false files: ^airflow-core/src/airflow/secrets/base_secrets\.py$|^task-sdk/src/airflow/sdk/execution_time/secrets/__init__\.py$ + - id: check-registry-types-json-sync + name: Check registry types.json in sync with types.py + entry: ./scripts/ci/prek/check_registry_types_json_sync.py + language: python + pass_filenames: false + files: ^dev/registry/registry_tools/types\.py$|^registry/src/_data/types\.json$ - id: ruff name: Run 'ruff' for extremely fast Python linting description: "Run 'ruff' for extremely fast Python linting" @@ -430,7 +436,7 @@ repos: types_or: [python, pyi] args: [--fix] require_serial: true - additional_dependencies: ['ruff==0.15.5'] + additional_dependencies: ['ruff==0.15.6'] exclude: ^airflow-core/tests/unit/dags/test_imports\.py$|^performance/tests/test_.*\.py$ - id: ruff-format name: Run 'ruff format' @@ -584,6 +590,7 @@ repos: ^airflow-core/src/airflow/serialization/serialized_objects\.py$| ^airflow-core/src/airflow/ui/openapi-gen/| ^airflow-core/src/airflow/ui/pnpm-lock\.yaml$| + ^providers/common/ai/src/airflow/providers/common/ai/plugins/www/pnpm-lock\.yaml$| ^airflow-core/src/airflow/ui/public/i18n/locales/de/README\.md$| ^airflow-core/src/airflow/ui/src/i18n/config\.ts$| ^airflow-core/src/airflow/utils/db\.py$| @@ -991,6 +998,13 @@ repos: entry: ./scripts/ci/prek/check_template_fields.py files: ^(providers/.*/)?airflow-core/.*/(sensors|operators)/.*\.py$ require_serial: true + - id: check-execution-api-versions + name: Check execution API datamodel changes have corresponding version updates + entry: ./scripts/ci/prek/check_execution_api_versions.py + language: python + pass_filenames: false + files: ^airflow-core/src/airflow/api_fastapi/execution_api/(datamodels|versions)/.*\.py$ + require_serial: true - id: generate-tasksdk-datamodels name: Generate Datamodels for TaskSDK client language: python diff --git a/AGENTS.md b/AGENTS.md index da68ce911b199..26608d69cff2d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,8 +32,8 @@ - **Type-check:** `breeze run mypy path/to/code` - **Lint with ruff only:** `prek run ruff --from-ref ` - **Format with ruff only:** `prek run ruff-format --from-ref ` -- **Run regular (fast) static checks:** `prek run --from-ref --hook-stage pre-commit` -- **Run manual (slower) checks:** `prek run --from-ref --hook-stage manual` +- **Run regular (fast) static checks:** `prek run --from-ref --stage pre-commit` +- **Run manual (slower) checks:** `prek run --from-ref --stage manual` - **Build docs:** `breeze build-docs` - **Determine which tests to run based on changed files:** `breeze selective-checks --commit-ref ` @@ -65,6 +65,18 @@ UV workspace monorepo. Key paths: 4. Workers execute tasks via Task SDK and communicate with the API server through the Execution API — **never access the metadata DB directly**. 5. API Server serves the React UI and handles all client-database interactions. 6. Triggerer evaluates deferred tasks/sensors in isolated processes. +7. Shared libraries that are symbolically linked to different Python distributions are in `shared` folder. +8. Airflow uses `uv workspace` feature to keep all the distributions sharing dependencies and venv +9. Each of the distributions should declare other needed distributions: `uv --project sync` command acts on the selected project in the monorepo with only dependencies that it has + +# Shared libraries + +- shared libraries provide implementation of some common utilities like logging, configuration where the code should be reused in different distributions (potentially in different versions) +- we have a number of shared libraries that are separate, small Python distributions located under `shared` folder +- each of the libraries has it's own src, tests, pyproject.toml and dependencies +- sources of those libraries are symbolically linked to the distributions that are using them (`airflow-core`, `task-sdk` for example) +- tests for the libraries (internal) are in the shared distribution's test and can be run from the shared distributions +- tests of the consumers using the shared libraries are present in the distributions that use the libraries and can be run from there ## Coding Standards @@ -86,6 +98,7 @@ UV workspace monorepo. Key paths: - Test fixtures: `devel-common/src/tests_common/pytest_plugin.py`. - Test location mirrors source: `airflow/cli/cli_parser.py` → `tests/cli/test_cli_parser.py`. + ## Commits and PRs Write commit messages focused on user impact, not implementation details. @@ -97,6 +110,8 @@ Write commit messages focused on user impact, not implementation details. Add a newsfragment for user-visible changes: `echo "Brief description" > airflow-core/newsfragments/{PR_NUMBER}.{bugfix|feature|improvement|doc|misc|significant}.rst` +- NEVER add Co-Authored-By with yourself as co-author of the commit. Agents cannot be authors, humans can be, Agents are assistants. + ### Creating Pull Requests **Always push to the user's fork**, not to the upstream `apache/airflow` repo. Never push @@ -122,9 +137,9 @@ code review checklist in [`.github/instructions/code-review.instructions.md`](.g API correctness, and AI-generated code signals. Fix any violations before pushing. 3. Confirm the code follows the project's coding standards and architecture boundaries described in this file. -4. Run regular (fast) static checks (`prek run --from-ref --hook-stage pre-commit`) +4. Run regular (fast) static checks (`prek run --from-ref --stage pre-commit`) and fix any failures. -5. Run manual (slower) checks (`prek run --from-ref --hook-stage manual`) and fix any failures. +5. Run manual (slower) checks (`prek run --from-ref --stage manual`) and fix any failures. 6. Run relevant individual tests and confirm they pass. 7. Find which tests to run for the changes with selective-checks and run those tests in parallel to confirm they pass and check for CI-specific issues. 8. Check for security issues — no secrets, no injection vulnerabilities, no unsafe patterns. diff --git a/COMMITTERS.rst b/COMMITTERS.rst index a4b247d8f35c8..23a52436918c4 100644 --- a/COMMITTERS.rst +++ b/COMMITTERS.rst @@ -22,7 +22,9 @@ Before reading this document, you should be familiar with the `Contributors' gui This document assumes that you are a bit familiar with how Airflow's community works, but you would like to learn more about the rules by which we add new committers and PMC members. -**The outline for this document in GitHub is available at the top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Committers vs. Maintainers -------------------------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index fc8180278f76f..00fe429fad81c 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -18,7 +18,6 @@ Contributing ============ -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** Contributions are welcome and are greatly appreciated! Every little bit helps, and credit will always be given. diff --git a/Dockerfile b/Dockerfile index 46c22cb91c627..e25050de67ca6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ ARG AIRFLOW_UID="50000" ARG AIRFLOW_USER_HOME_DIR=/home/airflow # latest released version here -ARG AIRFLOW_VERSION="3.1.7" +ARG AIRFLOW_VERSION="3.1.8" ARG BASE_IMAGE="debian:bookworm-slim" ARG AIRFLOW_PYTHON_VERSION="3.12.13" @@ -73,7 +73,7 @@ ARG PYTHON_LTO="true" # Also use `force pip` label on your PR to swap all places we use `uv` to `pip` ARG AIRFLOW_PIP_VERSION=26.0.1 # ARG AIRFLOW_PIP_VERSION="git+https://github.com/pypa/pip.git@main" -ARG AIRFLOW_UV_VERSION=0.10.8 +ARG AIRFLOW_UV_VERSION=0.10.9 ARG AIRFLOW_USE_UV="false" ARG AIRFLOW_IMAGE_REPOSITORY="https://github.com/apache/airflow" ARG AIRFLOW_IMAGE_README_URL="https://raw.githubusercontent.com/apache/airflow/main/docs/docker-stack/README.md" @@ -1201,7 +1201,7 @@ function install_from_external_spec() { installation_command_flags="apache-airflow[${AIRFLOW_EXTRAS}]${AIRFLOW_VERSION_SPECIFICATION}" else echo - echo "${COLOR_RED}The '${INSTALLATION_METHOD}' installation method is not supported${COLOR_RESET}" + echo "${COLOR_RED}The '${AIRFLOW_INSTALLATION_METHOD}' installation method is not supported${COLOR_RESET}" echo echo "${COLOR_YELLOW}Supported methods are ('.', 'apache-airflow')${COLOR_RESET}" echo @@ -1670,7 +1670,8 @@ COPY <<"EOF" /clean-logs.sh set -euo pipefail readonly DIRECTORY="${AIRFLOW_HOME:-/usr/local/airflow}" -readonly RETENTION="${AIRFLOW__LOG_RETENTION_DAYS:-15}" +readonly RETENTION_DAYS="${AIRFLOW__LOG_RETENTION_DAYS:-15}" +readonly RETENTION_MINUTES="${AIRFLOW__LOG_RETENTION_MINUTES:-0}" readonly FREQUENCY="${AIRFLOW__LOG_CLEANUP_FREQUENCY_MINUTES:-15}" readonly MAX_PERCENT="${AIRFLOW__LOG_MAX_SIZE_PERCENT:-0}" @@ -1692,13 +1693,15 @@ if [[ "$MAX_SIZE_BYTES" -gt 0 ]]; then echo "Max log size limit: $MAX_SIZE_BYTES bytes" fi -retention_days="${RETENTION}" +retention_days="${RETENTION_DAYS}" while true; do - echo "Trimming airflow logs to ${retention_days} days." + total_retention_minutes=$(( (retention_days * 1440) + RETENTION_MINUTES )) + echo "Trimming airflow logs older than ${total_retention_minutes} minutes." + find "${DIRECTORY}"/logs \ -type d -name 'lost+found' -prune -o \ - -type f -mtime +"${retention_days}" -name '*.log' -print0 | \ + -type f -mmin +"${total_retention_minutes}" -name '*.log' -print0 | \ xargs -0 rm -f || true if [[ "$MAX_SIZE_BYTES" -gt 0 && "$retention_days" -ge 0 ]]; then @@ -1714,7 +1717,7 @@ while true; do find "${DIRECTORY}"/logs -type d -empty -delete || true - retention_days="${RETENTION}" + retention_days="${RETENTION_DAYS}" seconds=$(( $(date -u +%s) % EVERY)) (( seconds < 1 )) || sleep $((EVERY - seconds - 1)) diff --git a/Dockerfile.ci b/Dockerfile.ci index 9a0282eeeda46..ce6a5883f08e1 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -936,7 +936,7 @@ function install_from_external_spec() { installation_command_flags="apache-airflow[${AIRFLOW_EXTRAS}]${AIRFLOW_VERSION_SPECIFICATION}" else echo - echo "${COLOR_RED}The '${INSTALLATION_METHOD}' installation method is not supported${COLOR_RESET}" + echo "${COLOR_RED}The '${AIRFLOW_INSTALLATION_METHOD}' installation method is not supported${COLOR_RESET}" echo echo "${COLOR_YELLOW}Supported methods are ('.', 'apache-airflow')${COLOR_RESET}" echo @@ -1160,6 +1160,11 @@ function environment_initialization() { echo " * ${COLOR_BLUE}Airflow backend:${COLOR_RESET} MySQL: ${MYSQL_VERSION}" elif [[ ${BACKEND=} == "sqlite" ]]; then echo " * ${COLOR_BLUE}Airflow backend:${COLOR_RESET} Sqlite" + elif [[ ${BACKEND=} == "custom" ]]; then + local _conn_url="${AIRFLOW__DATABASE__SQL_ALCHEMY_CONN:-}" + local _masked_url + _masked_url=$(echo "${_conn_url}" | sed -E 's|://([^:]+):([^@]+)@|://\1:***@|') + echo " * ${COLOR_BLUE}Airflow backend:${COLOR_RESET} Custom (${_masked_url})" fi echo @@ -1733,8 +1738,8 @@ COPY --from=scripts common.sh install_packaging_tools.sh install_additional_depe # Also use `force pip` label on your PR to swap all places we use `uv` to `pip` ARG AIRFLOW_PIP_VERSION=26.0.1 # ARG AIRFLOW_PIP_VERSION="git+https://github.com/pypa/pip.git@main" -ARG AIRFLOW_UV_VERSION=0.10.8 -ARG AIRFLOW_PREK_VERSION="0.3.4" +ARG AIRFLOW_UV_VERSION=0.10.9 +ARG AIRFLOW_PREK_VERSION="0.3.5" # UV_LINK_MODE=copy is needed since we are using cache mounted from the host ENV AIRFLOW_PIP_VERSION=${AIRFLOW_PIP_VERSION} \ diff --git a/INSTALL b/INSTALL index 0c264b9897c1e..cb3e045debdf3 100644 --- a/INSTALL +++ b/INSTALL @@ -131,7 +131,7 @@ In order to see UI in Airflow, you need to compile front-end assets first. In case you already installed `breeze` and `prek`, you can build the assets with the following commands: - prek --hook-stage manual compile-ui-assets --all-files + prek --stage manual compile-ui-assets --all-files or simply: diff --git a/PROVIDERS.rst b/PROVIDERS.rst index a48233eb3e8e0..e5bd86e5a13d4 100644 --- a/PROVIDERS.rst +++ b/PROVIDERS.rst @@ -19,7 +19,9 @@ Apache Airflow Providers ************************ -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: What is a provider? =================== @@ -48,6 +50,9 @@ You can read more about it in the `Installation and upgrade scenarios `_ chapter of our user documentation. +List of all available community providers is available at the `Providers index `_. + + Community managed providers =========================== @@ -70,10 +75,34 @@ Because of the constraint and potential conflicting dependencies, the community updated and the community might decide to suspend releases of a provider if we find out that we have trouble with updating the dependencies, or if we find out that the provider is not compatible with other more popular providers and when the popular providers are limited by the constraints of the less popular ones. -See the section below for more details on suspending releases of the community providers. +See `Suspending releases for providers`_ below for more details. -List of all available community providers is available at the `Providers index `_. +Detailed documents +================== + +The following documents provide detailed information about specific aspects of provider management: + +.. list-table:: + :widths: 30 70 + + * - `Provider Governance `_ + - Governance framework, stewardship model, lifecycle stages (incubation, production, + attic/deprecation), health metrics, and periodic reviews + * - `3rd-Party Providers `_ + - Relation to community providers, system test dashboards, and mixed governance model + * - `Accepting New Providers `_ + - Prerequisites, approval process, and historical examples for proposing new community providers + * - `Provider Releases and Versioning `_ + - Release process, SEMVER versioning, provider distribution states, and minimum Airflow + version policy + * - `Suspending and Removing Providers `_ + - Criteria and process for suspending or removing community providers + * - `Managing Provider's Lifecycle `_ + - Technical how-to for creating, suspending, resuming, and removing providers + + +.. _provider-governance-framework: Community providers lifecycle ============================= @@ -81,482 +110,58 @@ Community providers lifecycle This document describes the complete life-cycle of community providers - from inception and approval to Airflow main branch to being decommissioned and removed from the main branch in Airflow repository. -.. note:: - - Technical details on how to manage lifecycle of providers are described in the document: - - `Managing provider's lifecycle `_ - - -Provider Governance Framework ------------------------------ - -This section describes the governance framework for community providers, including lifecycle stages, -stewardship model, and quantitative health metrics. - -Airflow's success is built on its extensive ecosystem of community-supported integrations—over 1,600 -hooks, operators, and sensors released as part of 90+ provider packages. These integrations are critical -for "ubiquitous orchestration." This governance framework establishes a scalable method to grow the -number of integrations while ensuring quality. Actual code acceptance and release governance remains with the Airflow PMC. - -Provider Stewardship Model -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Each provider or integration component must have designated **stewards** responsible for ensuring -the health criteria described below are met: +For full details, see `Provider Governance `_. -* **Minimum stewards**: At least two unique individuals must serve as stewards for each provider -* **Role definition**: Stewards are subject matter experts for the integration. This could be expertise in the - service being integrated, the language being supported by the provider, or the framework being integrated. - Stewardship is a responsibility, not an additional authority or privilege -* **Committer sponsorship**: Each steward must be sponsored by at least one Airflow Committer. The - sponsor ensures that stewards fulfill their responsibilities and provides guidance on maintaining the - provider according to best practices. This includes regular dependency updates, issue resolution, and - monitoring that the provider meets the health metrics required for its current lifecycle stage (that is, incubation - or production). The sponsor is responsible for PR reviews and merges (including ensuring coding standards are met), but - is NOT responsible for active maintenance of the provider's codebase, which remains the responsibility of the stewards. - While sponsors should be accountable when it comes to reviews and merges, it's also OK and welcome that other committers merge code providing it fulfill the criteria. -* **Accountability**: Ultimate accountability remains with the Airflow PMC and Committers -* **Transitions**: Neither sponsorship nor stewardship are roles in perpetuity; they can be - transitioned to others based on mutual agreement and PMC approval +Governance framework +-------------------- -.. note:: +See `Provider Governance: Governance framework `_. - The quantitative criteria described below are aspirational. The PMC will revisit these metrics - based on actual experience 6 months from the date of the first quarterly review of the provider metrics being published. -Provider Lifecycle Stages -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Providers generally follow a three-stage lifecycle: **Incubation**, **Production**, and **Attic/Deprecation**. However, -not all providers will move through all stages. Additionally, providers may be designated as **Mature** under specific circumstances. - -**Incubation Stage** - -All new providers or integration modules (such as Notifiers, Message components, etc.) should start -in incubation unless specifically accelerated by the PMC. Incubation ensures that code, licenses, and -processes align with ASF guidelines and community principles such as community, collaborative -development, and openness. - -Requirements for incubation: - -* Working codebase to bootstrap credibility and attract contributions -* Visibility in the provider health dashboard (The provider health dashboard is to be added) -* Designated stewards with committer sponsorship +Accepting new community providers +---------------------------------- -Quantitative graduation criteria: +See `Accepting New Community Providers `_. -.. list-table:: - :header-rows: 1 - :widths: 40 60 - - * - Metric - - Threshold - * - PRs submitted - - Minimum of 10 PRs in the last 6 months - * - PR review time - - PRs reviewed within 14 days - * - Issues reported - - Minimum of 15 unique issues filed in the last 6 months - * - Contributors - - At least 3 unique individuals making contributions (code or documentation) in the last 6 months - * - Issue resolution rate - - At least 50% of reported issues closed within 90 days - * - Security/release issues - - All release and security related issues closed within 60 days - * - Governance participation - - Demonstrated participation in project governance channels including quarterly updates - * - Quality standards - - Meet quality criteria for code, documentation, and tests as listed in the Contributing Guide - -**Production Stage** - -All modules in production are expected to be well managed with prompt resolution of issues and -support for a consistent release cadence (at least monthly, but typically every 2 weeks when changes -exist). Production providers must: - -* Stay consistent with the main branch -* Pass tests for main + all supported Airflow versions -* Follow Airflow support guidelines, staying current with main Airflow releases - -Exceptions can be granted based on a PMC/devlist vote (PMC members only having binding votes) for -valid and one-off criteria. - -Quantitative criteria to maintain production status: -.. list-table:: - :header-rows: 1 - :widths: 40 60 - - * - Metric - - Threshold - * - PRs submitted - - Minimum of 10 PRs in the last 6 months - * - PR review time - - PRs reviewed within 14 days - * - Issues reported - - Minimum of 20 unique issues filed in the last 6 months - * - Contributors - - At least 5 unique individuals making contributions (code or documentation) in the last 6 months - * - Issue resolution rate - - At least 60% of reported issues closed within 90 days - * - Security/release issues - - All release and security related issues closed within 30 days - * - Feature releases - - At least 1 feature release every 6 months - * - User engagement - - Maintain support activity with response to questions within 2 weeks on average - * - Governance participation - - Demonstrated participation in project governance channels including quarterly updates - -**Attic / Deprecation Stage** - -Modules should be moved into the Attic when relevance wanes, typically measured by declining -activity. This commonly occurs when the integrated solution has faded in popularity and is replaced -by more modern alternatives. - -Movement to the Attic must be communicated on the dev list and voted on by the PMC. Exceptions can -be granted based on the vote. - -Quantitative criteria triggering attic consideration: +Releases and versioning +======================= -.. list-table:: - :header-rows: 1 - :widths: 40 60 - - * - Metric - - Threshold - * - PRs submitted - - Fewer than 5 PRs in the last 6 months - * - PR review time - - PRs not being reviewed in more than a month - * - Issues reported - - Fewer than 10 unique issues filed in the last 6 months - * - Contributors - - Fewer than 3 unique individuals making contributions (code or documentation) in the last 6 months - * - Issue resolution rate - - Less than 30% of reported issues closed within 90 days - * - Security/release issues - - Release and security related issues not getting closed within 30 days - * - Feature releases - - No feature releases in the last 6 months - -Consequences of attic status: - -* Modules remain readable but do not receive active maintenance -* Module is not actively tested in "main" in Airflow CI, its dependencies are not checked for conflicts - with other main providers, and common refactorings are not applied to it. -* After a period of at least 6 months in the attic, modules can be chosen for removal with - appropriate communication (see `Removing community providers`_ below) -* It is possible for the provider to be resurrected from the attic as long as there is confidence that there is a - clear need for the provider and the (possibly new) stewards are able to maintain it actively on this go around. - It should be noted that significant effort may be required to resurrect a provider from the attic. - -**Mature Providers** - -Some providers may have very stable interfaces requiring minimal changes on a regular basis (e.g., -HTTP provider integration). At the discretion of the Airflow PMC, certain providers can be tagged -as **"mature providers"**, which will not automatically be deprecated and moved into the attic due -to lack of activity alone. - -Periodic Reviews -^^^^^^^^^^^^^^^^ - -The Airflow PMC is responsible for reviewing the health status of integrations on a **quarterly -basis** and making decisions such as: - -* Graduating providers from incubation to production -* Moving providers from production to the attic -* Granting exceptions for specific providers - -These discussions will be held in public and subsequently summarized and shared on the Airflow devlist. +See `Provider Releases and Versioning `_. +Release process +--------------- -Accepting new community providers ---------------------------------- +See `Provider Releases: Release process `_. -The Airflow community welcomes new provider contributions. All new providers enter through the -**Incubation** stage (unless specifically accelerated by the PMC) to ensure alignment with ASF -guidelines and community principles. +Versioning scheme +----------------- -**Prerequisites for proposing a new provider:** +See `Provider Releases: Versioning scheme `_. -1. **Working codebase**: A functional implementation demonstrating the integration -2. **Stewardship commitment**: At least two individuals willing to serve as stewards -3. **Committer sponsorship**: At least one existing Airflow Committer willing to sponsor the stewards -4. **Quality standards**: Code, tests, and documentation meeting the Contributing Guide standards +Provider distribution states +----------------------------- -**Approval process:** +See `Provider Releases: Provider distribution states `_. -Accepting new community providers requires a ``[DISCUSSION]`` followed by ``[VOTE]`` thread at the -Airflow `devlist `_. +Upgrading minimum supported version of Airflow +----------------------------------------------- -For integrations with well-established open-source software (Apache Software Foundation, Linux -Foundation, or similar organizations with established governance), a ``[LAZY CONSENSUS]`` process -may be sufficient, provided the PR includes comprehensive test coverage and documentation. +See `Provider Releases: Upgrading minimum supported version of Airflow `_. -The ``[DISCUSSION]`` thread should include: -* Description of the integration and its value to the Airflow ecosystem -* Identification of the proposed stewards and their sponsoring committer(s) -* Commitment to meet incubation health metrics within 6 months -* Plan for participating in quarterly governance updates +Suspending releases for providers +---------------------------------- -The ``[VOTE]`` follows the usual Apache Software Foundation voting rules concerning -`Votes on Code Modification `_ +See `Suspending and Removing Providers: Suspending releases `_. -**Alternative: 3rd-party managed providers** -For service providers or systems integrators with dedicated teams to manage their provider and who wish to not participate -in the Airflow community, we encourage considering 3rd-party management of providers. The -`Ecosystem page `_ -provides visibility for 3rd-party providers, and this approach allows service providers or systems integrators to: +Removing community providers +------------------------------ -* Synchronize releases with their service updates -* Maintain direct control over the integration -* Support older Airflow versions if needed - -There is no difference in technical capabilities between community and 3rd-party providers. - -**Historical examples:** - -* Huawei Cloud provider - `Discussion `_ -* Cloudera provider - `Discussion `_, `Vote `_ - - -Community providers release process ------------------------------------ - -The community providers are released regularly (usually every 2 weeks) in batches consisting of any providers -that need to be released because they changed since last release. The release manager decides which providers -to include and whether some or all providers should be released (see the next chapter about upgrading the -minimum version of Airflow for example the case where we release all active meaning non-suspended providers, -together in a single batch). Also Release Manager decides on the version bump of the provider (depending on -classification, whether there are breaking changes, new features or just bugs comparing to previous version). - -Upgrading Minimum supported version of Airflow ----------------------------------------------- - -.. note:: - - The following policy applies for Airflow 2. It has not yet been finalized for Airflow 3 and is subject to changes. - -One of the important limitations of the Providers released by the community is that we introduce the limit -of a minimum supported version of Airflow. The minimum version of Airflow is the ``MINOR`` version (2.4, 2.5 etc.) -indicating that the providers might use features that appeared in this release. The default support timespan -for the minimum version of Airflow (there could be justified exceptions) is that we increase the minimum -Airflow version to the next MINOR release, when 12 months passed since the first release for the -MINOR version of Airflow. - -For example this means that by default we upgrade the minimum version of Airflow supported by providers -to 3.1.0 in the first Provider's release after 20th of May 2026. 20th of May 2025 was the date when the -first ``PATCHLEVEL`` version of 2.11 (2.11.0) was released and since Airflow 3.0 was released in April 2025, -we go straight to Airflow 3.1 as minimum supported version of Airflow for providers in May 2026. - -When we increase the minimum Airflow version, this is not a reason to bump ``MAJOR`` version of the providers -(unless there are other breaking changes in the provider). The reason for that is that people who use -older version of Airflow will not be able to use that provider (so it is not a breaking change for them) -and for people who are using supported version of Airflow this is not a breaking change on its own - they -will be able to use the new version without breaking their workflows. When we upgraded min-version to -2.2+, our approach was different but as of 2.3+ upgrade (November 2022) we only bump ``MINOR`` version of the -provider when we increase minimum Airflow version. - -Increasing the minimum version of the Providers is one of the reasons why 3rd-party provider maintainers -might want to maintain their own providers - as they can decide to support older versions of Airflow. - -3rd-parties relation to community providers -------------------------------------------- - -Providers, can also be maintained and released by 3rd parties (service providers or systems integrators). -There is no difference between the community and 3rd party providers - they have all the same capabilities -and limitations. - -This is especially in case the provider concerns 3rd-party service that has a team that can manage provider -on their own, has a rapidly evolving live service, and believe they need a faster release cycle than the community -can provide. - -Information about such 3rd-party providers are usually published at the -`Ecosystem: plugins and providers `_ -page of the Airflow website and we encourage the service providers to publish their providers there. You can also -find a 3rd-party registries of such providers, that you can use if you search for existing providers (they -are also listed at the "Ecosystem" page in the same chapter) - -While we already have - historically - a number of 3rd-party service providers managed by the community, -most of those services have dedicated teams that keep an eye on the community providers and not only take -active part in managing them (see mixed-governance model below), but also provide a way that we can -verify whether the provider works with the latest version of the service via dashboards that show -status of System Tests for the provider. This allows us to have a high level of confidence that when we -release the provider it works with the latest version of the service. System Tests are part of the Airflow -code, but they are executed and verified by those 3rd party service teams. We are working with the 3rd -party service teams (who are often important stakeholders of the Apache Airflow project) to add dashboards -for the historical providers that are managed by the community, and current set of Dashboards can be also -found at the -`Ecosystem: system test dashboards `_ - -Mixed governance model for 3rd-party related community providers ----------------------------------------------------------------- - -Providers are often connected with some stakeholders that are vitally interested in maintaining backwards -compatibilities in their integrations (for example cloud providers, or specific service providers). But, -we are also bound with the `Apache Software Foundation release policy `_ -which describes who releases, and how to release the ASF software. The provider's governance model is something we name -``mixed governance`` - where we follow the release policies, while the burden of maintaining and testing -the cherry-picked versions is on those who commit to perform the cherry-picks and make PRs to older -branches. - -The "mixed governance" (optional, per-provider) means that: - -* The Airflow Community and release manager decide when to release those providers. - This is fully managed by the community and the usual release-management process following the - `Apache Software Foundation release policy `_ -* The contributors (who might or might not be direct stakeholders in the provider) will carry the burden - of cherry-picking and testing the older versions of providers. -* There is no "selection" and acceptance process to determine which version of the provider is released. - It is determined by the actions of contributors raising the PR with cherry-picked changes and it follows - the usual PR review process where maintainer approves (or not) and merges (or not) such PR. Simply - speaking - the completed action of cherry-picking and testing the older version of the provider make - it eligible to be released. Unless there is someone who volunteers and perform the cherry-picking and - testing, the provider is not released. -* Branches to raise PR against are created when a contributor commits to perform the cherry-picking - (as a comment in PR to cherry-pick for example) - -Usually, community effort is focused on the most recent version of each provider. The community approach is -that we should rather aggressively remove deprecations in "major" versions of the providers - whenever -there is an opportunity to increase major version of a provider, we attempt to remove all deprecations. -However, sometimes there is a contributor (who might or might not represent stakeholder), -willing to make their effort on cherry-picking and testing the non-breaking changes to a selected, -previous major branch of the provider. This results in releasing at most two versions of a -provider at a time: - -* potentially breaking "latest" major version -* selected past major version with non-breaking changes applied by the contributor - -Cherry-picking such changes follows the same process for releasing Airflow -patch-level releases for a previous minor Airflow version. Usually such cherry-picking is done when -there is an important bugfix and the latest version contains breaking changes that are not -coupled with the bugfix. Releasing them together in the latest version of the provider effectively couples -them, and therefore they're released separately. The cherry-picked changes have to be merged by the committer following the usual rules of the -community. - -There is no obligation to cherry-pick and release older versions of the providers. -The community continues to release such older versions of the providers for as long as there is an effort -of the contributors to perform the cherry-picks and carry-on testing of the older provider version. - -The availability of stakeholder that can manage "service-oriented" maintenance and agrees to such a -responsibility, will also drive our willingness to accept future, new providers to become community managed. +See `Suspending and Removing Providers: Removing providers `_. -Suspending releases for providers ---------------------------------- - -In case a provider is found to require old dependencies that are not compatible with upcoming versions of -the Apache Airflow or with newer dependencies required by other providers, the provider's release -process can be suspended. - -This means: - -* The provider's state in ``provider.yaml`` is set to "suspended" -* No new releases of the provider will be made until the problem with dependencies is solved -* Sources of the provider remain in the repository for now (in the future we might add process to remove them) -* No new changes will be accepted for the provider (other than the ones that fix the dependencies) -* The provider will be removed from the list of Apache Airflow extras in the next Airflow release - (including patch-level release if it is possible/easy to cherry-pick the suspension change) -* Tests of the provider will not be run on our CI (in main branch) -* Dependencies of the provider will not be installed in our main branch CI image nor included in constraints -* We can still decide to apply security fixes to released providers - by adding fixes to the main branch - but cherry-picking, testing and releasing them in the patch-level branch of the provider similar to the - mixed governance model described above. - -The suspension may be triggered by any committer after the following criteria are met: - -* The maintainers of dependencies of the provider are notified about the issue and are given a reasonable - time to resolve it (at least 1 week) -* Other options to resolve the issue have been exhausted and there are good reasons for upgrading - the old dependencies in question -* Explanation why we need to suspend the provider is stated in a public discussion in the devlist. Followed - by ``[LAZY CONSENSUS]`` or ``[VOTE]`` discussion at the devlist (with the majority of the binding votes - agreeing that we should suspend the provider) - -The suspension will be lifted when the dependencies of the provider are made compatible with the Apache -Airflow and with other providers - by merging a PR that removes the suspension and succeeds. +3rd-party providers +=================== -Removing community providers ----------------------------- - -After a Provider has been deprecated, as described above with a ``[VOTE]`` thread, it can -be removed from main branch of Airflow when the community agrees that there should be no -more updates to the providers done by the community - except maybe potentially security fixes found. There -might be various reasons for the providers to be removed: - -* the service they connect to is no longer available -* the dependencies for the provider are not maintained anymore and there is no viable alternative -* there is another, more popular provider that supersedes community provider -* etc. etc. - -Generally speaking a discussion thread ``[DISCUSS]`` is advised before such removal and -sufficient time should pass (at least a week) to give a chance for community members to express their -opinion on the removal. - -There are the following consequences (or lack of them) of removing the provider: - -* One last release of the provider is done with documentation updated informing that the provider is no - longer maintained by the Apache Airflow community - linking to this page. This information should also - find its way to the package documentation and consequently - to the description of the package in PyPI. -* An ``[ANNOUNCE]`` thread is sent to the devlist and user list announcing removal of the provider -* The released providers remain available on PyPI and in the - `Archives `_ of the Apache - Software Foundation, while they are removed from the - `Downloads `_ . - Also it remains in the Index of the Apache Airflow Providers documentation at - `Airflow Documentation `_ with note ``(not maintained)`` next to it. -* The code of the provider is removed from ``main`` branch of the Apache Airflow repository - including - the tests and documentation. It is no longer built in CI and dependencies of the provider no longer - contribute to the CI image/constraints of Apache Airflow for development and future ``MINOR`` release. -* The provider is removed from the list of Apache Airflow extras in the next ``MINOR`` Airflow release -* The dependencies of the provider are removed from the constraints of the Apache Airflow - (and the constraints are updated in the next ``MINOR`` release of Airflow) -* In case of confirmed security issues that need fixing that are reported to the provider after it has been - removed, there are two options: - * in case there is a viable alternative or in case the provider is anyhow not useful to be installed, we - might issue advisory to the users to remove the provider (and use alternatives if applicable) - * in case the users might still need the provider, we still might decide to release new version of the - provider with security issue fixed, starting from the source code in Git history where the provider was - last released. This however, should only be done in case there are no viable alternatives for the users. -* Removed provider might be re-instated as maintained provider, but it needs to go through the regular process - of accepting new provider described above. - -Provider distributions versioning ---------------------------------- - -We are using the `SEMVER `_ versioning scheme for the Provider distributions. This is in order -to give the users confidence about maintaining backwards compatibility in the new releases of those -packages. - -Details about maintaining the SEMVER version are going to be discussed and implemented in -`the related issue `_ - -Possible states of Provider distributions ------------------------------------------ - -The Provider distributions can be in one of several states. - -* The ``not-ready`` state is used when the provider has some in-progress changes (usually API changes) that - we do not want to release yet as part of the regular release cycle. Providers in this state are excluded - from being released as part of the regular release cycle (including documentation building). - The ``not-ready`` providers are treated as regular providers when it comes to running tests and preparing - and releasing packages in ``CI`` - as we want to make sure they are properly releasable any time and we - want them to contribute to dependencies and we want to test them. Also in case of preinstalled providers, - the ``not-ready`` providers are contributing their dependencies rather than the provider package to - requirements of Airflow. -* The ``ready`` state is the usual state of the provider that is released in the regular release cycle - (including the documentation, package building and publishing). This is the state most providers are in. -* The ``suspended``` state is used when we have a good reason to suspend such provider, following the devlist - discussion and vote or "lazy consensus". The process of suspension is described above. - The ``suspended`` providers are excluded from being released as part of the regular release cycle (including - documentation building) but also they do not contribute dependencies to the CI image and their tests are - not run in CI process. The ``suspended`` providers are not released as part of the regular release cycle. -* The ``removed`` state is a temporary state after the provider has been voted (or agreed in "lazy consensus") - to be removed and it is only used for exactly one release cycle - in order to produce the final version of - the package - identical to the previous version with the exception of the removal notice. The process - of removal is described in [Provider's docs](../PROVIDERS.rst). The difference between ``suspended`` - and ``removed`` providers is that additional information is added to their documentation about the provider - not being maintained any more by the community. +See `3rd-Party Providers `_. diff --git a/README.md b/README.md index d4aa7b8b0d86b..ab016a7156eff 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Airflow is not a streaming solution, but it is often used to process real-time d Apache Airflow is tested with: -| | Main version (dev) | Stable version (3.1.7) | Stable version (2.11.1) | +| | Main version (dev) | Stable version (3.1.8) | Stable version (2.11.1) | |------------|------------------------------------|------------------------|------------------------------| | Python | 3.10, 3.11, 3.12, 3.13 | 3.10, 3.11, 3.12, 3.13 | 3.10, 3.11, 3.12 | | Platform | AMD64/ARM64 | AMD64/ARM64 | AMD64/ARM64(\*) | @@ -171,15 +171,15 @@ them to the appropriate format and workflow that your tool requires. ```bash -pip install 'apache-airflow==3.1.7' \ - --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-3.1.7/constraints-3.10.txt" +pip install 'apache-airflow==3.1.8' \ + --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-3.1.8/constraints-3.10.txt" ``` 2. Installing with extras (i.e., postgres, google) ```bash -pip install 'apache-airflow[postgres,google]==3.1.7' \ - --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-3.1.7/constraints-3.10.txt" +pip install 'apache-airflow[postgres,google]==3.1.8' \ + --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-3.1.8/constraints-3.10.txt" ``` For information on installing provider distributions, check @@ -293,7 +293,7 @@ Apache Airflow version life cycle: | Version | Current Patch/Minor | State | First Release | Limited Maintenance | EOL/Terminated | |-----------|-----------------------|---------------------|-----------------|-----------------------|------------------| -| 3 | 3.1.7 | Maintenance | Apr 22, 2025 | TBD | TBD | +| 3 | 3.1.8 | Maintenance | Apr 22, 2025 | TBD | TBD | | 2 | 2.11.1 | Limited maintenance | Dec 17, 2020 | Oct 22, 2025 | Apr 22, 2026 | | 1.10 | 1.10.15 | EOL | Aug 27, 2018 | Dec 17, 2020 | June 17, 2021 | | 1.9 | 1.9.0 | EOL | Jan 03, 2018 | Aug 27, 2018 | Aug 27, 2018 | diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 598ab1e7fc747..1accb35dfb98b 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -24,6 +24,115 @@ .. towncrier release notes start +Airflow 3.1.8 (2026-03-11) +-------------------------- + +Significant Changes +^^^^^^^^^^^^^^^^^^^ + +Backfill permissions are now handled via ``DagAccessEntity.Run`` (#61456) +"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" +``is_authorized_backfill`` of the ``BaseAuthManager`` interface has been removed. Core will no longer call this method and their +provider counterpart implementation will be marked as deprecated. +Permissions for backfill operations are now checked against the ``DagAccessEntity.Run`` permission using the existing +``requires_access_dag`` decorator. In other words, if a user has permission to run a DAG, they can perform backfill operations on it. + +Please update your security policies to ensure that users who need to perform backfill operations have the appropriate ``DagAccessEntity.Run`` permissions. (Users +having the Backfill permissions without having the DagRun ones will no longer be able to perform backfill operations without any update) + +Elasticsearch is now fully compatible with remote logging along (#62940) +"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" +Elasticsearch is now fully compatible with remote logging along side with ``apache-airflow-providers-elasticsearch>=6.5.0``. Please review elasticsearch provider release notes for more information https://airflow.apache.org/docs/apache-airflow-providers-elasticsearch/6.5.0/changelog.html (#62121) (#62940) + +Bug Fixes +^^^^^^^^^ +- Fix SQLite migration disable ``disable_sqlite_fkeys`` in revision ``509b94a1042d`` (#63256) (#63272) +- Fix: 404 queued asset events from API server logs (#62934) (#62976) +- Fix: Always include kid in JWT header for symmetric key tokens (#62883) (#62943) +- Fix: Scope session token in cookie to base_url (#62771) (#62851) +- Fix: UI of Scope session token in cookie to base_url (#62771) (#62859) +- Fix: UI tasks log missing in UP_FOR_RETRY and UP_FOR_RESCHEDULE states (#54547) (#62862) +- Fix: Backfill permissions (#62856) (#62873) +- Fix: Use ``useAssetServiceGetDagAssetQueuedEvents`` to get the correct number of ADRQs (#62868) (#62902) +- Fix: Adds task instance validation for HITL (#62886) (#62909) +- Fix: Restore task_instance_history sequence on downgrade (#62759) +- Fix broken ``dag_processing.total_parse_time`` metric (#62128) (#62764) +- Fix Trigger UI form rendering for null enum values (#62060) (#62767) +- Fix ``timer.duration`` unit labels in logs (#61824) (#62757) +- Fix XCom migration failing for NaN/Infinity float values (#62686) (#62760) +- Fix SQL not rendered in Rendered Templates view (#60739) (#62348) +- Fix missing DAG read permission checks on dependencies endpoint (#62046) (#62586) +- Changed ``dag_bundle.signed_url_template`` from ``varchar(200)`` to ``text`` (#61041) (#62568) +- Fix WASB remote logging base path handling (#58946) (#61013) (#62456) +- Handle non-dictionary json payload during logging to avoid internal server error. (#62355) (#62367) +- Fix grid view crash when task converted to TaskGroup (#61208) (#61279) (#62181) +- Fix running task duration showing as null in UI (#61898) (#62136) +- Fix deferrable sensors not respecting soft_fail on timeout (#61132) (#61421) +- Fix task failure details being obscured by finalization errors (#62070) (#62113) +- Add missing ti.start and ti.finish metrics in Airflow 3 (#62019) (#62110) +- Fix DepContext mutation leak and restore reschedule-mode guard (#62089) +- Fix scheduler heartbeat misses caused by slow reschedule dependency check (#61983) (#62068) +- Flush in-memory OTEL metrics at process shutdown (#61808) (#61869) +- Fix executor slots showing negative infinity (#61140) (#61768) +- Fix recursion depth error in _redact_exception_with_context (#61776) (#61795) +- Fix API server segfault when ``PYTHONASYNCIODEBUG=1`` is set (#61281) (#61933) +- Fix scheduler crash when queuing TI with null dag_version_id (#61813) (#61846) +- Fix secrets masking in Rendered Templates for complex objects (#61394) (#61763) +- Fix list dag versions permissions (#61675) (#61733) +- Fix Triggerer crashing if Trigger uses builtin print function (#60258) (#61703) +- Fix GZipMiddleware with correct comment placement (#61538) (#61566) +- Fix middleware order to prevent chunked FastAPI responses (#61043) (#61539) +- Fix XCom serialization for ``pendulum.date.Date`` values (#61176) (#61717) +- Fix ``access_key`` and ``connection_string`` not being masked in logs (#61580) (#61582) +- Fix ``minimatch`` ReDoS vulnerabilities via ``pnpm`` overrides (#62805) +- Fix language selector state not updating on change (#61060) (#61263) +- Make conn_type optional in task SDK Connection data model (#61728) (#61835) +- UI: optimize grid view refresh pressure on the API (#62085) (#62135) +- UI: Fix main content margin to align with navigation sidebar width (#61614) (#61622) +- UI: Fix Preserve variable value formatting in edit dialog (#58757) (#62339) +- UI: Fix missing translation keys for blocking dependencies in UI (#61314) (#61366) (#61638) +- UI: Add error handling for pause/unpause toggle permission errors (#61389) (#61533) +- UI: Flatten grid structure endpoint memory consumption (#61273) (#61393) +- UI: Reduce memory usage in grid view by optimizing node data storage (#61656) (#61789) +- UI: Fix variable table word-break when values are expanded (#62416) (#62781) +- UI: Fix use ISO dates in Gantt chart for cross-browser consistency (#61250) (#62784) +- UI: Fix DataTable overflow on narrow screens (#62603) +- UI: Fix unique keys for pagination ellipses (#62352) (#62366) +- UI: Fix ``elk.portConstraints`` for LR orientation in graph view (#62144) (#62187) +- UI: Fix show active backfill in banner instead of first one (#61851) (#62137) +- UI: Fix star icon visibility in Favorite filter buttons when selected (#61862) +- UI: Fix grid view tooltip z-index issue (#61275) (#61403) +- UI: Fix mini-map on DAG graph view not showing DAG nodes (#61511) (#61530) +- UI: Fix pale appearance of filter buttons when selected (#60346 backport fix) (#61457) + +Miscellaneous +^^^^^^^^^^^^^ +- Add logging to detect try number race (#62703) (#62821) +- Override tar dependency in Simple auth manager (#62787) +- Remove mp_start_method remnants (#61150) (#62762) +- Expose literal and ParamsDict at SDK top level (#59782) (#62756) +- Add on_task_instance_skipped listener hookspec (#59467) (#61863) +- Persist table columns visibility in local storage (#61858) (#61868) +- Add ``run_after`` alias to ``XComResponse`` for backward compatibility (#61443) (#61672) +- UI: Add task_display_name to LightGridTaskInstanceSummary model (#61440) (#61505) +- UI: Add multi-line text display option on Variables page (#61679) (#62779) +- UI: Add bulk actions for connections and variables (#61570) (#62076) +- UI: Allow selecting file path using cursor in log viewer (#61011) (#61506) + +Doc Only Changes +^^^^^^^^^^^^^^^^ +- Fix Liveness / Readiness / Startup probe path for Airflow 3.x (#58734) (#61411) +- Update health check command syntax for celery worker (#58861) (#61412) +- Translation fixes: Polish (#62031) (#62761), Catalan (#62477), Taiwanese Mandarin (#62397), + German (#61478), Polish (#61423) +- Remove docs mentioning old, unsupported hybrid executors (#62093) (#62096) +- Clarify security model of Airflow (#61754) (#61770) +- Clarify ExternalTaskSensor path in dags.rst (#61555) (#61617) +- Clarify policy for exposing sensitive data (#59864) (#61392) +- Clarify template context for asset-triggered DAGs in airflow-core docs (#61258) (#61282) +- Add Keycloak token documentation to Security/API (#61228) (#61248) + + Airflow 3.1.7 (2026-02-04) -------------------------- diff --git a/airflow-core/.pre-commit-config.yaml b/airflow-core/.pre-commit-config.yaml index c3b869761a260..7573eec4e6533 100644 --- a/airflow-core/.pre-commit-config.yaml +++ b/airflow-core/.pre-commit-config.yaml @@ -264,15 +264,6 @@ repos: (?x) ^src/airflow/migrations/versions/.*\.py$| ^docs/migrations-ref\.rst$ - - id: update-er-diagram - name: Update ER diagram - language: python - entry: ../scripts/ci/prek/update_er_diagram.py - pass_filenames: false - files: - (?x) - ^src/airflow/migrations/versions/.*\.py$| - ^docs/migrations-ref\.rst$ - id: check-default-configuration name: Check the default configuration entry: ../scripts/ci/prek/check_default_configuration.py diff --git a/airflow-core/docs/administration-and-deployment/listeners.rst b/airflow-core/docs/administration-and-deployment/listeners.rst index 170c55de1c576..70dde0b7fd2af 100644 --- a/airflow-core/docs/administration-and-deployment/listeners.rst +++ b/airflow-core/docs/administration-and-deployment/listeners.rst @@ -24,7 +24,7 @@ You can write listeners to enable Airflow to notify you when events happen. .. warning:: Listeners are an advanced feature of Airflow. They are not isolated from the Airflow components they run in, and - can slow down or in come cases take down your Airflow instance. As such, extra care should be taken when writing listeners. + can slow down or in some cases take down your Airflow instance. As such, extra care should be taken when writing listeners. Airflow supports notifications for the following events: diff --git a/airflow-core/docs/administration-and-deployment/logging-monitoring/callbacks.rst b/airflow-core/docs/administration-and-deployment/logging-monitoring/callbacks.rst index 040b278f09729..9cbd3b2976bb3 100644 --- a/airflow-core/docs/administration-and-deployment/logging-monitoring/callbacks.rst +++ b/airflow-core/docs/administration-and-deployment/logging-monitoring/callbacks.rst @@ -53,23 +53,47 @@ Callback Types There are six types of events that can trigger a callback: -=========================================== ================================================================ -Name Description -=========================================== ================================================================ -``on_success_callback`` Invoked when the :ref:`Dag succeeds ` or :ref:`task succeeds `. - Available at the Dag or task level. -``on_failure_callback`` Invoked when the task :ref:`fails `. - Available at the Dag or task level. -``on_retry_callback`` Invoked when the task is :ref:`up for retry `. - Available only at the task level. -``on_execute_callback`` Invoked right before the task begins executing. - Available only at the task level. -``on_skipped_callback`` Invoked when the task is :ref:`running ` and AirflowSkipException raised. - Explicitly it is NOT called if a task is not started to be executed because of a preceding branching - decision in the Dag or a trigger rule which causes execution to skip so that the task execution - is never scheduled. - Available only at the task level. -=========================================== ================================================================ +=========================================== ======================================================================= ================= +Name Description Availability +=========================================== ======================================================================= ================= +``on_success_callback`` Invoked when the :ref:`Dag succeeds ` Dag or Task + or :ref:`task succeeds `. +``on_failure_callback`` Invoked when the :ref:`Dag fails ` Dag or Task + or task :ref:`fails `. +``on_retry_callback`` Invoked when the task is :ref:`up for retry `. Task +``on_execute_callback`` Invoked right before the task begins executing. Task +``on_skipped_callback`` Invoked when the task is :ref:`running ` Task + and AirflowSkipException raised. Explicitly it is NOT called if a task + is not started to be executed because of a preceding branching + decision in the Dag or a trigger rule which causes execution + to skip so that the task execution is never scheduled. +=========================================== ======================================================================= ================= + + +Context Mapping +--------------- + +A context mapping that contains runtime information about a task instance is passed to every callback. +Full list of variables available in ``context`` are in :doc:`docs <../../templates-ref>` and `code `_. + + +Dag Callbacks +^^^^^^^^^^^^^ + +As the context mapping describes execution of a task instance, contexts passed to Dag callbacks will also contain task instance variables, +and the task selected depends on the state of a Dag: + +#. On regular failure, the latest failed task is selected. +#. On Dag run timeout, the latest started but not finished task is passed. +#. If tasks are deadlocked, a task that should have run next but couldn't is passed. +#. On success, the latest succeeded task is passed. + +It's not recommended to rely on task instance variables in Dag callbacks except for human analysis, as they reflect only partial information about the Dag's state. +For example, a timeout may be caused by a number of stalling tasks, but only one will eventually be selected for context. + +.. note:: + Before Airflow 3.2.0, the rules above did not apply and the task instance passed to Dag callback was not related to Dag state, rather being selected as the latest task in the Dag + lexicographically. Examples @@ -109,8 +133,6 @@ Before each task begins to execute, the ``task_execute_callback`` function will task3 = EmptyOperator(task_id="task3") task1 >> task2 >> task3 -Full list of variables available in ``context`` in :doc:`docs <../../templates-ref>` and `code `_. - Using Notifiers ^^^^^^^^^^^^^^^ @@ -141,3 +163,13 @@ Here's an example of using a custom notifier: For a list of community-managed Notifiers, see :doc:`apache-airflow-providers:core-extensions/notifications`. For more information on writing a custom Notifier, see the :doc:`Notifiers <../../howto/notifications>` how-to page. + +Deadline Alert Callbacks +^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to the Dag/task lifecycle callbacks above, Airflow supports **Deadline Alert** callbacks which +trigger when a Dag run exceeds a configured time threshold. Deadline Alert callbacks use +:class:`~airflow.sdk.AsyncCallback` (runs in the Triggerer) or :class:`~airflow.sdk.SyncCallback` +(runs in the executor) and are configured on the Dag via the ``deadline`` parameter. + +For full details, see :doc:`/howto/deadline-alerts`. diff --git a/airflow-core/docs/best-practices.rst b/airflow-core/docs/best-practices.rst index 09eb668a510cf..5a49ac86cd043 100644 --- a/airflow-core/docs/best-practices.rst +++ b/airflow-core/docs/best-practices.rst @@ -310,7 +310,7 @@ Installing and Using ruff .. code-block:: bash - pip install "ruff>=0.15.5" + pip install "ruff>=0.15.6" 2. **Running ruff**: Execute ``ruff`` to check your Dags for potential issues: diff --git a/airflow-core/docs/conf.py b/airflow-core/docs/conf.py index 99e0527fcdb4f..71e2b0cd06aeb 100644 --- a/airflow-core/docs/conf.py +++ b/airflow-core/docs/conf.py @@ -104,6 +104,7 @@ "sphinx.ext.graphviz", "sphinxcontrib.httpdomain", "extra_files_with_substitutions", + "generate_erd", ] ) diff --git a/airflow-core/docs/core-concepts/multi-team.rst b/airflow-core/docs/core-concepts/multi-team.rst index 0f8a720567316..6beccc249b1cf 100644 --- a/airflow-core/docs/core-concepts/multi-team.rst +++ b/airflow-core/docs/core-concepts/multi-team.rst @@ -91,6 +91,27 @@ Secrets Backends are supported on a case by case basis. When a task requests a Variable or Connection, the secrets backend will return a team-specific value, if any. The backend will automatically resolve the correct value based on the requesting task's team. +Auth Manager +"""""""""""" + +In order to use multi-team mode, the auth manager must be compatible with it. A compatible auth manager must +implement two methods: + +- ``is_authorized_team``: Determines whether a user is authorized to perform a given action on a team. It is + used primarily to check whether a user belongs to a team. +- ``_get_teams``: Returns the set of teams defined in the auth manager. + +During initialization, Airflow validates that all teams defined in the auth manager are also present in the +Airflow metadata database. If any team is missing, Airflow will raise an error. + +If the auth manager you are using does not implement these methods, Airflow will raise a +``NotImplementedError`` at runtime. + +Example of auth managers compatible with multi-team: + +- :doc:`Simple auth manager ` (recommended for development usage only) +- :doc:`Keycloak auth manager ` + Enabling Multi-Team Mode ------------------------ diff --git a/airflow-core/docs/database-erd-ref.rst b/airflow-core/docs/database-erd-ref.rst index 3f9ea2c77fc65..abb808385e680 100644 --- a/airflow-core/docs/database-erd-ref.rst +++ b/airflow-core/docs/database-erd-ref.rst @@ -33,6 +33,6 @@ Here is the current Database schema diagram. `db command `_ for the commands that you can use to manage the migrations. -.. This image is automatically generated by prek hook via ``scripts/ci/prek/update_er_diagram.py`` +.. This image is automatically generated during documentation build by the ``generate_erd`` Sphinx extension. .. image:: img/airflow_erd.svg diff --git a/airflow-core/docs/howto/connection.rst b/airflow-core/docs/howto/connection.rst index 0b9c352c0c4d9..014da48fc354e 100644 --- a/airflow-core/docs/howto/connection.rst +++ b/airflow-core/docs/howto/connection.rst @@ -283,22 +283,16 @@ Custom connection types Airflow allows the definition of custom connection types -- including modifications of the add/edit form for the connections. Custom connection types are defined in community maintained providers, but you can -can also add a custom provider that adds custom connection types. See :doc:`apache-airflow-providers:index` +also add a custom provider that adds custom connection types. See :doc:`apache-airflow-providers:index` for description on how to add custom providers. -The custom connection types are defined via Hooks delivered by the providers. The Hooks can implement -methods defined in the protocol class :class:`~airflow.hooks.base_hook.DiscoverableHook`. Note that your -custom Hook should not derive from this class, this class is an example to document expectations -regarding about class fields and methods that your Hook might define. Another good example is -:py:class:`~airflow.providers.jdbc.hooks.jdbc.JdbcHook`. - -By implementing those methods in your hooks and exposing them via ``connection-types`` array (and -deprecated ``hook-class-names``) in the provider meta-data, you can customize Airflow by: +By exposing connection types via the ``connection-types`` array in your ``provider.yaml``, you can +customize Airflow by: * Adding custom connection types * Adding automated Hook creation from the connection type -* Adding custom form widget to display and edit custom "extra" parameters in your connection URL -* Hiding fields that are not used for your connection +* Adding custom form fields to display and edit custom "extra" parameters in your connection URL +* Hiding standard fields that are not used for your connection * Adding placeholders showing examples of how fields should be formatted You can read more about details how to add custom providers in the :doc:`apache-airflow-providers:index` @@ -306,11 +300,81 @@ You can read more about details how to add custom providers in the :doc:`apache- Custom connection fields ------------------------ -It is possible to add custom form fields in the connection add / edit views in the Airflow webserver. -Custom fields are stored in the ``Connection.extra`` field as JSON. To add a custom field, implement -method :meth:`~BaseHook.get_connection_form_widgets`. This method should return a dictionary. The keys -should be the string name of the field as it should be stored in the ``extra`` dict. The values should -be inheritors of :class:`wtforms.fields.core.Field`. +.. note:: Preferred approach: define connection UI metadata in ``provider.yaml`` + + From Airflow 3.2, the preferred way to define custom connection fields and field behaviour is + declaratively in ``provider.yaml``. This approach does not require importing + ``flask_appbuilder`` or ``wtforms`` at runtime. + + The Python hook methods ``get_connection_form_widgets()`` and ``get_ui_field_behaviour()`` + continue to work as a fallback and **will only be removed after a deprecation notice and + migration window**. Custom providers written with the older approach will remain working. + However, new providers should use the YAML approach described below. + +Defining connection UI metadata in ``provider.yaml`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Connection form metadata is defined declaratively under ``connection-types`` in your provider's +``provider.yaml`` file. There are two sections: + +**conn-fields** — custom fields stored in ``Connection.extra``: + +.. code-block:: yaml + + connection-types: + - hook-class-name: airflow.providers.myservice.hooks.myservice.MyServiceHook + connection-type: myservice + conn-fields: + workspace: + label: Workspace + schema: + type: + - string + - 'null' + project: + label: Project ID + schema: + type: + - string + - 'null' + +**ui-field-behaviour** — customizations to standard connection fields (hiding, relabeling, placeholders): + +.. code-block:: yaml + + connection-types: + - hook-class-name: airflow.providers.myservice.hooks.myservice.MyServiceHook + connection-type: myservice + ui-field-behaviour: + hidden-fields: + - port + - host + - login + - schema + relabeling: + password: API Token + placeholders: + password: your-api-token + workspace: My workspace gid + project: My project gid + +Field schema types follow `JSON Schema `_ conventions. + +For a full reference of supported ``conn-fields`` schema options, see +`Use Params to Provide a Trigger UI Form `_. + +Defining connection UI metadata in Python (legacy) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. note:: + + The Python method approach below continues to work and will not be removed without a + deprecation notice. However, new providers should use the YAML approach described above. + +It is possible to add custom form fields in the connection add / edit views by implementing +:meth:`~BaseHook.get_connection_form_widgets` on your Hook class. The keys should be the string +name of the field as it should be stored in the ``extra`` dict. The values should be inheritors +of :class:`wtforms.fields.core.Field`. Here's an example: @@ -333,8 +397,8 @@ Here's an example: Prior to Airflow 2.3, if you wanted a custom field in the UI, you had to prefix it with ``extra____``, and this is how its value would be stored in the ``extra`` dict. From 2.3 onward, you no longer need to do this. -Method :meth:`~BaseHook.get_ui_field_behaviour` lets you customize behavior of both . For example you can -hide or relabel a field (e.g. if it's unused or re-purposed) and you can add placeholder text. +Method :meth:`~BaseHook.get_ui_field_behaviour` lets you customize behavior of standard fields. For example +you can hide or relabel a field (e.g. if it's unused or re-purposed) and you can add placeholder text. An example: @@ -355,11 +419,12 @@ An example: .. note:: - If you want to add a form placeholder for an ``extra`` field whose name conflicts with a standard connection attribute (i.e. login, password, host, scheme, port, extra) then - you must prefix it with ``extra____``. E.g. ``extra__myservice__password``. + If you want to add a form placeholder for an ``extra`` field whose name conflicts with a standard + connection attribute (i.e. login, password, host, scheme, port, extra) then you must prefix it + with ``extra____``. E.g. ``extra__myservice__password``. -Take a look at providers for examples of what you can do, for example :py:class:`~airflow.providers.jdbc.hooks.jdbc.JdbcHook` -and :py:class:`~airflow.providers.asana.hooks.jdbc.AsanaHook` both make use of this feature. +Take a look at providers for examples of what you can do, for example +:py:class:`~airflow.providers.jdbc.hooks.jdbc.JdbcHook`. .. note:: Deprecated ``hook-class-names`` diff --git a/airflow-core/docs/howto/customize-ui.rst b/airflow-core/docs/howto/customize-ui.rst index ef330a80517f6..24e4b03589000 100644 --- a/airflow-core/docs/howto/customize-ui.rst +++ b/airflow-core/docs/howto/customize-ui.rst @@ -70,7 +70,7 @@ We can provide a JSON configuration to customize the UI. .. important:: - - Currently only the ``brand`` color palette and ``globalCss`` can be customized. + - You can customize the ``brand`` color palette, ``globalCss`` and the navigation icon via ``icon`` (and ``icon_dark_mode``). - You must supply ``50``-``950`` OKLCH color values for ``brand`` color. - OKLCH colors must have next format ``oklch(l c h)`` For more info see :ref:`config:api__theme` - There is also the ability to provide custom global CSS for a fine grained theme control. @@ -180,6 +180,45 @@ Dark Mode } }' +Icon (SVG-only) +^^^^^^^^^^^^^^^ + +You can replace the default Airflow icon in the navigation bar by providing an ``icon`` key (and optionally +``icon_dark_mode`` for dark color mode) in the ``theme`` configuration. The value must be either an absolute +``http(s)`` URL or an app-relative path starting with ``/``, and must point to an ``.svg`` file. + +.. code-block:: + + [api] + + theme = { + "tokens": { + "colors": { + "brand": { + "50": { "value": "oklch(0.971 0.013 17.38)" }, + "100": { "value": "oklch(0.936 0.032 17.717)" }, + "200": { "value": "oklch(0.885 0.062 18.334)" }, + "300": { "value": "oklch(0.808 0.114 19.571)" }, + "400": { "value": "oklch(0.704 0.191 22.216)" }, + "500": { "value": "oklch(0.637 0.237 25.331)" }, + "600": { "value": "oklch(0.577 0.245 27.325)" }, + "700": { "value": "oklch(0.505 0.213 27.518)" }, + "800": { "value": "oklch(0.444 0.177 26.899)" }, + "900": { "value": "oklch(0.396 0.141 25.723)" }, + "950": { "value": "oklch(0.258 0.092 26.042)" } + } + } + }, + "icon": "/static/company-icon.svg", + "icon_dark_mode": "/static/company-icon-dark.svg" + } + +.. note:: + + - Only SVG icons are supported. + - If the icon fails to load, Airflow falls back to its default icon. + - Icon sizing is controlled by the UI and cannot be configured via the theme. + | Adding Dashboard Alert Messages diff --git a/airflow-core/docs/howto/deadline-alerts.rst b/airflow-core/docs/howto/deadline-alerts.rst index 643e17fc185fb..64f39c0244050 100644 --- a/airflow-core/docs/howto/deadline-alerts.rst +++ b/airflow-core/docs/howto/deadline-alerts.rst @@ -21,13 +21,15 @@ Deadline Alerts .. warning:: Deadline Alerts are new in Airflow 3.1 and should be considered experimental. The feature may be - subject to changes in 3.2 without warning based on user feedback. + subject to changes in future versions without warning based on user feedback. |experimental| Deadline Alerts allow you to set time thresholds for your Dag runs and automatically respond when those -thresholds are exceeded. You can set up Deadline Alerts by choosing a built-in reference point, setting -an interval, and defining a response using either Airflow's Notifiers or a custom callback function. +thresholds are exceeded. You configure Deadline Alerts by choosing a reference point, setting an interval, +and defining a callback to execute if the deadline is missed. A reference may be one of the built-in +DeadlineReference options such as when the dagrun is queued or any custom method that returns a timestamp. +The callback can either be one of Airflow's Notifiers or a custom callback function. Migrating from SLA ------------------ @@ -57,8 +59,7 @@ Below is an example Dag implementation. If the Dag has not finished 15 minutes a .. code-block:: python from datetime import datetime, timedelta - from airflow import DAG - from airflow.sdk import AsyncCallback, DeadlineAlert, DeadlineReference + from airflow.sdk import AsyncCallback, DAG, DeadlineAlert, DeadlineReference from airflow.providers.slack.notifications.slack_webhook import SlackWebhookNotifier from airflow.providers.standard.operators.empty import EmptyOperator @@ -196,18 +197,19 @@ Using Callbacks --------------- When a deadline is exceeded, the callback's callable is executed with the specified kwargs. You can use an -existing :doc:`Notifier ` or create a custom callable. A callback must be an -:class:`~airflow.sdk.AsyncCallback`, with support coming soon for :class:`~airflow.sdk.SyncCallback`. +existing :doc:`Notifier ` or create a custom callable. A callback must be either an +:class:`~airflow.sdk.AsyncCallback`, or a :class:`~airflow.sdk.SyncCallback` (SyncCallback support added in 3.2). Using Built-in Notifiers ^^^^^^^^^^^^^^^^^^^^^^^^ -Here's an example using the Slack Notifier if the Dag run has not finished within 30 minutes of it being queued: +Here's an example using the Slack Notifier with an **asynchronous callback** if the Dag run has not finished +within 30 minutes of it being queued. The callback runs in the Triggerer: .. code-block:: python with DAG( - dag_id="slack_deadline_alert", + dag_id="slack_deadline_alert_async", deadline=DeadlineAlert( reference=DeadlineReference.DAGRUN_QUEUED_AT, interval=timedelta(minutes=30), @@ -221,13 +223,33 @@ Here's an example using the Slack Notifier if the Dag run has not finished withi ): EmptyOperator(task_id="example_task") +Here's the same example using a **synchronous callback**. The callback runs in the executor: + +.. code-block:: python + + with DAG( + dag_id="slack_deadline_alert_sync", + deadline=DeadlineAlert( + reference=DeadlineReference.DAGRUN_QUEUED_AT, + interval=timedelta(minutes=30), + callback=SyncCallback( + SlackWebhookNotifier, + kwargs={ + "text": "🚨 Dag {{ dag_run.dag_id }} missed deadline at {{ deadline.deadline_time }}. DagRun: {{ dag_run }}" + }, + ), + ), + ): + EmptyOperator(task_id="example_task") + Creating Custom Callbacks ^^^^^^^^^^^^^^^^^^^^^^^^^ You can create custom callables for more complex handling. If ``kwargs`` are specified in the ``Callback``, they are passed to the callback function. **Asynchronous callbacks** must be defined somewhere in the -Triggerer's system path. +Triggerer's system path. **Synchronous callbacks** must be importable on the worker where they will be executed. + .. note:: Regarding Async Custom Deadline callbacks: @@ -237,6 +259,11 @@ Triggerer's system path. Nested callables are not currently supported. * The Triggerer will need to be restarted when a callback is added or changed in order to reload the file. +.. note:: + Regarding Synchronous callbacks: + + * Sync callbacks are sent to the executor and treated just like a Dag task with top priority. + .. note:: **Airflow ``context``:** When a deadline is missed, Airflow automatically provides a ``context`` kwarg into the callback containing information about the Dag run and the deadline. To receive it, @@ -245,9 +272,60 @@ Triggerer's system path. the callable accepts. The ``context`` keyword is reserved and cannot be used in the ``kwargs`` parameter of a ``Callback``; attempting to do so will raise a ``ValueError`` at DAG parse time. + +A **custom synchronous callback** might look like this: + +1. Place this method in your plugins folder (e.g. ``$AIRFLOW_HOME/plugins/deadline_callbacks.py``): + +.. code-block:: python + + def custom_sync_callback(**kwargs): + """Handle deadline violation with custom logic.""" + context = kwargs.get("context", {}) + print(f"Deadline exceeded for Dag {context.get('dag_run', {}).get('dag_id')}!") + print(f"Context: {context}") + print(f"Alert type: {kwargs.get('alert_type')}") + # Additional custom handling here + +2. Place this in a Dag file: + +.. code-block:: python + + from datetime import timedelta + + from deadline_callbacks import custom_sync_callback + + from airflow.providers.standard.operators.empty import EmptyOperator + from airflow.sdk import DAG, DeadlineAlert, DeadlineReference, SyncCallback + + with DAG( + dag_id="custom_sync_deadline_alert", + deadline=DeadlineAlert( + reference=DeadlineReference.DAGRUN_QUEUED_AT, + interval=timedelta(minutes=15), + callback=SyncCallback( + custom_sync_callback, + kwargs={"alert_type": "time_exceeded"}, + ), + ), + ): + EmptyOperator(task_id="example_task") + +.. tip:: + ``SyncCallback`` accepts an optional ``executor`` parameter to target a specific executor. + If not specified, the default executor is used. + + .. code-block:: python + + SyncCallback( + my_callback, + kwargs={"msg": "deadline missed"}, + executor="celery_executor", + ) + A **custom asynchronous callback** might look like this: -1. Place this method in ``/files/plugins/deadline_callbacks.py``: +1. Place this method in your plugins folder (e.g. ``$AIRFLOW_HOME/plugins/deadline_callbacks.py``): .. code-block:: python @@ -268,9 +346,8 @@ A **custom asynchronous callback** might look like this: from deadline_callbacks import custom_async_callback - from airflow import DAG from airflow.providers.standard.operators.empty import EmptyOperator - from airflow.sdk import AsyncCallback, DeadlineAlert, DeadlineReference + from airflow.sdk import AsyncCallback, DAG, DeadlineAlert, DeadlineReference with DAG( dag_id="custom_deadline_alert", @@ -302,7 +379,7 @@ A deadline's trigger time is calculated by adding the ``interval`` to the dateti the ``reference``. For ``FIXED_DATETIME`` references, negative intervals can be particularly useful to trigger the callback *before* the reference time. -For example: +In the following examples, ``notify_team`` is either a SyncCallback or AsyncCallback defined elsewhere: .. code-block:: python @@ -348,17 +425,16 @@ implement an ``_evaluate_with()`` method. .. code-block:: python - from airflow.models.deadline import ReferenceModels from sqlalchemy.orm import Session from airflow.sdk import DeadlineReference - from airflow.sdk.definitions.deadline import deadline_reference + from airflow.sdk.definitions.deadline import BaseDeadlineReference, deadline_reference from airflow.sdk.timezone import datetime # By default, the evaluate_with method will be executed when the dagrun is created. @deadline_reference() - class MyCustomDecoratedReference(ReferenceModels.BaseDeadlineReference): + class MyCustomDecoratedReference(BaseDeadlineReference): """A custom reference evaluated when Dag runs are created.""" def _evaluate_with(self, *, session: Session, **kwargs) -> datetime: @@ -368,7 +444,7 @@ implement an ``_evaluate_with()`` method. # You can specify when evaluate_with will be called by providing a DeadlineReference.TYPES value. @deadline_reference(DeadlineReference.TYPES.DAGRUN_QUEUED) - class MyQueuedReference(ReferenceModels.BaseDeadlineReference): + class MyQueuedReference(BaseDeadlineReference): """A custom reference evaluated when Dag runs are queued.""" required_kwargs = {"custom_param"} @@ -386,8 +462,7 @@ Once registered [see notes below], use your custom references in Dag definitions .. code-block:: python from datetime import timedelta - from airflow import DAG - from airflow.sdk import AsyncCallback, DeadlineAlert, DeadlineReference + from airflow.sdk import AsyncCallback, DAG, DeadlineAlert, DeadlineReference with DAG( dag_id="custom_reference_example", @@ -400,6 +475,48 @@ Once registered [see notes below], use your custom references in Dag definitions # Your tasks here ... +Multiple Deadline Alerts +^^^^^^^^^^^^^^^^^^^^^^^^ + +A Dag can have multiple Deadline Alerts. Pass a list to the ``deadline`` parameter instead of a single +``DeadlineAlert``. Each alert in the list is evaluated independently, and each may use any combination +of reference points and callback types (sync or async). + +.. code-block:: python + + from datetime import timedelta + from airflow.sdk import AsyncCallback, DAG, DeadlineAlert, DeadlineReference, SyncCallback + from airflow.providers.slack.notifications.slack_webhook import SlackWebhookNotifier + from airflow.providers.standard.operators.empty import EmptyOperator + + with DAG( + dag_id="multiple_deadline_alerts", + deadline=[ + # First alert: warn via Slack (async) if not done 30 min after queuing + DeadlineAlert( + reference=DeadlineReference.DAGRUN_QUEUED_AT, + interval=timedelta(minutes=30), + callback=AsyncCallback( + SlackWebhookNotifier, + kwargs={"text": "⚠️ Dag {{ dag_run.dag_id }} is approaching its deadline."}, + ), + ), + # Second alert: escalate via custom sync callback if not done 60 min after queuing + DeadlineAlert( + reference=DeadlineReference.DAGRUN_QUEUED_AT, + interval=timedelta(minutes=60), + callback=SyncCallback( + "my_plugins.escalation.escalate_to_oncall", + kwargs={"severity": "high"}, + ), + ), + ], + ): + EmptyOperator(task_id="example_task") + +This pattern is useful for creating tiered alerting strategies — for example, a warning notification +followed by a more urgent escalation if the Dag is still running. + **Important Notes:** * **Timezone Awareness**: Always return timezone-aware datetime objects. diff --git a/airflow-core/docs/img/airflow_erd.sha256 b/airflow-core/docs/img/airflow_erd.sha256 deleted file mode 100644 index a75e1304429ad..0000000000000 --- a/airflow-core/docs/img/airflow_erd.sha256 +++ /dev/null @@ -1 +0,0 @@ -615d06d0b6fd8cf40d0e4f6f7f9eda2c56013fa390b437f569c9eb1a627f40c3 \ No newline at end of file diff --git a/airflow-core/docs/img/airflow_erd.svg b/airflow-core/docs/img/airflow_erd.svg deleted file mode 100644 index f9fc89140cce3..0000000000000 --- a/airflow-core/docs/img/airflow_erd.svg +++ /dev/null @@ -1,3350 +0,0 @@ - - - - - - -%3 - - - -dag_bundle - -dag_bundle - -name - - [VARCHAR(250)] - NOT NULL - -active - - [BOOLEAN] - -last_refreshed - - [TIMESTAMP] - -signed_url_template - - [TEXT] - -template_params - - [JSON] - -version - - [VARCHAR(200)] - - - -dag_bundle_team - -dag_bundle_team - -dag_bundle_name - - [VARCHAR(250)] - NOT NULL - -team_name - - [VARCHAR(50)] - NOT NULL - - - -dag_bundle:name--dag_bundle_team:dag_bundle_name - -0..N -1 - - - -dag - -dag - -dag_id - - [VARCHAR(250)] - NOT NULL - -allowed_run_types - - [JSON] - -asset_expression - - [JSON] - -bundle_name - - [VARCHAR(250)] - NOT NULL - -bundle_version - - [VARCHAR(200)] - -dag_display_name - - [VARCHAR(2000)] - -deadline - - [JSON] - -description - - [TEXT] - -exceeds_max_non_backfill - - [BOOLEAN] - NOT NULL - -fail_fast - - [BOOLEAN] - NOT NULL - -fileloc - - [VARCHAR(2000)] - -has_import_errors - - [BOOLEAN] - NOT NULL - -has_task_concurrency_limits - - [BOOLEAN] - NOT NULL - -is_paused - - [BOOLEAN] - NOT NULL - -is_stale - - [BOOLEAN] - NOT NULL - -last_expired - - [TIMESTAMP] - -last_parse_duration - - [DOUBLE PRECISION] - -last_parsed_time - - [TIMESTAMP] - -max_active_runs - - [INTEGER] - -max_active_tasks - - [INTEGER] - NOT NULL - -max_consecutive_failed_dag_runs - - [INTEGER] - NOT NULL - -next_dagrun - - [TIMESTAMP] - -next_dagrun_create_after - - [TIMESTAMP] - -next_dagrun_data_interval_end - - [TIMESTAMP] - -next_dagrun_data_interval_start - - [TIMESTAMP] - -next_dagrun_partition_date - - [TIMESTAMP] - -next_dagrun_partition_key - - [VARCHAR(255)] - -owners - - [VARCHAR(2000)] - -relative_fileloc - - [VARCHAR(2000)] - -timetable_description - - [VARCHAR(1000)] - -timetable_partitioned - - [BOOLEAN] - NOT NULL - -timetable_summary - - [TEXT] - -timetable_type - - [VARCHAR(255)] - NOT NULL - - - -dag_bundle:name--dag:bundle_name - -0..N -1 - - - -team - -team - -name - - [VARCHAR(50)] - NOT NULL - - - -team:name--dag_bundle_team:team_name - -0..N -1 - - - -connection - -connection - -id - - [INTEGER] - NOT NULL - -conn_id - - [VARCHAR(250)] - NOT NULL - -conn_type - - [VARCHAR(500)] - NOT NULL - -description - - [TEXT] - -extra - - [TEXT] - -host - - [VARCHAR(500)] - -is_encrypted - - [BOOLEAN] - NOT NULL - -is_extra_encrypted - - [BOOLEAN] - NOT NULL - -login - - [TEXT] - -password - - [TEXT] - -port - - [INTEGER] - -schema - - [VARCHAR(500)] - -team_name - - [VARCHAR(50)] - - - -team:name--connection:team_name - -0..N -{0,1} - - - -slot_pool - -slot_pool - -id - - [INTEGER] - NOT NULL - -description - - [TEXT] - -include_deferred - - [BOOLEAN] - NOT NULL - -pool - - [VARCHAR(256)] - NOT NULL - -slots - - [INTEGER] - NOT NULL - -team_name - - [VARCHAR(50)] - - - -team:name--slot_pool:team_name - -0..N -{0,1} - - - -variable - -variable - -id - - [INTEGER] - NOT NULL - -description - - [TEXT] - -is_encrypted - - [BOOLEAN] - NOT NULL - -key - - [VARCHAR(250)] - NOT NULL - -team_name - - [VARCHAR(50)] - -val - - [TEXT] - NOT NULL - - - -team:name--variable:team_name - -0..N -{0,1} - - - -trigger - -trigger - -id - - [INTEGER] - NOT NULL - -classpath - - [VARCHAR(1000)] - NOT NULL - -created_date - - [TIMESTAMP] - NOT NULL - -kwargs - - [TEXT] - NOT NULL - -queue - - [VARCHAR(256)] - -triggerer_id - - [INTEGER] - - - -callback - -callback - -id - - [UUID] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -data - - [JSONB] - NOT NULL - -fetch_method - - [VARCHAR(20)] - NOT NULL - -output - - [TEXT] - -priority_weight - - [INTEGER] - NOT NULL - -state - - [VARCHAR(10)] - -trigger_id - - [INTEGER] - -type - - [VARCHAR(20)] - NOT NULL - - - -trigger:id--callback:trigger_id - -0..N -{0,1} - - - -asset_watcher - -asset_watcher - -asset_id - - [INTEGER] - NOT NULL - -trigger_id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(1500)] - NOT NULL - - - -trigger:id--asset_watcher:trigger_id - -0..N -1 - - - -task_instance - -task_instance - -id - - [UUID] - NOT NULL - -context_carrier - - [JSONB] - -custom_operator_name - - [VARCHAR(1000)] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_version_id - - [UUID] - -duration - - [DOUBLE PRECISION] - -end_date - - [TIMESTAMP] - -executor - - [VARCHAR(1000)] - -executor_config - - [BYTEA] - NOT NULL - -external_executor_id - - [TEXT] - -hostname - - [VARCHAR(1000)] - NOT NULL - -last_heartbeat_at - - [TIMESTAMP] - -map_index - - [INTEGER] - NOT NULL - -max_tries - - [INTEGER] - NOT NULL - -next_kwargs - - [JSONB] - -next_method - - [VARCHAR(1000)] - -operator - - [VARCHAR(1000)] - -pid - - [INTEGER] - -pool - - [VARCHAR(256)] - NOT NULL - -pool_slots - - [INTEGER] - NOT NULL - -priority_weight - - [INTEGER] - NOT NULL - -queue - - [VARCHAR(256)] - NOT NULL - -queued_by_job_id - - [INTEGER] - -queued_dttm - - [TIMESTAMP] - -rendered_map_index - - [VARCHAR(250)] - -run_id - - [VARCHAR(250)] - NOT NULL - -scheduled_dttm - - [TIMESTAMP] - -span_status - - [VARCHAR(250)] - NOT NULL - -start_date - - [TIMESTAMP] - -state - - [VARCHAR(20)] - -task_display_name - - [VARCHAR(2000)] - -task_id - - [VARCHAR(250)] - NOT NULL - -trigger_id - - [INTEGER] - -trigger_timeout - - [TIMESTAMP] - -try_number - - [INTEGER] - NOT NULL - -unixname - - [VARCHAR(1000)] - NOT NULL - -updated_at - - [TIMESTAMP] - - - -trigger:id--task_instance:trigger_id - -0..N -{0,1} - - - -deadline - -deadline - -id - - [UUID] - NOT NULL - -callback_id - - [UUID] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -dagrun_id - - [INTEGER] - -deadline_alert_id - - [UUID] - -deadline_time - - [TIMESTAMP] - NOT NULL - -last_updated_at - - [TIMESTAMP] - NOT NULL - -missed - - [BOOLEAN] - NOT NULL - - - -callback:id--deadline:callback_id - -0..N -1 - - - -asset_alias - -asset_alias - -id - - [INTEGER] - NOT NULL - -group - - [VARCHAR(1500)] - NOT NULL - -name - - [VARCHAR(1500)] - NOT NULL - - - -asset_alias_asset - -asset_alias_asset - -alias_id - - [INTEGER] - NOT NULL - -asset_id - - [INTEGER] - NOT NULL - - - -asset_alias:id--asset_alias_asset:alias_id - -0..N -1 - - - -asset_alias_asset_event - -asset_alias_asset_event - -alias_id - - [INTEGER] - NOT NULL - -event_id - - [INTEGER] - NOT NULL - - - -asset_alias:id--asset_alias_asset_event:alias_id - -0..N -1 - - - -dag_schedule_asset_alias_reference - -dag_schedule_asset_alias_reference - -alias_id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - - - -asset_alias:id--dag_schedule_asset_alias_reference:alias_id - -0..N -1 - - - -asset - -asset - -id - - [INTEGER] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -extra - - [JSON] - NOT NULL - -group - - [VARCHAR(1500)] - NOT NULL - -name - - [VARCHAR(1500)] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - -uri - - [VARCHAR(1500)] - NOT NULL - - - -asset:id--asset_alias_asset:asset_id - -0..N -1 - - - -asset:id--asset_watcher:asset_id - -0..N -1 - - - -asset_active - -asset_active - -name - - [VARCHAR(1500)] - NOT NULL - -uri - - [VARCHAR(1500)] - NOT NULL - - - -asset:name--asset_active:name - -1 -1 - - - -asset:uri--asset_active:uri - -1 -1 - - - -dag_schedule_asset_reference - -dag_schedule_asset_reference - -asset_id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - - - -asset:id--dag_schedule_asset_reference:asset_id - -0..N -1 - - - -task_outlet_asset_reference - -task_outlet_asset_reference - -asset_id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - - - -asset:id--task_outlet_asset_reference:asset_id - -0..N -1 - - - -task_inlet_asset_reference - -task_inlet_asset_reference - -asset_id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - - - -asset:id--task_inlet_asset_reference:asset_id - -0..N -1 - - - -asset_dag_run_queue - -asset_dag_run_queue - -asset_id - - [INTEGER] - NOT NULL - -target_dag_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - - - -asset:id--asset_dag_run_queue:asset_id - -0..N -1 - - - -asset_event - -asset_event - -id - - [INTEGER] - NOT NULL - -asset_id - - [INTEGER] - NOT NULL - -extra - - [JSON] - NOT NULL - -partition_key - - [VARCHAR(250)] - -source_dag_id - - [VARCHAR(250)] - -source_map_index - - [INTEGER] - -source_run_id - - [VARCHAR(250)] - -source_task_id - - [VARCHAR(250)] - -timestamp - - [TIMESTAMP] - NOT NULL - - - -asset_event:id--asset_alias_asset_event:event_id - -0..N -1 - - - -dagrun_asset_event - -dagrun_asset_event - -dag_run_id - - [INTEGER] - NOT NULL - -event_id - - [INTEGER] - NOT NULL - - - -asset_event:id--dagrun_asset_event:event_id - -0..N -1 - - - -job - -job - -id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - -end_date - - [TIMESTAMP] - -executor_class - - [VARCHAR(500)] - -hostname - - [VARCHAR(500)] - -job_type - - [VARCHAR(30)] - -latest_heartbeat - - [TIMESTAMP] - -start_date - - [TIMESTAMP] - -state - - [VARCHAR(20)] - -unixname - - [VARCHAR(1000)] - - - -partitioned_asset_key_log - -partitioned_asset_key_log - -id - - [INTEGER] - NOT NULL - -asset_event_id - - [INTEGER] - NOT NULL - -asset_id - - [INTEGER] - NOT NULL - -asset_partition_dag_run_id - - [INTEGER] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -source_partition_key - - [VARCHAR(250)] - NOT NULL - -target_dag_id - - [VARCHAR(250)] - NOT NULL - -target_partition_key - - [VARCHAR(250)] - NOT NULL - - - -log - -log - -id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - -dttm - - [TIMESTAMP] - NOT NULL - -event - - [VARCHAR(60)] - NOT NULL - -extra - - [TEXT] - -logical_date - - [TIMESTAMP] - -map_index - - [INTEGER] - -owner - - [VARCHAR(500)] - -owner_display_name - - [VARCHAR(500)] - -run_id - - [VARCHAR(250)] - -task_id - - [VARCHAR(250)] - -try_number - - [INTEGER] - - - -dag_priority_parsing_request - -dag_priority_parsing_request - -id - - [VARCHAR(32)] - NOT NULL - -bundle_name - - [VARCHAR(250)] - NOT NULL - -relative_fileloc - - [VARCHAR(2000)] - NOT NULL - - - -import_error - -import_error - -id - - [INTEGER] - NOT NULL - -bundle_name - - [VARCHAR(250)] - -filename - - [VARCHAR(1024)] - -stacktrace - - [TEXT] - -timestamp - - [TIMESTAMP] - - - -revoked_token - -revoked_token - -jti - - [VARCHAR(32)] - NOT NULL - -exp - - [TIMESTAMP] - NOT NULL - - - -dag_schedule_asset_name_reference - -dag_schedule_asset_name_reference - -dag_id - - [VARCHAR(250)] - NOT NULL - -name - - [VARCHAR(1500)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - - - -dag:dag_id--dag_schedule_asset_name_reference:dag_id - -0..N -1 - - - -dag_schedule_asset_uri_reference - -dag_schedule_asset_uri_reference - -dag_id - - [VARCHAR(250)] - NOT NULL - -uri - - [VARCHAR(1500)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - - - -dag:dag_id--dag_schedule_asset_uri_reference:dag_id - -0..N -1 - - - -dag:dag_id--dag_schedule_asset_alias_reference:dag_id - -0..N -1 - - - -dag:dag_id--dag_schedule_asset_reference:dag_id - -0..N -1 - - - -dag:dag_id--task_outlet_asset_reference:dag_id - -0..N -1 - - - -dag:dag_id--task_inlet_asset_reference:dag_id - -0..N -1 - - - -dag:dag_id--asset_dag_run_queue:target_dag_id - -0..N -1 - - - -dag_version - -dag_version - -id - - [UUID] - NOT NULL - -bundle_name - - [VARCHAR(250)] - -bundle_version - - [VARCHAR(250)] - -created_at - - [TIMESTAMP] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -last_updated - - [TIMESTAMP] - NOT NULL - -version_number - - [INTEGER] - NOT NULL - - - -dag:dag_id--dag_version:dag_id - -0..N -1 - - - -dag_tag - -dag_tag - -dag_id - - [VARCHAR(250)] - NOT NULL - -name - - [VARCHAR(100)] - NOT NULL - - - -dag:dag_id--dag_tag:dag_id - -0..N -1 - - - -dag_owner_attributes - -dag_owner_attributes - -dag_id - - [VARCHAR(250)] - NOT NULL - -owner - - [VARCHAR(500)] - NOT NULL - -link - - [VARCHAR(500)] - NOT NULL - - - -dag:dag_id--dag_owner_attributes:dag_id - -0..N -1 - - - -dag_warning - -dag_warning - -dag_id - - [VARCHAR(250)] - NOT NULL - -warning_type - - [VARCHAR(50)] - NOT NULL - -message - - [TEXT] - NOT NULL - -timestamp - - [TIMESTAMP] - NOT NULL - - - -dag:dag_id--dag_warning:dag_id - -0..N -1 - - - -dag_favorite - -dag_favorite - -dag_id - - [VARCHAR(250)] - NOT NULL - -user_id - - [VARCHAR(250)] - NOT NULL - - - -dag:dag_id--dag_favorite:dag_id - -0..N -1 - - - -dag_run - -dag_run - -id - - [INTEGER] - NOT NULL - -backfill_id - - [INTEGER] - -bundle_version - - [VARCHAR(250)] - -clear_number - - [INTEGER] - NOT NULL - -conf - - [JSONB] - -context_carrier - - [JSONB] - -created_at - - [TIMESTAMP] - NOT NULL - -created_dag_version_id - - [UUID] - -creating_job_id - - [INTEGER] - -dag_id - - [VARCHAR(250)] - NOT NULL - -data_interval_end - - [TIMESTAMP] - -data_interval_start - - [TIMESTAMP] - -end_date - - [TIMESTAMP] - -last_scheduling_decision - - [TIMESTAMP] - -log_template_id - - [INTEGER] - NOT NULL - -logical_date - - [TIMESTAMP] - -partition_date - - [TIMESTAMP] - -partition_key - - [VARCHAR(250)] - -queued_at - - [TIMESTAMP] - -run_after - - [TIMESTAMP] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -run_type - - [VARCHAR(50)] - NOT NULL - -scheduled_by_job_id - - [INTEGER] - -span_status - - [VARCHAR(250)] - NOT NULL - -start_date - - [TIMESTAMP] - -state - - [VARCHAR(50)] - NOT NULL - -triggered_by - - [VARCHAR(50)] - -triggering_user_name - - [VARCHAR(512)] - -updated_at - - [TIMESTAMP] - NOT NULL - - - -dag_version:id--dag_run:created_dag_version_id - -0..N -{0,1} - - - -dag_code - -dag_code - -id - - [UUID] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_version_id - - [UUID] - NOT NULL - -fileloc - - [VARCHAR(2000)] - NOT NULL - -last_updated - - [TIMESTAMP] - NOT NULL - -source_code - - [TEXT] - NOT NULL - -source_code_hash - - [VARCHAR(32)] - NOT NULL - - - -dag_version:id--dag_code:dag_version_id - -0..N -1 - - - -serialized_dag - -serialized_dag - -id - - [UUID] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -dag_hash - - [VARCHAR(32)] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_version_id - - [UUID] - NOT NULL - -data - - [JSONB] - -data_compressed - - [BYTEA] - -last_updated - - [TIMESTAMP] - NOT NULL - - - -dag_version:id--serialized_dag:dag_version_id - -0..N -1 - - - -dag_version:id--task_instance:dag_version_id - -0..N -{0,1} - - - -log_template - -log_template - -id - - [INTEGER] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -elasticsearch_id - - [TEXT] - NOT NULL - -filename - - [TEXT] - NOT NULL - - - -log_template:id--dag_run:log_template_id - -0..N -1 - - - -dag_run:id--dagrun_asset_event:dag_run_id - -0..N -1 - - - -asset_partition_dag_run - -asset_partition_dag_run - -id - - [INTEGER] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -created_dag_run_id - - [INTEGER] - -partition_key - - [VARCHAR(250)] - NOT NULL - -target_dag_id - - [VARCHAR(250)] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - - - -dag_run:id--asset_partition_dag_run:created_dag_run_id - -0..N -{0,1} - - - -dag_run:run_id--task_instance:run_id - -0..N -1 - - - -dag_run:dag_id--task_instance:dag_id - -0..N -1 - - - -backfill_dag_run - -backfill_dag_run - -id - - [INTEGER] - NOT NULL - -backfill_id - - [INTEGER] - NOT NULL - -dag_run_id - - [INTEGER] - -exception_reason - - [VARCHAR(250)] - -logical_date - - [TIMESTAMP] - -partition_key - - [VARCHAR(250)] - -sort_ordinal - - [INTEGER] - NOT NULL - - - -dag_run:id--backfill_dag_run:dag_run_id - -0..N -{0,1} - - - -dag_run_note - -dag_run_note - -dag_run_id - - [INTEGER] - NOT NULL - -content - - [VARCHAR(1000)] - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - -user_id - - [VARCHAR(128)] - - - -dag_run:id--dag_run_note:dag_run_id - -1 -1 - - - -dag_run:id--deadline:dagrun_id - -0..N -{0,1} - - - -backfill - -backfill - -id - - [INTEGER] - NOT NULL - -completed_at - - [TIMESTAMP] - -created_at - - [TIMESTAMP] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_run_conf - - [JSON] - NOT NULL - -from_date - - [TIMESTAMP] - NOT NULL - -is_paused - - [BOOLEAN] - -max_active_runs - - [INTEGER] - NOT NULL - -reprocess_behavior - - [VARCHAR(250)] - NOT NULL - -to_date - - [TIMESTAMP] - NOT NULL - -triggering_user_name - - [VARCHAR(512)] - -updated_at - - [TIMESTAMP] - NOT NULL - - - -backfill:id--dag_run:backfill_id - -0..N -{0,1} - - - -backfill:id--backfill_dag_run:backfill_id - -0..N -1 - - - -deadline_alert - -deadline_alert - -id - - [UUID] - NOT NULL - -callback_def - - [JSON] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -description - - [TEXT] - -interval - - [DOUBLE PRECISION] - NOT NULL - -name - - [VARCHAR(250)] - -reference - - [JSON] - NOT NULL - -serialized_dag_id - - [UUID] - NOT NULL - - - -serialized_dag:id--deadline_alert:serialized_dag_id - -0..N -1 - - - -deadline_alert:id--deadline:deadline_alert_id - -0..N -{0,1} - - - -hitl_detail - -hitl_detail - -ti_id - - [UUID] - NOT NULL - -assignees - - [JSON] - -body - - [TEXT] - -chosen_options - - [JSON] - -created_at - - [TIMESTAMP] - NOT NULL - -defaults - - [JSON] - -multiple - - [BOOLEAN] - -options - - [JSON] - NOT NULL - -params - - [JSON] - NOT NULL - -params_input - - [JSON] - NOT NULL - -responded_at - - [TIMESTAMP] - -responded_by - - [JSON] - -subject - - [TEXT] - NOT NULL - - - -task_instance:id--hitl_detail:ti_id - -1 -1 - - - -task_map - -task_map - -dag_id - - [VARCHAR(250)] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -keys - - [JSONB] - -length - - [INTEGER] - NOT NULL - - - -task_instance:run_id--task_map:run_id - -0..N -1 - - - -task_instance:dag_id--task_map:dag_id - -0..N -1 - - - -task_instance:map_index--task_map:map_index - -0..N -1 - - - -task_instance:task_id--task_map:task_id - -0..N -1 - - - -task_reschedule - -task_reschedule - -id - - [INTEGER] - NOT NULL - -duration - - [INTEGER] - NOT NULL - -end_date - - [TIMESTAMP] - NOT NULL - -reschedule_date - - [TIMESTAMP] - NOT NULL - -start_date - - [TIMESTAMP] - NOT NULL - -ti_id - - [UUID] - NOT NULL - - - -task_instance:id--task_reschedule:ti_id - -0..N -1 - - - -xcom - -xcom - -dag_run_id - - [INTEGER] - NOT NULL - -key - - [VARCHAR(512)] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -timestamp - - [TIMESTAMP] - NOT NULL - -value - - [JSONB] - - - -task_instance:run_id--xcom:run_id - -0..N -1 - - - -task_instance:dag_id--xcom:dag_id - -0..N -1 - - - -task_instance:map_index--xcom:map_index - -0..N -1 - - - -task_instance:task_id--xcom:task_id - -0..N -1 - - - -task_instance_note - -task_instance_note - -ti_id - - [UUID] - NOT NULL - -content - - [VARCHAR(1000)] - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - -user_id - - [VARCHAR(128)] - - - -task_instance:id--task_instance_note:ti_id - -1 -1 - - - -task_instance_history - -task_instance_history - -task_instance_id - - [UUID] - NOT NULL - -context_carrier - - [JSONB] - -custom_operator_name - - [VARCHAR(1000)] - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_version_id - - [UUID] - -duration - - [DOUBLE PRECISION] - -end_date - - [TIMESTAMP] - -executor - - [VARCHAR(1000)] - -executor_config - - [BYTEA] - -external_executor_id - - [TEXT] - -hostname - - [VARCHAR(1000)] - -map_index - - [INTEGER] - NOT NULL - -max_tries - - [INTEGER] - -next_kwargs - - [JSONB] - -next_method - - [VARCHAR(1000)] - -operator - - [VARCHAR(1000)] - -pid - - [INTEGER] - -pool - - [VARCHAR(256)] - NOT NULL - -pool_slots - - [INTEGER] - NOT NULL - -priority_weight - - [INTEGER] - -queue - - [VARCHAR(256)] - -queued_by_job_id - - [INTEGER] - -queued_dttm - - [TIMESTAMP] - -rendered_map_index - - [VARCHAR(250)] - -run_id - - [VARCHAR(250)] - NOT NULL - -scheduled_dttm - - [TIMESTAMP] - -span_status - - [VARCHAR(250)] - NOT NULL - -start_date - - [TIMESTAMP] - -state - - [VARCHAR(20)] - -task_display_name - - [VARCHAR(2000)] - -task_id - - [VARCHAR(250)] - NOT NULL - -trigger_id - - [INTEGER] - -trigger_timeout - - [TIMESTAMP] - -try_number - - [INTEGER] - NOT NULL - -unixname - - [VARCHAR(1000)] - -updated_at - - [TIMESTAMP] - - - -task_instance:map_index--task_instance_history:map_index - -0..N -1 - - - -task_instance:task_id--task_instance_history:task_id - -0..N -1 - - - -task_instance:dag_id--task_instance_history:dag_id - -0..N -1 - - - -task_instance:run_id--task_instance_history:run_id - -0..N -1 - - - -rendered_task_instance_fields - -rendered_task_instance_fields - -dag_id - - [VARCHAR(250)] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -k8s_pod_yaml - - [JSON] - -rendered_fields - - [JSON] - NOT NULL - - - -task_instance:run_id--rendered_task_instance_fields:run_id - -0..N -1 - - - -task_instance:task_id--rendered_task_instance_fields:task_id - -0..N -1 - - - -task_instance:map_index--rendered_task_instance_fields:map_index - -0..N -1 - - - -task_instance:dag_id--rendered_task_instance_fields:dag_id - -0..N -1 - - - -hitl_detail_history - -hitl_detail_history - -ti_history_id - - [UUID] - NOT NULL - -assignees - - [JSON] - -body - - [TEXT] - -chosen_options - - [JSON] - -created_at - - [TIMESTAMP] - NOT NULL - -defaults - - [JSON] - -multiple - - [BOOLEAN] - -options - - [JSON] - NOT NULL - -params - - [JSON] - NOT NULL - -params_input - - [JSON] - NOT NULL - -responded_at - - [TIMESTAMP] - -responded_by - - [JSON] - -subject - - [TEXT] - NOT NULL - - - -task_instance_history:task_instance_id--hitl_detail_history:ti_history_id - -1 -1 - - - -edge_job - -edge_job - -dag_id - - [VARCHAR(250)] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -try_number - - [INTEGER] - NOT NULL - -command - - [VARCHAR(2048)] - NOT NULL - -concurrency_slots - - [INTEGER] - NOT NULL - -edge_worker - - [VARCHAR(64)] - -last_update - - [TIMESTAMP] - -queue - - [VARCHAR(256)] - NOT NULL - -queued_dttm - - [TIMESTAMP] - -state - - [VARCHAR(20)] - NOT NULL - - - -alembic_version - -alembic_version - -version_num - - [VARCHAR(32)] - NOT NULL - - - -edge_logs - -edge_logs - -dag_id - - [VARCHAR(250)] - NOT NULL - -log_chunk_time - - [TIMESTAMP] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -try_number - - [INTEGER] - NOT NULL - -log_chunk_data - - [TEXT] - NOT NULL - - - -edge_worker - -edge_worker - -worker_name - - [VARCHAR(64)] - NOT NULL - -first_online - - [TIMESTAMP] - -jobs_active - - [INTEGER] - NOT NULL - -jobs_failed - - [INTEGER] - NOT NULL - -jobs_success - - [INTEGER] - NOT NULL - -jobs_taken - - [INTEGER] - NOT NULL - -last_update - - [TIMESTAMP] - -maintenance_comment - - [VARCHAR(1024)] - -queues - - [VARCHAR(256)] - -state - - [VARCHAR(20)] - NOT NULL - -sysinfo - - [VARCHAR(256)] - - - -alembic_version_edge3 - -alembic_version_edge3 - -version_num - - [VARCHAR(32)] - NOT NULL - - - -ab_user - -ab_user - -id - - [INTEGER] - NOT NULL - -active - - [BOOLEAN] - -changed_by_fk - - [INTEGER] - -changed_on - - [TIMESTAMP] - -created_by_fk - - [INTEGER] - -created_on - - [TIMESTAMP] - -email - - [VARCHAR(512)] - NOT NULL - -fail_login_count - - [INTEGER] - -first_name - - [VARCHAR(256)] - NOT NULL - -last_login - - [TIMESTAMP] - -last_name - - [VARCHAR(256)] - NOT NULL - -login_count - - [INTEGER] - -password - - [VARCHAR(256)] - -username - - [VARCHAR(512)] - NOT NULL - - - -ab_user:id--ab_user:changed_by_fk - -0..N -{0,1} - - - -ab_user:id--ab_user:created_by_fk - -0..N -{0,1} - - - -ab_user_role - -ab_user_role - -id - - [INTEGER] - NOT NULL - -role_id - - [INTEGER] - -user_id - - [INTEGER] - - - -ab_user:id--ab_user_role:user_id - -0..N -{0,1} - - - -ab_user_group - -ab_user_group - -id - - [INTEGER] - NOT NULL - -group_id - - [INTEGER] - -user_id - - [INTEGER] - - - -ab_user:id--ab_user_group:user_id - -0..N -{0,1} - - - -ab_register_user - -ab_register_user - -id - - [INTEGER] - NOT NULL - -email - - [VARCHAR(512)] - NOT NULL - -first_name - - [VARCHAR(256)] - NOT NULL - -last_name - - [VARCHAR(256)] - NOT NULL - -password - - [VARCHAR(256)] - -registration_date - - [TIMESTAMP] - -registration_hash - - [VARCHAR(256)] - -username - - [VARCHAR(512)] - NOT NULL - - - -ab_group - -ab_group - -id - - [INTEGER] - NOT NULL - -description - - [VARCHAR(512)] - -label - - [VARCHAR(150)] - -name - - [VARCHAR(100)] - NOT NULL - - - -ab_group_role - -ab_group_role - -id - - [INTEGER] - NOT NULL - -group_id - - [INTEGER] - -role_id - - [INTEGER] - - - -ab_group:id--ab_group_role:group_id - -0..N -{0,1} - - - -ab_group:id--ab_user_group:group_id - -0..N -{0,1} - - - -ab_role - -ab_role - -id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(64)] - NOT NULL - - - -ab_role:id--ab_group_role:role_id - -0..N -{0,1} - - - -ab_role:id--ab_user_role:role_id - -0..N -{0,1} - - - -ab_permission_view_role - -ab_permission_view_role - -id - - [INTEGER] - NOT NULL - -permission_view_id - - [INTEGER] - -role_id - - [INTEGER] - - - -ab_role:id--ab_permission_view_role:role_id - -0..N -{0,1} - - - -ab_permission - -ab_permission - -id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(100)] - NOT NULL - - - -ab_permission_view - -ab_permission_view - -id - - [INTEGER] - NOT NULL - -permission_id - - [INTEGER] - NOT NULL - -view_menu_id - - [INTEGER] - NOT NULL - - - -ab_permission:id--ab_permission_view:permission_id - -0..N -1 - - - -ab_permission_view:id--ab_permission_view_role:permission_view_id - -0..N -{0,1} - - - -ab_view_menu - -ab_view_menu - -id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(250)] - NOT NULL - - - -ab_view_menu:id--ab_permission_view:view_menu_id - -0..N -1 - - - -alembic_version_fab - -alembic_version_fab - -version_num - - [VARCHAR(32)] - NOT NULL - - - -session - -session - -id - - [INTEGER] - NOT NULL - -data - - [BYTEA] - -expiry - - [TIMESTAMP] - -session_id - - [VARCHAR(255)] - - - diff --git a/airflow-core/docs/installation/supported-versions.rst b/airflow-core/docs/installation/supported-versions.rst index 5b87983ae8db5..6637d18cfd349 100644 --- a/airflow-core/docs/installation/supported-versions.rst +++ b/airflow-core/docs/installation/supported-versions.rst @@ -29,7 +29,7 @@ Apache Airflow® version life cycle: ========= ===================== =================== =============== ===================== ================ Version Current Patch/Minor State First Release Limited Maintenance EOL/Terminated ========= ===================== =================== =============== ===================== ================ -3 3.1.7 Maintenance Apr 22, 2025 TBD TBD +3 3.1.8 Maintenance Apr 22, 2025 TBD TBD 2 2.11.1 Limited maintenance Dec 17, 2020 Oct 22, 2025 Apr 22, 2026 1.10 1.10.15 EOL Aug 27, 2018 Dec 17, 2020 June 17, 2021 1.9 1.9.0 EOL Jan 03, 2018 Aug 27, 2018 Aug 27, 2018 diff --git a/airflow-core/docs/security/secrets/secrets-backend/index.rst b/airflow-core/docs/security/secrets/secrets-backend/index.rst index a4ae1547a103d..029dadcfb875a 100644 --- a/airflow-core/docs/security/secrets/secrets-backend/index.rst +++ b/airflow-core/docs/security/secrets/secrets-backend/index.rst @@ -78,6 +78,29 @@ the example below. $ airflow config get-value secrets backend airflow.providers.google.cloud.secrets.secret_manager.CloudSecretManagerBackend +Setting individual backend kwargs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Instead of encoding all kwargs as a JSON blob, you can set each one as a separate environment +variable using the ``AIRFLOW__SECRETS__BACKEND_KWARG__`` prefix: + +.. code-block:: bash + + # These two are equivalent: + export AIRFLOW__SECRETS__BACKEND_KWARGS='{"role_id": "abc", "secret_id": "xyz"}' + + # or individually (useful for K8s Secrets): + export AIRFLOW__SECRETS__BACKEND_KWARG__ROLE_ID=abc + export AIRFLOW__SECRETS__BACKEND_KWARG__SECRET_ID=xyz + +Per-key variables override the same key from ``BACKEND_KWARGS``. Values are raw strings +(not JSON-parsed). For workers, use the +``AIRFLOW__WORKERS__SECRETS_BACKEND_KWARG__`` prefix. + +.. note:: + These environment variables are masked in logs at startup, the same way + ``BACKEND_KWARGS`` is masked. + Worker Specific Configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/airflow-core/docs/security/security_model.rst b/airflow-core/docs/security/security_model.rst index 07f02ac70e478..15b59b250904c 100644 --- a/airflow-core/docs/security/security_model.rst +++ b/airflow-core/docs/security/security_model.rst @@ -106,7 +106,7 @@ sensitive information accessible through connection configuration. They also have the ability to create a API Server Denial of Service situation and should be trusted not to misuse this capability. -Only admin users have access to audit logs. +Only admin users have access to audit logs by default. Operations users ................ @@ -277,11 +277,30 @@ executing arbitrary code. This is all fully in hands of the Deployment Manager a following chapter. Access to all Dags -........................................................................ +.................. All Dag authors have access to all Dags in the Airflow deployment. This means that they can view, modify, and update any Dag without restrictions at any time. +Custom RBAC limitations +----------------------- + +While RBAC defined in Airflow might limit access for certain UI users to certain Dags and features, when +it comes to custom roles and permissions, some permissions might override individual access to Dags or lack +of those. For example - audit log permission allows the user who has it to see logs of all Dags, even if +they don't have access to those Dags explicitly. This is something that the Deployment Manager +should be aware of when creating custom RBAC roles. + +Triggering Dags via Assets +-------------------------- + +Triggering Dags via Assets is a feature that allows an asset materialization to trigger a Dag. This feature +is designed to allow triggering Dags without giving users specific access to triggering the Dags manually. +The "Trigger Dag" permission only affects triggering dags manually via the UI or API, but it does not affect +triggering Dags via Assets. Dag authors explicitly allow for specific assets to trigger the Dags and +they give anyone who has capability to create those assets to trigger the Dags via Assets. + + Responsibilities of Deployment Managers --------------------------------------- diff --git a/airflow-core/docs/start.rst b/airflow-core/docs/start.rst index a2ed26fd34b7f..e1263ccd57ea1 100644 --- a/airflow-core/docs/start.rst +++ b/airflow-core/docs/start.rst @@ -67,7 +67,11 @@ This quick start guide will help you bootstrap an Airflow standalone instance on For creating virtual environment with ``uv``, refer to the documentation here: `Creating and Maintaining Local virtual environment with uv `_ -For installation using ``pip`` and ``venv``, carry out following steps: +For installation using ``pip`` and ``venv``, carry out following steps. +On Debian/Ubuntu systems, Python may enforce +externally managed environments (PEP 668), so use a virtual environment +before running ``pip install`` commands: + .. code-block:: bash # For Windows after WSL2 install, restart computer, then in WSL Ubuntu terminal diff --git a/airflow-core/docs/templates-ref.rst b/airflow-core/docs/templates-ref.rst index 2d9aaec2f291d..c908e03da9411 100644 --- a/airflow-core/docs/templates-ref.rst +++ b/airflow-core/docs/templates-ref.rst @@ -70,8 +70,7 @@ Variable Type Description ``{{ prev_start_date_success }}`` `pendulum.DateTime`_ Start date from prior successful :class:`~airflow.models.dagrun.DagRun` (if available). | ``None`` ``{{ prev_end_date_success }}`` `pendulum.DateTime`_ End date from prior successful :class:`~airflow.models.dagrun.DagRun` (if available). - | ``None`` -``{{ start_date }}`` `pendulum.DateTime`_ Datetime of when current task has been started. + | ``None`` ``{{ inlets }}`` list List of inlets declared on the task. ``{{ inlet_events }}`` dict[str, ...] Access past events of inlet assets. See :doc:`Assets `. Added in version 2.10. ``{{ outlets }}`` list List of outlets declared on the task. diff --git a/airflow-core/docs/tutorial/hitl.rst b/airflow-core/docs/tutorial/hitl.rst index ab5c6ed0175b2..3a5e35c6f7918 100644 --- a/airflow-core/docs/tutorial/hitl.rst +++ b/airflow-core/docs/tutorial/hitl.rst @@ -159,7 +159,7 @@ The method ``HITLOperator.generate_link_to_ui_from_context`` can be used to gene - ``context`` – automatically passed to ``notify`` by the notifier - ``base_url`` – (optional) the base URL of the Airflow UI; if not provided, ``api.base_url`` in the configuration will be used -- ``options`` – (optional) pre-selected options for the UI page +- ``options`` – (optional) preselected options for the UI page - ``params_inputs`` – (optional) pre-loaded inputs for the UI page This makes it easy to include actionable links in notifications or logs. diff --git a/airflow-core/hatch_build.py b/airflow-core/hatch_build.py index 018067a87a9ae..e199d4b70d343 100644 --- a/airflow-core/hatch_build.py +++ b/airflow-core/hatch_build.py @@ -65,7 +65,7 @@ def build_standard(self, directory: str, artifacts: Any, **build_data: Any) -> s self.write_git_version() # run this in the parent directory of the airflow-core (i.e. airflow repo root) work_dir = Path(self.root).parent.resolve() - cmd = ["prek", "run", "--hook-stage", "manual", "compile-ui-assets", "--all-files"] + cmd = ["prek", "run", "--stage", "manual", "compile-ui-assets", "--all-files"] log.warning("Running command: %s", " ".join(cmd)) run(cmd, cwd=work_dir.as_posix(), check=True) dist_path = Path(self.root) / "src" / "airflow" / "ui" / "dist" diff --git a/airflow-core/newsfragments/61153.significant.rst b/airflow-core/newsfragments/61153.significant.rst new file mode 100644 index 0000000000000..51f4727c240ae --- /dev/null +++ b/airflow-core/newsfragments/61153.significant.rst @@ -0,0 +1,19 @@ +Add synchronous callback support (``SyncCallback``) for Deadline Alerts + +Deadline Alerts now support synchronous callbacks via ``SyncCallback`` in addition to the existing +asynchronous ``AsyncCallback``. Synchronous callbacks are executed by the executor (rather than +the triggerer), and can optionally target a specific executor via the ``executor`` parameter. + +A DAG can also define multiple Deadline Alerts by passing a list to the ``deadline`` parameter, +and each alert can use either callback type. + +* Types of change + + * [ ] Dag changes + * [ ] Config changes + * [ ] API changes + * [ ] CLI changes + * [x] Behaviour changes + * [ ] Plugin changes + * [ ] Dependency changes + * [ ] Code interface changes diff --git a/airflow-core/newsfragments/61274.improvement.rst b/airflow-core/newsfragments/61274.improvement.rst new file mode 100644 index 0000000000000..ab9f4f4c0f085 --- /dev/null +++ b/airflow-core/newsfragments/61274.improvement.rst @@ -0,0 +1 @@ +Improve Dag callback relevancy by passing a context-relevant task instance based on the Dag's final state (e.g., the last failed, timed out, or successful task) instead of an arbitrary lexicographical selection. diff --git a/airflow-core/newsfragments/61400.significant.rst b/airflow-core/newsfragments/61400.significant.rst deleted file mode 100644 index 7bdee4390a7ea..0000000000000 --- a/airflow-core/newsfragments/61400.significant.rst +++ /dev/null @@ -1,20 +0,0 @@ -AuthManager Backfill permissions are now handled by the ``requires_access_dag`` on the ``DagAccessEntity.Run`` - -``is_authorized_backfill`` of the ``BaseAuthManager`` interface has been removed. Core will no longer call this method and their -provider counterpart implementation will be marked as deprecated. -Permissions for backfill operations are now checked against the ``DagAccessEntity.Run`` permission using the existing -``requires_access_dag`` decorator. In other words, if a user has permission to run a DAG, they can perform backfill operations on it. - -Please update your security policies to ensure that users who need to perform backfill operations have the appropriate ``DagAccessEntity.Run`` permissions. (Users -having the Backfill permissions without having the DagRun ones will no longer be able to perform backfill operations without any update) - -* Types of change - - * [ ] Dag changes - * [ ] Config changes - * [x] API changes - * [ ] CLI changes - * [x] Behaviour changes - * [ ] Plugin changes - * [ ] Dependency changes - * [ ] Code interface changes diff --git a/airflow-core/newsfragments/62121.bugfix.rst b/airflow-core/newsfragments/62121.bugfix.rst deleted file mode 100644 index a35d8ae3bb765..0000000000000 --- a/airflow-core/newsfragments/62121.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Elasticsearch is now fully compatible with remote logging along side with apache-airflow-providers-elasticsearch>=6.5.0. Please review elasticsearch provider release notes for more information https://airflow.apache.org/docs/apache-airflow-providers-elasticsearch/6.5.0/changelog.html diff --git a/airflow-core/newsfragments/62344.feature.rst b/airflow-core/newsfragments/62344.feature.rst new file mode 100644 index 0000000000000..7fd43b0c03fe3 --- /dev/null +++ b/airflow-core/newsfragments/62344.feature.rst @@ -0,0 +1 @@ +CLI ``connections list`` and ``variables list`` now hide sensitive values by default. Use ``--show-values`` to display full details and ``--hide-sensitive`` to mask passwords, URIs, and extras. diff --git a/airflow-core/newsfragments/63141.bugfix.rst b/airflow-core/newsfragments/63141.bugfix.rst new file mode 100644 index 0000000000000..c9855b3f5e49b --- /dev/null +++ b/airflow-core/newsfragments/63141.bugfix.rst @@ -0,0 +1 @@ +Fix security iframe navigation when AIRFLOW__API__BASE_URL basename is configured diff --git a/airflow-core/newsfragments/63205.bugfix.rst b/airflow-core/newsfragments/63205.bugfix.rst new file mode 100644 index 0000000000000..7e1781bc8ed32 --- /dev/null +++ b/airflow-core/newsfragments/63205.bugfix.rst @@ -0,0 +1 @@ +Fix grid view URL for dynamic task groups producing 404 by not appending ``/mapped`` to group URLs. diff --git a/airflow-core/newsfragments/63312.feature.rst b/airflow-core/newsfragments/63312.feature.rst new file mode 100644 index 0000000000000..1af30e04dedb1 --- /dev/null +++ b/airflow-core/newsfragments/63312.feature.rst @@ -0,0 +1 @@ +Allow individual secrets backend kwargs to be set via ``AIRFLOW__SECRETS__BACKEND_KWARG__`` environment variables diff --git a/airflow-core/newsfragments/63365.significant.rst b/airflow-core/newsfragments/63365.significant.rst new file mode 100644 index 0000000000000..494055ec52534 --- /dev/null +++ b/airflow-core/newsfragments/63365.significant.rst @@ -0,0 +1,9 @@ +Structured JSON logging for all API server output + +The new ``json_logs`` option under the ``[logging]`` section makes Airflow +produce all its output as newline-delimited JSON (structured logs) instead of +human-readable formatted logs. This covers the API server (gunicorn/uvicorn), +including access logs, warnings, and unhandled exceptions. + +Not all components support this yet — notably ``airflow celery worker`` but +any non-JSON output when ``json_logs`` is enabled will be treated as a bug. diff --git a/airflow-core/newsfragments/63452.significant.rst b/airflow-core/newsfragments/63452.significant.rst new file mode 100644 index 0000000000000..b0ffc64d4eae8 --- /dev/null +++ b/airflow-core/newsfragments/63452.significant.rst @@ -0,0 +1,7 @@ +Remove legacy OTel Trace metaclass and shared tracer wrappers + +The interfaces and functions located in ``airflow.traces`` were +internal code that provided a standard way to manage spans in +internal Airflow code. They were not intended as user-facing code +and were never documented. They are no longer needed so we +remove them in 3.2. diff --git a/airflow-core/newsfragments/continuous-optional-start-date.improvement.rst b/airflow-core/newsfragments/continuous-optional-start-date.improvement.rst new file mode 100644 index 0000000000000..c001c62093345 --- /dev/null +++ b/airflow-core/newsfragments/continuous-optional-start-date.improvement.rst @@ -0,0 +1 @@ +The ``schedule="@continuous"`` parameter now works without requiring a ``start_date``, and any DAGs with this schedule will begin running immediately when unpaused. diff --git a/airflow-core/pyproject.toml b/airflow-core/pyproject.toml index 627c07fe898f7..97ab77007a98f 100644 --- a/airflow-core/pyproject.toml +++ b/airflow-core/pyproject.toml @@ -23,7 +23,7 @@ requires = [ "packaging==26.0", "pathspec==1.0.4", "pluggy==1.6.0", - "smmap==5.0.2", + "smmap==5.0.3", "tomli==2.4.0; python_version < '3.11'", "trove-classifiers==2026.1.14.14", ] @@ -237,6 +237,8 @@ exclude = [ "src/airflow/ui/node_modules/", "src/airflow/api_fastapi/auth/managers/simple/ui/node_modules", "src/airflow/ui/openapi.merged.json", + "src/airflow/_shared/AGENTS.md", + "src/airflow/_shared/README.md", ] [tool.hatch.build.targets.sdist.force-include] @@ -273,6 +275,8 @@ exclude = [ # Only dist/ (declared as artifacts) is needed "src/airflow/ui/**", "src/airflow/api_fastapi/auth/managers/simple/ui/**", + "src/airflow/_shared/AGENTS.md", + "src/airflow/_shared/README.md", ] [dependency-groups] diff --git a/airflow-core/src/airflow/_shared/AGENTS.md b/airflow-core/src/airflow/_shared/AGENTS.md new file mode 100644 index 0000000000000..1947c43f88ffc --- /dev/null +++ b/airflow-core/src/airflow/_shared/AGENTS.md @@ -0,0 +1,14 @@ + + + +# The `_shared` package — Agent Instructions + +Each shared library is a symbolic link to the library package sources from the shared library +located in the [shared folder](../../shared). In the shared folder each library is a separate +distribution that has it's own tests and dependencies. Those dependencies and links to those +libraries are maintained by `prek` hook automatically. + +When you modify any of the files in place here - because those are symbolic links - they are +modified in the corresponding shared library, so any modification here should result in modifying +and running tests in the shared library the folder points to. diff --git a/airflow-core/src/airflow/_shared/README.md b/airflow-core/src/airflow/_shared/README.md new file mode 100644 index 0000000000000..658af6b5d975a --- /dev/null +++ b/airflow-core/src/airflow/_shared/README.md @@ -0,0 +1,35 @@ + + +## Why symbolic links here ? + +The sub-folders you see in this folder are symbolic links to the actual code in the `shared` folder. +The reason we are doing it is that we want to be able to use the shared libraries in different +distributions potentially in different versions - when several packages are using the same shared library. + +Python - unlike for example npm - does not have a way to install different versions of the same distribution, +so what we effectively have to do is to vendor-in those shared libraries in different packages that are +using those libraries. + +By employing symbolic links we can avoid code duplication (single source of current version of the shared +library code is stored in "shared" folder) - and at the same time we can have different versions of the +same shared library in different packages when for example `airflow-core` and `task-sdk` package are +installed together in different version. + +You can read about it in [the shared README.md](../../shared/README.md) document. diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py index 4d7c985035156..0f89fd20c4cb7 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py @@ -433,7 +433,11 @@ def _generate_password() -> str: @staticmethod def _print_output(output: str): - name = "Simple auth manager" - colorized_name = colored(f"{name:10}", "white") - for line in output.splitlines(): - print(f"{colorized_name} | {line.strip()}") + if conf.getboolean("logging", "json_logs", fallback=False): + for line in output.splitlines(): + log.info(line.strip()) + else: + name = "Simple auth manager" + colorized_name = colored(f"{name:10}", "white") + for line in output.splitlines(): + print(f"{colorized_name} | {line.strip()}") diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json index f6c64221f1d34..744656caee0e1 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json @@ -18,9 +18,9 @@ "coverage": "vitest run --coverage" }, "dependencies": { - "@chakra-ui/react": "^3.33.0", + "@chakra-ui/react": "^3.34.0", "@hey-api/client-axios": "^0.9.1", - "@hey-api/openapi-ts": "^0.93.1", + "@hey-api/openapi-ts": "^0.94.0", "@tanstack/react-query": "^5.90.21", "axios": "^1.13.6", "next-themes": "^0.4.6", @@ -54,7 +54,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-unicorn": "^63.0.0", - "happy-dom": "^20.7.0", + "happy-dom": "^20.8.3", "prettier": "^3.8.1", "ts-morph": "^27.0.2", "typescript": "~5.9.3", @@ -69,7 +69,7 @@ "esbuild" ], "overrides": { - "tar@<7.5.10": ">=7.5.10", + "tar@<7.5.11": ">=7.5.11", "lodash-es@>=4.0.0 <=4.17.22": ">=4.17.23", "minimatch@<10.2.3": ">=10.2.3", "ajv@<6.14.0": ">=6.14.0", diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml index a852b0206d757..061cb4e7cd2e0 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - tar@<7.5.10: '>=7.5.10' + tar@<7.5.11: '>=7.5.11' lodash-es@>=4.0.0 <=4.17.22: '>=4.17.23' minimatch@<10.2.3: '>=10.2.3' ajv@<6.14.0: '>=6.14.0' @@ -16,14 +16,14 @@ importers: .: dependencies: '@chakra-ui/react': - specifier: ^3.33.0 - version: 3.33.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^3.34.0 + version: 3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@hey-api/client-axios': specifier: ^0.9.1 - version: 0.9.1(@hey-api/openapi-ts@0.93.1(magicast@0.3.5)(typescript@5.9.3))(axios@1.13.6) + version: 0.9.1(@hey-api/openapi-ts@0.94.0(magicast@0.3.5)(typescript@5.9.3))(axios@1.13.6) '@hey-api/openapi-ts': - specifier: ^0.93.1 - version: 0.93.1(magicast@0.3.5)(typescript@5.9.3) + specifier: ^0.94.0 + version: 0.94.0(magicast@0.3.5)(typescript@5.9.3) '@tanstack/react-query': specifier: ^5.90.21 version: 5.90.21(react@19.2.4) @@ -87,10 +87,10 @@ importers: version: 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) '@vitejs/plugin-react-swc': specifier: ^4.2.3 - version: 4.2.3(@swc/helpers@0.5.18)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)) + version: 4.2.3(@swc/helpers@0.5.19)(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)) '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1)) + version: 4.0.18(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)) eslint: specifier: ^10.0.2 version: 10.0.2(jiti@2.6.1) @@ -119,8 +119,8 @@ importers: specifier: ^63.0.0 version: 63.0.0(eslint@10.0.2(jiti@2.6.1)) happy-dom: - specifier: ^20.7.0 - version: 20.7.0 + specifier: ^20.8.3 + version: 20.8.3 prettier: specifier: ^3.8.1 version: 3.8.1 @@ -135,13 +135,13 @@ importers: version: 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1) + version: 7.3.1(@types/node@25.3.5)(jiti@2.6.1) vite-plugin-css-injected-by-js: specifier: ^4.0.1 - version: 4.0.1(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)) + version: 4.0.1(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1) + version: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1) packages: @@ -161,8 +161,8 @@ packages: resolution: {integrity: sha512-pRrmXMCwnmrkS3MLgAIW5dXRzeTv6GLjkjb4HmxNnvAKXN1Nfzp4KmGADBQvlVUcqi+a5D+hfGDLLnd5NnYxog==} engines: {node: '>= 16'} - '@ark-ui/react@5.31.0': - resolution: {integrity: sha512-XHzq6Y3VcORoMCk4KfkAxauyuk8sTtllb1FaD3dcKfKRxIf6fw1mlAHfGIofuaqtTnP0mt0RX0ohzCsEG7ityQ==} + '@ark-ui/react@5.34.1': + resolution: {integrity: sha512-RJlXCvsHzbK9LVxUVtaSD5pyF1PL8IUR1rHHkf0H0Sa397l6kOFE4EH7MCSj3pDumj2NsmKDVeVgfkfG0KCuEw==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' @@ -280,8 +280,8 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@chakra-ui/react@3.33.0': - resolution: {integrity: sha512-HNbUFsFABjVL5IHBxsqtuT+AH/vQT1+xsEWrxnG0GBM2VjlzlMqlqCxNiDyQOsjLZXQC1ciCMbzPNcSCc63Y9w==} + '@chakra-ui/react@3.34.0': + resolution: {integrity: sha512-VLhpVwv5IVxhwajO10KnS1VQT4hDqQMQP/A796Ya+uVu8AdoSX+5HHyTLTkYIeXIDMe0xLqJfov04OBKbBchJA==} peerDependencies: '@emotion/react': '>=11' react: '>=18' @@ -535,14 +535,14 @@ packages: resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@floating-ui/core@1.7.4': - resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - '@floating-ui/dom@1.7.5': - resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - '@floating-ui/utils@0.2.10': - resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} '@hey-api/client-axios@0.9.1': resolution: {integrity: sha512-fvpOdnEz6tu5T2+IMNZW3g9mAZwaXavqpsvtapEZNtYxyYtQ+lQs9wJn/VPhZEvdXAXu8HPTCRpmfa0t1aRATA==} @@ -555,8 +555,8 @@ packages: resolution: {integrity: sha512-T8T3yCl2+AiVVDP6tvfnU/rXOkEHddMTOYCZXUVbydj7URVErh5BelIa8UWBkFYZBP2/mi2nViScNhe9eBolPw==} deprecated: Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts. - '@hey-api/codegen-core@0.7.0': - resolution: {integrity: sha512-HglL4B4QwpzocE+c8qDU6XK8zMf8W8Pcv0RpFDYxHuYALWLTnpDUuEsglC7NQ4vC1maoXsBpMbmwpco0N4QviA==} + '@hey-api/codegen-core@0.7.1': + resolution: {integrity: sha512-X5qG+rr/BJvr+pEGcoW6l2azoZGrVuxsviEIhuf+3VwL9bk0atfubT65Xwo+4jDxXvjbhZvlwS0Ty3I7mLE2fg==} engines: {node: '>=20.19.0'} peerDependencies: typescript: '>=5.5.3' @@ -572,15 +572,15 @@ packages: peerDependencies: typescript: ^5.x - '@hey-api/openapi-ts@0.93.1': - resolution: {integrity: sha512-oQJPHiVkJKesZFpoW3jfQhrSQ7xdgzai7895ENl6ZDjCaIK6bOUTly7bsu+7+0ONsGH9jbtGbkoUzC+MtY+RKg==} + '@hey-api/openapi-ts@0.94.0': + resolution: {integrity: sha512-dbg3GG+v7sg9/Ahb7yFzwzQIJwm151JAtsnh9KtFyqiN0rGkMGA3/VqogEUq1kJB9XWrlMQwigwzhiEQ33VCSg==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: typescript: '>=5.5.3' - '@hey-api/shared@0.2.1': - resolution: {integrity: sha512-uWI9047e9OVe3Ss+6vPMnRiixjRcjcBbdgpeq4IQymet3+wsn0+N/4RLDHBz1h57SemaxayPRUA0JOOsuC1qyA==} + '@hey-api/shared@0.2.2': + resolution: {integrity: sha512-vMqCS+j7F9xpWoXC7TBbqZkaelwrdeuSB+s/3elu54V5iq++S59xhkSq5rOgDIpI1trpE59zZQa6dpyUxItOgw==} engines: {node: '>=20.19.0'} peerDependencies: typescript: '>=5.5.3' @@ -606,8 +606,8 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@internationalized/date@3.10.0': - resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==} + '@internationalized/date@3.11.0': + resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==} '@internationalized/number@3.6.5': resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} @@ -635,8 +635,8 @@ packages: '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} - '@pandacss/is-valid-prop@1.8.2': - resolution: {integrity: sha512-wfZ4kzvHXQ9pG3wuIGTUCcbC7Op8pqIQZNIRY/bqWQu67WTHxZUHUPSZgQMhguI8Tz4ot+DNf4Qhha0bhLvNEQ==} + '@pandacss/is-valid-prop@1.9.0': + resolution: {integrity: sha512-AZvpXWGyjbHc8TC+YVloQ31Z2c4j2xMvYj6UfVxuZdB5w4c9+4N8wy5R7I/XswNh8e4cfUlkvsEGDXjhJRgypw==} '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} @@ -868,8 +868,8 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.18': - resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} + '@swc/helpers@0.5.19': + resolution: {integrity: sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==} '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} @@ -948,8 +948,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@25.3.0': - resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} + '@types/node@25.3.5': + resolution: {integrity: sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1071,231 +1071,234 @@ packages: '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} - '@zag-js/accordion@1.33.1': - resolution: {integrity: sha512-D80BZxceCIrxaXCi4CWDIzrCNJtojTGysD23C8FOxEGm9pQVuF7NvIdes7lbfUvwlZypMUUvhVlh8kKXN9uyeQ==} + '@zag-js/accordion@1.35.3': + resolution: {integrity: sha512-wmw6yo5Zr6ShiKGTc5ICEOJCurWAOSGubIpGISiHi3cZ4tlxKF/vpATIUT3eq8xzdB56YK57yKCujs/WmwqqoA==} - '@zag-js/anatomy@1.33.1': - resolution: {integrity: sha512-iME14VHGGEPNMakilI6qvEkv9sll4AFZHpeoMLpczesw5hmqQjjNRifDTPR+idqCb8O8PdkAPE9hyMeP+4JjtA==} + '@zag-js/anatomy@1.35.3': + resolution: {integrity: sha512-oqU9iLNNylrtJMBX5Xu4DsxnPNvtZLiobryv2oNtsDI1mi1Fca/XHghQC9K5aYT0qNsmHj1M3W5WAWTaOtPLkQ==} - '@zag-js/angle-slider@1.33.1': - resolution: {integrity: sha512-Y44IND5koNWD/EMKEWJbuEnzNW9y1WsrQFFvKRsMp/m3n60hiLa8qtZHoZWm8eOZCKFlsjVJ0gueEuZp43nobA==} + '@zag-js/angle-slider@1.35.3': + resolution: {integrity: sha512-HXRlmsbNEJSBT53fq9XQKL/vwZWwJC3nprskI7s4f/jy8a4uXPTlv7N7zuBYjew+ScTMzZah6fLWzUztBehmSg==} - '@zag-js/aria-hidden@1.33.1': - resolution: {integrity: sha512-TpRAtssDHVfra5qxigk7w1NMf/crKu615INu6GAbNNMUBWD1rPZAfxdg/xe/BAcxLy+XM5/q62dVSNvpjXzN3g==} + '@zag-js/aria-hidden@1.35.3': + resolution: {integrity: sha512-dk5POebn10WneQfLrEgbTzwolaXWpCSHL6F3jCTinW9IbOx7BXghzJD21iU5Iun+y9CorqJPW3p7LplYNUMO5Q==} - '@zag-js/async-list@1.33.1': - resolution: {integrity: sha512-K0OFoN9hKjM5y029kRi52sjiAct1Wl3dbcZShXZypET/Y2rGv4q9ghasuU8jyX2oAoRwBtofwQgg8nrcoxBLFg==} + '@zag-js/async-list@1.35.3': + resolution: {integrity: sha512-SXX3wGzLK/maKS1PJ3XfLIGWbu0022f/OhcFsT1PbiHnoFZTH7h2fBhirrCBfy2TYFQ6r5uxgjkhPUNkuaeYnA==} - '@zag-js/auto-resize@1.33.1': - resolution: {integrity: sha512-ci+hotx5/1zig1+Z2ljNBZEQ1OWhd6MV/E/X7suXmzK3lfvMb+g4OX2FjkuGqumwZyStrg4kh/ZJ+7Bj1CxRsw==} + '@zag-js/auto-resize@1.35.3': + resolution: {integrity: sha512-ufG8HSqzLd9h5rnos8aumj8iORlRskeR/gbpJu1NHrnHBWIrpuXm6KJJR2oZhTFY1BUMMk8eYIBA2QkVuiJzWA==} - '@zag-js/avatar@1.33.1': - resolution: {integrity: sha512-D8HBPvIVLoty14CDx6wWfdfcalr/pf2FgJ0N7VTgExvZt8t64JWJarL75ZkIB3ROaNe4RMFdzabz1uc7BlcDyg==} + '@zag-js/avatar@1.35.3': + resolution: {integrity: sha512-lbQ2Q4Va8AAScKULOHw2tCQez+0JRYGHSMFq6i+dJmeT3dlSgRanm69ra6K2po6hM9E4v6pRe+xOVE+9QMDnuA==} - '@zag-js/bottom-sheet@1.33.1': - resolution: {integrity: sha512-yWTAgbbb7N2B6epoq/Jpkaix8qNJz6OLZ6jDaHuZDnrEoM/LzQTHA77LQbjcWulmggBwX9IKPm1xeqFWXiHmeQ==} + '@zag-js/carousel@1.35.3': + resolution: {integrity: sha512-F+b8HzUeZfB+xUkAkLG4r0Ubui8pj7pSgZhi26ZiWgsM7tsd7cD+xRMXkvPEITN5Fd5QCe3KlVBuE00w5byjmg==} - '@zag-js/carousel@1.33.1': - resolution: {integrity: sha512-FB72jCHhTTn0gXsWwDT/DrGMpBHQTxlKvwjEiBGkcprWVpptN0WGJR+EtX2Si/668sdH/471rew2DKA+h5k6Tw==} + '@zag-js/cascade-select@1.35.3': + resolution: {integrity: sha512-Nifdx77hEuAdXqr1wpZSPjLXqygRhq/WvnPjGhCeSqFPpy62uT4JZ3avyjUZ4I0UhvIpkleUcXtFwQ3cSMh4ww==} - '@zag-js/checkbox@1.33.1': - resolution: {integrity: sha512-3rIPXB3O7hZukyjKpRAOn+Ob7jByBmDNU7wdpS2HRv7Urv9i5jUExlwayevw/a6JHQaT7mR1dL4culTyX+fJVA==} + '@zag-js/checkbox@1.35.3': + resolution: {integrity: sha512-8XBt/Wg2zSQWqV2ZFqZBQUjYRkOYHA2O3IEi0VVYtds3S1n7Pu/HqkZT5qDw+E/SY2+X9Uyx4hO7h2XrlsiZQQ==} - '@zag-js/clipboard@1.33.1': - resolution: {integrity: sha512-BcuHY3h7fOgR8yX0JHHN/SIAfZOGwrMF1AXKpqeY9Xq2R0lbDMEyXBwT7rQtQUBWCkoSau1e3Nk8ey1yOsWmYw==} + '@zag-js/clipboard@1.35.3': + resolution: {integrity: sha512-obTwynBpp6c17fLHe5tg//FQ497QsyCEry+K3bTdlrivWW200wvfHxZ6RKVbKwDAwhH+ye0bI1xkYAId8j7sdA==} - '@zag-js/collapsible@1.33.1': - resolution: {integrity: sha512-FnEaoIufmYM4kFUET6gusFD7J5cAu/PY78BQ4BqhT3I6sS9FWiu/eHCCsFf/6BqhtqtiCQoki/O5g0arZqOZfw==} + '@zag-js/collapsible@1.35.3': + resolution: {integrity: sha512-IweG8JOBCerJwLO6QzTZGEMlsYUmQfQSeD0jniFguMM8vcunvGVSrM+AaL8pDbmXd+snXokaGyJpGO3vzMW6Fw==} - '@zag-js/collection@1.33.1': - resolution: {integrity: sha512-4Js8oWS0C1zETlQzqJRny63uV/e54R6OerHfJfH9qAzkZuQnhMqZOAA4q6N+5GG6vb8WGB3927jS1A+Zn/pZuQ==} + '@zag-js/collection@1.35.3': + resolution: {integrity: sha512-BYoWJ4b7ma2PgiuQbRSnP603f2DlK6se5JtViUHTamZScLLLWnWHuQ6zFa1KS5kiIkbb7CFM6/bJ3WNYLch8Ig==} - '@zag-js/color-picker@1.33.1': - resolution: {integrity: sha512-PjssCiirvGssPPSoCqeAjK8Brh32K29I2eWck6LAK9IL7FMCpUyXKbSJNjtHeDGK60rzI/xNj8aeQgVmaBJ0Xg==} + '@zag-js/color-picker@1.35.3': + resolution: {integrity: sha512-i9roSgtqeA1b4Q+jWqnxjXB//BQXMP5m1FQ4YcZVq/0yT14A53JIknchuqrh3wC3yPsJMXFqCoKg+NET2+OVig==} - '@zag-js/color-utils@1.33.1': - resolution: {integrity: sha512-YJIBn24IE5LcjKUVK8ndm3VY7ferdlJrl1J02s0uDtBbWywQ4TpufVZQ9aEONeazfCJC4/3etaQCiX9RSpW2uA==} + '@zag-js/color-utils@1.35.3': + resolution: {integrity: sha512-vxkEVgz4YdSbdaPvjiRI1VsJAdwzu/dUNvzqOaiVcPDrHr/FFgmUbv0SOFjnfSb2QWGI8EDEMn02RW9ym+BzGw==} - '@zag-js/combobox@1.33.1': - resolution: {integrity: sha512-9K2i5P+zf6T9Cqa9idzYXvEC/If5gDDbQWYgqflO18ptB0dTvfKkihBsA4/PEig3Ayvj/UGFTlFlbC17M5aACQ==} + '@zag-js/combobox@1.35.3': + resolution: {integrity: sha512-s1qmttTGJTMjlDakL+uvWSEggpafKr1vhOeZCh8j+N4eFt9bLAwaffjuh/1JzWBvzovw7WoMVkizdTXPlN8oYg==} - '@zag-js/core@1.33.1': - resolution: {integrity: sha512-8hnw0/CFTytcYiIRij4Orpni2a79NSiH6Em+58A9AqMJGX8UE1zh6GsLWgrKQPiEiC8Cf3WgNXgCddJKpm8/Yw==} + '@zag-js/core@1.35.3': + resolution: {integrity: sha512-fGAHyqOYSEFmo52t7wI4dvbFfLyJmUlyf7wknsiUlzUHlrn3yv5PAZYZ2TibpOD1hwXIp4AoCjbiIPPZBxirZw==} - '@zag-js/date-picker@1.33.1': - resolution: {integrity: sha512-PfVvttb83DosW9p9BXRAkNsk/duueicd7sEVdOGfgfIs3QJeVn+jvuli8Z2A0oQCok3VCfBwXd+MiwKjyLRpIg==} + '@zag-js/date-picker@1.35.3': + resolution: {integrity: sha512-4G10h6pzzLbd84SE2CKtqi6Z9wEBhSyx4GRSxxy3tsf5wAxnz4anRFat9CGwn2YVUYcUJpD+umYgBMPt6zGDnA==} peerDependencies: '@internationalized/date': '>=3.0.0' - '@zag-js/date-utils@1.33.1': - resolution: {integrity: sha512-hnM/IJ4jBHHCcVNfZyjvAI/0suW6c2XFYwcjM6xoGyG4P1x7YU9H9vuhp8mv7XDj4qqQFS/x8+UEcytZG9wtAg==} + '@zag-js/date-utils@1.35.3': + resolution: {integrity: sha512-1co0FPpZ6nO5dN8sZtECkMYaf+3E5zu0KSIJZpZiXb4TgsZMDyHu7K7IsiKFHk9qmhuF6AdPpNxBju91pSXMFg==} peerDependencies: '@internationalized/date': '>=3.0.0' - '@zag-js/dialog@1.33.1': - resolution: {integrity: sha512-OUjcIby0VSFBULpakDQJL+gtpVR13hvMZDydUm44LF5ygfoe5E7mfp24Q09VGgvbofOZTuwAK5xKTV/AaSX/MQ==} + '@zag-js/dialog@1.35.3': + resolution: {integrity: sha512-byosV+aBHH5LoFKnjEgC7WdqJid7bP9UhgWLSC7+IXbxrif9Czg1YVp6ZlQM6Nx6uD1vnty4touI3P7D7CTKcw==} - '@zag-js/dismissable@1.33.1': - resolution: {integrity: sha512-ZER2LFMTdhQxkIMuT3EMg6vZCjVjttDJJP8g6d7kSARcxN75myUG+H8qZqj9JbH5WSF6Xaf++O+LMUgwzIeixw==} + '@zag-js/dismissable@1.35.3': + resolution: {integrity: sha512-XPk+lqmsZp2Z1yMb5K1yj/e7Sobv4D7zK66B1GS97lk9Xzz8vuSgsimcLy0p7RXQl3KL6H5L69inSuQa2exybQ==} - '@zag-js/dom-query@1.33.1': - resolution: {integrity: sha512-Iyl0D3nLvJuMkkuRy22xhj4pkzexUCDlRpCzqIrOMDKsmFka/WV9PIclZKVpMECTi9dEQmJuGTjBVaCOReLu+Q==} + '@zag-js/dom-query@1.35.3': + resolution: {integrity: sha512-1RbFZoT4CjlHN9TUNse1++ZVOyKo45ktucTIT349o6HMsoWWKmTJDPvFkMBbmu/qY6XXn4dT+LJEp4bL3DR+Qw==} - '@zag-js/editable@1.33.1': - resolution: {integrity: sha512-uLLwopl5naET76ND+/GZDVMlXaAIwepAhmfNA+Esj4Upgtd3lpD5SNzJiVuyzZ0ewVyp2cuXHHAfNiibhkoFlA==} + '@zag-js/drawer@1.35.3': + resolution: {integrity: sha512-DN5bwa7bDCDaUSbNzFxMc2U/WmbLcXvPSQjyOpKI6CC3VbW2kKaOnjJ5qQG+W5YBO0FpmJBtaxRV7lke4sZH2w==} - '@zag-js/file-upload@1.33.1': - resolution: {integrity: sha512-+1jRkJLUZZYVqZJkDOa5bGosFUM6wU6+i12GavbkVgu5QHRc7VEYlPSlX/qmDxrErI9yC/ZWtoVEVFZ8N6DW0g==} + '@zag-js/editable@1.35.3': + resolution: {integrity: sha512-HcjeacS61vQXfNT9IalZj/+oS45yW5bIDO2NjJWV7zNe5AG29NCceUnvBhy+hrUKPnKcjfDocdW5rCL+Lvs/CQ==} - '@zag-js/file-utils@1.33.1': - resolution: {integrity: sha512-x2Vw5JrUElidDSd34x+gydxjkyy3nU6KSr3rSez231MyScj8RtoLCH1BkCLsW86Yc+Mynp8pbHLdjC++AUtKZA==} + '@zag-js/file-upload@1.35.3': + resolution: {integrity: sha512-oIYwnDct4ERo2mfmcxsBIJnlmpzjrzYx82SQsXWD3NGKx3cgdh2lwBX+ebItaLH1jkgzBa3z0TWxc6rfvcUXbw==} - '@zag-js/floating-panel@1.33.1': - resolution: {integrity: sha512-MKtFyC3xxCUmHEnugR+KMcVIX7FdHsoZfDxcKc74h+2M6FAmk6YB8lByoY9pkCR9ems/5DkHcMU9cVVJ9kiFqA==} + '@zag-js/file-utils@1.35.3': + resolution: {integrity: sha512-Tb05RCzx4swc156hd4jLiO7z+Gxg/HQ+JCds03jgTbrFJAz2D56YaMeI7gSDc1m4Xre3nyqQpSo9AeX5nzbE/w==} - '@zag-js/focus-trap@1.33.1': - resolution: {integrity: sha512-aX1YpER7dsegKroNGMnBDfcS14Z9LTdwESSXFDc9C9jFo45qOzfhxmXR+a5rsveMRkvhMFxGffrbpwfvZbRs0A==} + '@zag-js/floating-panel@1.35.3': + resolution: {integrity: sha512-nTZypcS0X46Oo1kpCQTnP5UlzjhypOAj3B4dq2z/3bAOC0TntYTnFkj8PbEJtExk7364xfMyxfgZOiv7Aqq01w==} - '@zag-js/focus-visible@1.33.1': - resolution: {integrity: sha512-xnk2BwO6jYuudj4jMzNYD4AxgaD2sqnLHkwmHImOnVa5frbYziGzevo9iJWC+2THyqQjUXLQ6Zfo6J/Hi3KyNQ==} + '@zag-js/focus-trap@1.35.3': + resolution: {integrity: sha512-evErLlGFdDVCI8xipNS5k0rAvO+KFRA9g273bbfWAL1+mT54mcB/XHa85nC3QpPgMNrSh+6LUNq9fapyOGoyYg==} - '@zag-js/highlight-word@1.33.1': - resolution: {integrity: sha512-row6yPiADeraQFDvoiwuXP0F0qTt7gGnwdeWEcoaqGj27DYZSZKXXK03mQWMo6sdi+VU6z79ZqrlE6bnk6fqWQ==} + '@zag-js/focus-visible@1.35.3': + resolution: {integrity: sha512-g4F8PRGIoFoKBrHiQ1HQh5AjCS7brFRXHvpbDNb9+T11FGlF5Turb+6OVRoNV8MmiuqMltO2I28l36YsGc//uQ==} - '@zag-js/hover-card@1.33.1': - resolution: {integrity: sha512-8f4J0UWqcnEtM5uXtF8a7WbLwo4ornXpHYEPubSLJYFKWsgaPlNtVVX8WNxB9uFFQEB111RfuQSoUrqMlRQ7xw==} + '@zag-js/highlight-word@1.35.3': + resolution: {integrity: sha512-K+mvEBbf3SUFjQeMeJQYb3cjri3x6sPaPhcKWayalelSLB/StWEGqcpmz+a6uUYrCUAK5kEi3Hn0YLGfn0GOig==} - '@zag-js/i18n-utils@1.33.1': - resolution: {integrity: sha512-7frklMwgbD7YjJqxt9nWhFMxFzrqQyPPu+r8u1hEWHwjD9GZPteHIYIyEKKmpYVQqANMpTEoIZi+oUI8YT+OhQ==} + '@zag-js/hover-card@1.35.3': + resolution: {integrity: sha512-xVoKOtvrnzhYzciZ1csgiV76IQ4DRtx1lsJeFSrfg5MH0kYWeC/pcmm3yCd2+Qh/45J7DbSXeZneqxpyiF5Vvw==} - '@zag-js/image-cropper@1.33.1': - resolution: {integrity: sha512-/P+IZapbSvZw7Yudmxll2Pd8/3x6sOebeQW/LghuWUbDi1ilYCjCpsuhlhZrD3NFfiZ+QZfX1+8ofLOiax1g4A==} + '@zag-js/i18n-utils@1.35.3': + resolution: {integrity: sha512-k7UcNxbnC2jvGwCoHYAkFD3ZaRSMQNVHfuy8TujZQ+ci3IJovwgWLveZoRfFbXHkTLfhmbpE2tFXBdpwOVZutg==} - '@zag-js/interact-outside@1.33.1': - resolution: {integrity: sha512-XnqwYsGw0GVmjBpDziwWXKE/+KeZLgRnjEpyVr6HMATMGD+c4j6TmIbI9OGEaWliLuwvHdTclkmK4WYTaAGmiw==} + '@zag-js/image-cropper@1.35.3': + resolution: {integrity: sha512-1PH6bg8JAQESHzNqjka2TJ0QGNBGBAO6rb7AZ+9CaCCLw0pIzbUJhqPMkwd9GhdWGKGP+e7wFitnjcT4W5Js8g==} - '@zag-js/json-tree-utils@1.33.1': - resolution: {integrity: sha512-+t42cJY3QJirlXQHDyZmJMdWVoWlAXGUJ3vuGoUBNoHNq+rAte6i/1+VMq/KkNEh/8QehA/4FdtQAstSMVbAEQ==} + '@zag-js/interact-outside@1.35.3': + resolution: {integrity: sha512-tOcuo/IztzpU7UKXtjVrLZtXzzcbhP4n2WynKwDRkTkq3mRCp61xXJp1csIBycI3JHm/CMeAEcPdRIioxIT/Zw==} - '@zag-js/listbox@1.33.1': - resolution: {integrity: sha512-8XT+6T82xG3BJwC7VYu/I1W8Hxyjgpke8tB1odQSWOV23pVXXPbol7wQbtoieSVeNDsZD8K12CpB40oRVrcSHA==} + '@zag-js/json-tree-utils@1.35.3': + resolution: {integrity: sha512-nOv2dPJf+1mxsobYiSlYt96hR1MK7iHKG1iDLoO5wLggS6GQA3ix1BerHJK0zdehoEZ71R45el5ghCG1HB9VzQ==} - '@zag-js/live-region@1.33.1': - resolution: {integrity: sha512-KbU2wUSMd01fY7dgc9WhvU2x07FxNHKSCrn+fFUnB+Qoy6iiVv0A729JDbzPUUcpBV0BFoQ3qNdBDVyBalbpaQ==} + '@zag-js/listbox@1.35.3': + resolution: {integrity: sha512-FE6FOuBr6aWtOb8U8oDvAvcUzD6JKLXAe8WngiLFG+b2yyW4nlaz2AcKRG1bjjB066UMxMo9/+2p4D0Kf5Id1Q==} - '@zag-js/marquee@1.33.1': - resolution: {integrity: sha512-u5tITcDMZ+L16LKJhIEHzpenxNFosq5BzwUqcF7FD5syEhbA3Jopnq+mWR5CMUaFlbYhRGMSJ1ySNyNwuxU81g==} + '@zag-js/live-region@1.35.3': + resolution: {integrity: sha512-64rWcfggYpyr2Fn4pdrB/lljMgm3quwn9is+vdDN85Vv3WShKWoz08T4njidm0hwcIbzas0bRqQYWDLLsAoSJQ==} - '@zag-js/menu@1.33.1': - resolution: {integrity: sha512-QihwaFCgGcrPbJSoP73nt749/rlUANiIrCU//8WWfQTgv0NBJprBD7d3banDNlK9ZSGmvELcpyQ/fKU4cfn0GQ==} + '@zag-js/marquee@1.35.3': + resolution: {integrity: sha512-bKZVpmAJWPDORP7WOWnS+65W5ZQBQmRs8zvV33ZfCpFbkXjhRiqKSzIj223/VOc2NEDjyWagz2vioAxrFYVzww==} - '@zag-js/navigation-menu@1.33.1': - resolution: {integrity: sha512-QnkK8Q7vEQtj7nc3fpzNLkjmtyxz1WGpwdDqpbiemxT8pZT3BxrSDC3n6795t9xhbOGVWjhyMfDw/3xBT/3JYA==} + '@zag-js/menu@1.35.3': + resolution: {integrity: sha512-KyY0EZXkIU57Mjt+Lg+pupiePk3LcnQcB3Gl05Vva61bNjBjdKV71qwCQru/OxPZEwYgPo46L7TDIb56kfK/VQ==} - '@zag-js/number-input@1.33.1': - resolution: {integrity: sha512-5YKr8uagIDGXp3hIqo4IUBGxS5WhH0xM1CQf2zimfDWvBOng+Y+MH/4Lwu9wKuyIq/J3SJqsjO+2OOF7u6ju/g==} + '@zag-js/navigation-menu@1.35.3': + resolution: {integrity: sha512-8cCHx0X/KjEpr2BaMOxJS5LiA6fs/CNqVTF/sTTgZAv7Dm+MH0yNuKm4kpPvcLaVeBpVE09bnyCHrNKzZes+Fw==} - '@zag-js/pagination@1.33.1': - resolution: {integrity: sha512-TZxxFEgvkz66Y3rX9ug5Vm1CPoN1PgmR9GuW21W7ob9xSWXC9ZQKwTaC1I6qO83dZqBzRK51Q9K1iCghIb3q/w==} + '@zag-js/number-input@1.35.3': + resolution: {integrity: sha512-uqawVybAcLcefVEHMVONuAA5kDSDPP5TsROr5PnAyFlhM1iD85+r3KAfCueoDX5w2X4ibbu9o2tdV6zTFKD/nQ==} - '@zag-js/password-input@1.33.1': - resolution: {integrity: sha512-pJrz50JhQLTfiatehATr40udJYggYmJ7V/7/dBKqthGpMwoaVV3bmtKFSenFGc2mMb5Rlf9KKqHO/dYB7jpNiA==} + '@zag-js/pagination@1.35.3': + resolution: {integrity: sha512-fKm4s5KAd12RiCI/EDmmGKjPQ+i2qS/UsJPdMe65yb/4mY5OibwV2zyHcVeFsOD4gBZpnU6kYlDAGSttmLWLlQ==} - '@zag-js/pin-input@1.33.1': - resolution: {integrity: sha512-q6/DRsIV6ZDKzkFmdzbcsVBm7+I7hMlrsLr/P/jH0/fYE5T9t+1m9ll5j7/5RHFJHQ1WajHpdt5ad5mfXMuxKA==} + '@zag-js/password-input@1.35.3': + resolution: {integrity: sha512-etd0gm6ELAm3y+cFhPU+TYm8khm9cL5Mg5m2DcZxu1Mqpj7JY0LsXZ8SFOdCZgTIHuMEhKBiYfnuyMAd4CJztA==} - '@zag-js/popover@1.33.1': - resolution: {integrity: sha512-layppQOtvKMuJKXlyAA6rW88KfxCilRNS2uZuhJFpPwgASqk5piDdp2G3DA9s0SNTMY8rcNmc197wkDCcGnDew==} + '@zag-js/pin-input@1.35.3': + resolution: {integrity: sha512-ZFt+WIHMdVlSg29BrQLFq5ijabiUO3tXMhoKhjjzTSe/tLqfNeu3UxFB6y/FYpn8+Cvn6xwvhu3lgnORYmI0zQ==} - '@zag-js/popper@1.33.1': - resolution: {integrity: sha512-DNKRh/SRXB2wcvVYK1wvcEufS4vfVXJOv23QUee761bTv4nrPNll5pZFsYEHatiCNkAmO0MRRYA2Sc6jk9nxNA==} + '@zag-js/popover@1.35.3': + resolution: {integrity: sha512-+MIEENPsbKPxzoNuDI/C5d5ZN9uxnfZ+MBDc5C5XSgjjg9FcvMXClNq7IFM1aZi24peRXg9cMNf//lApVRT37w==} - '@zag-js/presence@1.33.1': - resolution: {integrity: sha512-IqrZa+djwkLQiANlp4nS6bq+FOtTYLZOOynJP9zz5+egNtA1qkmCdeBXA5/CgWM83sMmjJEDAe6nmp8darICyQ==} + '@zag-js/popper@1.35.3': + resolution: {integrity: sha512-gpB7Xn9WtlfrUsIVbSgNQGDwgNOL/cSGt0Id3wEQKArmqVC704EWtPvXzOMMybBEdm8YW2hQrXuo+o66abI1Sg==} - '@zag-js/progress@1.33.1': - resolution: {integrity: sha512-Pp4h6ChcIOLKSloBBCOcPy9/C2r3YqrSbrcbY47IjZiDg6JPkivVPqScqM3wH8OpKEEyKyljBottZmbKkjQ3Zg==} + '@zag-js/presence@1.35.3': + resolution: {integrity: sha512-ev5E7+U9IZAGvEaflpdVLHaZl8ZaQMhGB3ypd0yKhPwXeM51obV8w3+5HjzTqHPl8TKuoHWL31YaiUBd5EuS6w==} - '@zag-js/qr-code@1.33.1': - resolution: {integrity: sha512-8Fc/TwlIkLQYfcvXhxCe+rTsmS+cHJpk/WRNMwKO1QvLZw2mBdNIt2pfoGJf8SdufBv5U3KyzCQ4T9iZ1CaYAQ==} + '@zag-js/progress@1.35.3': + resolution: {integrity: sha512-u0GxQN1AfXMAgzYOUMxKQA12DyuAP0svh2S//KvOorTSv7d5hAa8nZXi2cEv5abYsyfKJ6/bc1Z56byzW1jVZw==} - '@zag-js/radio-group@1.33.1': - resolution: {integrity: sha512-W/T8Hea3Z4mWCErm2fJc/EYabxRkKHFJStSClyllqknF3Y+b42MaKGuub1IcACO3pe6csLTkomdxy1qDLWl/dg==} + '@zag-js/qr-code@1.35.3': + resolution: {integrity: sha512-t0Ehwogr49vTNtWyNdQU2tYex7uJyfAn7N/5LgD7FXw8aa+RBMWZWlqjCUvHqJ929tVMrn+LIrQnZCcwNunalA==} - '@zag-js/rating-group@1.33.1': - resolution: {integrity: sha512-Bb6mv8GE9OpMA+tEwEuR1DOqP9P9ovkeyDaehfDy/hBDT90kCjl2RJ4aCsJINX5k2E+/AD2uv36HcSClqZKiYg==} + '@zag-js/radio-group@1.35.3': + resolution: {integrity: sha512-kOzocjqWk3dXuRfyfsHwfw63Z99NHbc7rvVUutSsfXANXi+DFYZHuqdPUwMt+29LfaL15XTOfuGV+yUXDCgQHQ==} - '@zag-js/react@1.33.1': - resolution: {integrity: sha512-TZ66zU99ixsPMWTKaGOF5u4sM9Ki25ZwuGbZXkz8K6mM28UZAt5o+bro6030XI2VLkP0W+VI9cHUFn6AXJPsHw==} + '@zag-js/rating-group@1.35.3': + resolution: {integrity: sha512-BmhJZdbaTnd3nFWMY+nR+HF952UhWXfaXXxiBWptSLMBfAYImQTWBMrLgTHCSnVfmFATj4Gb7xQe79FQU8T5fA==} + + '@zag-js/react@1.35.3': + resolution: {integrity: sha512-x2PxYUCQ6OgOpUdmSkG5tbL9JWVqYRh42r4V2UeAdMh0MRwjAJtxjvAy50DZ8Sfia5o4UGdZMXJyDY2O7Pdhyw==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' - '@zag-js/rect-utils@1.33.1': - resolution: {integrity: sha512-vCIgZF/z8oeYfUhGUgRiNEfOS8on4rUXi4vtL4IvHSdAv5VxZw4ODoLhIzRGT3BwsiMfr8qJ8fmrcR2oFRFQgA==} + '@zag-js/rect-utils@1.35.3': + resolution: {integrity: sha512-mt/oD3RXdyaX6ZPSd8BO13vvPBJ7QpVWieubE3O0WM3OPhU7ykDMRp/tR7cYMQrzUm04GlY9pbkmSSw2uABxlA==} - '@zag-js/remove-scroll@1.33.1': - resolution: {integrity: sha512-5+Mvboqlmv8EdJoixAbGrftFVWZTznsVJn40BuB/6fYQeqdsZ2vFmSmSIr7btFOPcj3BcTMo0SbWNNta3fAOrg==} + '@zag-js/remove-scroll@1.35.3': + resolution: {integrity: sha512-e59z9SbEpPiw0qwNQa2cB5/h30ZCLREaHsCw1TKTANFhwg7v85k9Lq1H/G/49li1CAjmiaOU9BNGlDvbzpNETQ==} - '@zag-js/scroll-area@1.33.1': - resolution: {integrity: sha512-jJIDViQ3W1NCLNdB/Q4jfL/MnTG0BF5bEHGW5YxaigHMSXs41EVXT/aaNNwQZVlnR48NfHc9S8U9c/4fvIt3EQ==} + '@zag-js/scroll-area@1.35.3': + resolution: {integrity: sha512-IQwdUws/AckRIHK1z/wHdHurnOeGd8h8Dmspfh3VT7NkwTnxeJ4SW9di9smuD+d25eXkJRuX5zGEDHAyx2IaPQ==} - '@zag-js/scroll-snap@1.33.1': - resolution: {integrity: sha512-GLEb+YJj800ia2zyTFxVZomQ1cFSShazUQ/1uAxX0Lj7+aZK88cZhIn7AI0+yBXTPBS0zrZDhBPsGEDQX+Q9Fw==} + '@zag-js/scroll-snap@1.35.3': + resolution: {integrity: sha512-NVa2yRm2DQnF6hTV9k7Xz7l8YCZBagZTiqSwNvWKUulKD1csjt2fpBxvUt2cK+1iQnLOey2ydhs7MMsAnXPbJA==} - '@zag-js/select@1.33.1': - resolution: {integrity: sha512-eG+Ftdse0zvCAkXBMNZVBlM+KNvFRKHToxlxgid6wOd5QgRGwr4HaJuWaz908nBIZRYMFVvC+lLaygUVORHmGg==} + '@zag-js/select@1.35.3': + resolution: {integrity: sha512-ztszGHWvlbBDE0YT5LYPH+sMd6VH1ct5pH/M9VSzIUO6C5PARkW0NwSVQ1rCQJMj4sfvSE1gC1/r7urRzqEcUQ==} - '@zag-js/signature-pad@1.33.1': - resolution: {integrity: sha512-bnTuG28F1A5Kdt+tsveBgNFhRG71vBBIoW8xVW+udph+9XhWfxsLC2j/O6QlnPgYEjOPUlG6/4wNT4LHzLQYUQ==} + '@zag-js/signature-pad@1.35.3': + resolution: {integrity: sha512-jvtxxzAQ8fre11zWUh6HflG4Ycr5z83Wba4pONRJbUE/vNgkJQ7yJgfyUl1QTlkn8Arfg2Zwoxu9GIq80HLZWg==} - '@zag-js/slider@1.33.1': - resolution: {integrity: sha512-tGbBiSHBXRa5y462QXVQ0YrluwlHsSCVdsInJAkQGkgBGZgikMPvYIHffmno1HVWYZlC/1hvRx7wq+PSfV/vXQ==} + '@zag-js/slider@1.35.3': + resolution: {integrity: sha512-Th142JO4Fqla5AWhGrTW6CQicwvTw87PdVpur/WotQ7brlZIww5HipzEMh5eQJSWfwpKD4PI2bYK9V/ZE/mpXA==} - '@zag-js/splitter@1.33.1': - resolution: {integrity: sha512-22mwXecfaflGoPivPj4+v2QwI9jdD5pMAgWO0CJUwDE397LtPShn8h8NHd6yTycg/Km25DyIy8wXQpX8oYtxPQ==} + '@zag-js/splitter@1.35.3': + resolution: {integrity: sha512-IsIbRwzjr5amGANEDsZDSToaSn8wHUWvS2l0XHmf3BiiguVApaZgQTlfqthVQC9hBHMOaGIXIW1CFUOrQYkvUQ==} - '@zag-js/steps@1.33.1': - resolution: {integrity: sha512-Plo/TRi7lZFngFlJxJrqT4CSYQqdJExVSKa17RXe1lpKHjHBD7D1jHbuekUuPhurV0SS8vaU9iYTcuF1p0T39g==} + '@zag-js/steps@1.35.3': + resolution: {integrity: sha512-TYIrqV+v9/ULhvrTRBtQFFvJQPPTWOmjFXxlIxDwozek5R4dCIyeUYt1/ChJEc2mNETocbfDVSTxRO1dwCFpwQ==} - '@zag-js/store@1.33.1': - resolution: {integrity: sha512-FYkrR9IskD5wyKjYUAHWwdGf/C3FmnactfHR9/6dm9YzNO/+jtWxYsFnHQB8dUm9/6VxAZHofw3FbuyPRJ/x3g==} + '@zag-js/store@1.35.3': + resolution: {integrity: sha512-7kEV4T/20DU36UIfVMzuDlLhWSSEy/vabmpiB700tcdD9BBBODTiSg3ZeljW17dQbvE545vZOFEjVf/cQ5LVGA==} - '@zag-js/switch@1.33.1': - resolution: {integrity: sha512-2jl/R4CKLYvk+4cmSYFo3D2gQ+1ts9H7Y4yH98o9rXgPMvdEM9KMKX1FTqJRIY7v6ZkcNbvV/vKP3bDvMdTpug==} + '@zag-js/switch@1.35.3': + resolution: {integrity: sha512-EP/2cJ46sd+6C5x5+89jn/9NOpM05CRESYB4RMhOnTe/WFtcS4IpiYtVHFhikdXkvJoibm67O2EHep2Pm/Xj4w==} - '@zag-js/tabs@1.33.1': - resolution: {integrity: sha512-Xquhso7jUch9UrG5N+5vNfR8S2bWUk6EDpBBArY0X5oPSnlzgwJcjWh98hH1QyHX3JmWZN4kAfVKUxNdQxRnVw==} + '@zag-js/tabs@1.35.3': + resolution: {integrity: sha512-lZKlDmxE25miCikj9QZCCnL02SVV2K14KZy5bn7+XDgrWlfSNTpNTj8r5E3zGlSgio5pkTGou57ASqS7WaPDWg==} - '@zag-js/tags-input@1.33.1': - resolution: {integrity: sha512-PRRZlVBETX72e8GLg431A/CPr0Vf2dbGAq1ES8Z+3ltQurDCQaq6FQWgSXgNr3Iy+S2h+eSwKPIV7PMpjl1MCg==} + '@zag-js/tags-input@1.35.3': + resolution: {integrity: sha512-HqyoQ3DZFhByOGnDShFfxi6u0bIf7aSVTlwmAvcL+b2ZhyU6/wIMGc4WJE7BMx1NYWM/jNLHedvGExAI8R0kXQ==} - '@zag-js/timer@1.33.1': - resolution: {integrity: sha512-GgqntefAEQbf66aNgA6NL9Rtrrxcd0/IJVddTj1/xihCnJ8u6AOU4syG5tie0Tpc2caDAntOwlYjpEy3n2AGcA==} + '@zag-js/timer@1.35.3': + resolution: {integrity: sha512-edmgitbRgsq+msxvVB4wc17Q5d5k63zMWaLJnWjUdDGAgEtM6/HNxwGb3riv46S2U3RgYxaaHTNZ/M7EE5mvYw==} - '@zag-js/toast@1.33.1': - resolution: {integrity: sha512-kI2/VJcBQGgHpmuWiIDqPn8ejFEODh5YhjWbnvjGRG+x3XoPuMq6hhxXV6VWJslbZJtTmzxDcP+Xamdrf1hbZA==} + '@zag-js/toast@1.35.3': + resolution: {integrity: sha512-whlR791GHdnMD21nNPsl2Dbql8+qu1wBZl75QzwYrjR8FlKjp8bhr3gXKzQEddcBXe9GPEFGvUs4iCyXsuTbpg==} - '@zag-js/toggle-group@1.33.1': - resolution: {integrity: sha512-KZaMFN5u26d8elAcdu6LDC7byltpzeoemXHMMa7H/1upS3/98ESKUzx1VlA5SSTAinU4t9+rXoR3VTtP2RJbTw==} + '@zag-js/toggle-group@1.35.3': + resolution: {integrity: sha512-Gn6JHzkQ4tlttjZcE0ZjIdxYkFeVp9VHrcMVizjJTkGZRmQ+kPZ5G/wOsZhIrvLX3Dw6Y0NkuBcP+jDHz/o3TA==} - '@zag-js/toggle@1.33.1': - resolution: {integrity: sha512-bmHNxuW3GVclvFTqcuLJYbEuqs6v3Sf0d2b3daOvGMZL1FwyL0zEAdo5Pui2hthe7QTaH7MJQIF8yPQ4vhLprg==} + '@zag-js/toggle@1.35.3': + resolution: {integrity: sha512-aFfHKuR4sKzglhkmWLA+0RTNPs9dfeqwtc96qljawGYfAYWJXkEPYK9dFfVa+arZ7L84xBi24QSLiTg7LGSFLw==} - '@zag-js/tooltip@1.33.1': - resolution: {integrity: sha512-2CmOMp8qvdTYLE1kgZKnE5RiObzpjJcfVdYYRgVqyIli20AAsOxyahE7WlgLwUGjqpzezah+Z20ZOir6x4jsnQ==} + '@zag-js/tooltip@1.35.3': + resolution: {integrity: sha512-/pImDGYl79MfLdvEphj3rSvNdj2tLW4GwGEncgdLM/GKwQiEUjfi/9EJOfLYP23M4lOOnoW7orehJ9xeaXOAkA==} - '@zag-js/tour@1.33.1': - resolution: {integrity: sha512-eRZD4nePguquNkyrlMzpJr7XxXTVTm3Rxw0p5n1qwQYp3urCYIwupZcWXei1OtiYXenqIdbYMBfNtQRev0x1Ig==} + '@zag-js/tour@1.35.3': + resolution: {integrity: sha512-DI2aCXmZaE9KcPZDs9itc2BO7ixLApJ/yVRfM69pXwVOrucdSeDDNPFkfbhj5XwB+9VjjZEkqWFHKntRIyPl5g==} - '@zag-js/tree-view@1.33.1': - resolution: {integrity: sha512-5SiwSGdcqiGoCQl46pvEAgGkM5gTsPpLLPXB2Eqfojm2fm2oev73+1gWsZt1/sX/qsIQ1hH3a2h44rXW1W2IWg==} + '@zag-js/tree-view@1.35.3': + resolution: {integrity: sha512-DbHaLxSNa1goE3o3IsXxEdzp8P5dvmkk1rVWgNUUIhpA+44idEjSSNXJkHPl18Mk5blqSMVjK1EX91oqai01Vw==} - '@zag-js/types@1.33.1': - resolution: {integrity: sha512-huJdwaeyptKDuZqhhFQRWNiMAJEdei4fTAQ3xIBw07GW27zKwust4Bn0y+8PYlnVVQn2auH4lpIXXwPccFRclQ==} + '@zag-js/types@1.35.3': + resolution: {integrity: sha512-Fnm3AMs1lfb55hlkip/eJeWHOjFB3gSi1JkZlkkdltG2l7y/zsHkumPSe6jIKy+DRRIFKRCyXVTatbPN27bO3w==} - '@zag-js/utils@1.33.1': - resolution: {integrity: sha512-N73enDcveuto5BdYd15m7bu08vd+Re//eufgzGyKPWuzFowEFV77si1v9zZjmK9eXVMTFyde/TPal3aHv4VEJg==} + '@zag-js/utils@1.35.3': + resolution: {integrity: sha512-LHcC+9y6TFhDsIz9I3koYxONl2JFfx5yQDzc6ZEQO2cqzXedRcN0R9IPqNGCX7JuhGt14ctDkVCm1JWGP2J6Wg==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1947,8 +1950,8 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - happy-dom@20.7.0: - resolution: {integrity: sha512-hR/uLYQdngTyEfxnOoa+e6KTcfBFyc1hgFj/Cc144A5JJUuHFYqIEBDcD4FeGqUeKLRZqJ9eN9u7/GDjYEgS1g==} + happy-dom@20.8.3: + resolution: {integrity: sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ==} engines: {node: '>=20.0.0'} has-bigints@1.1.0: @@ -2719,8 +2722,8 @@ packages: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} - tar@7.5.10: - resolution: {integrity: sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==} + tar@7.5.11: + resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==} engines: {node: '>=18'} tinybench@2.9.0: @@ -2992,72 +2995,73 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.1.1 - '@ark-ui/react@5.31.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@internationalized/date': 3.10.0 - '@zag-js/accordion': 1.33.1 - '@zag-js/anatomy': 1.33.1 - '@zag-js/angle-slider': 1.33.1 - '@zag-js/async-list': 1.33.1 - '@zag-js/auto-resize': 1.33.1 - '@zag-js/avatar': 1.33.1 - '@zag-js/bottom-sheet': 1.33.1 - '@zag-js/carousel': 1.33.1 - '@zag-js/checkbox': 1.33.1 - '@zag-js/clipboard': 1.33.1 - '@zag-js/collapsible': 1.33.1 - '@zag-js/collection': 1.33.1 - '@zag-js/color-picker': 1.33.1 - '@zag-js/color-utils': 1.33.1 - '@zag-js/combobox': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/date-picker': 1.33.1(@internationalized/date@3.10.0) - '@zag-js/date-utils': 1.33.1(@internationalized/date@3.10.0) - '@zag-js/dialog': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/editable': 1.33.1 - '@zag-js/file-upload': 1.33.1 - '@zag-js/file-utils': 1.33.1 - '@zag-js/floating-panel': 1.33.1 - '@zag-js/focus-trap': 1.33.1 - '@zag-js/highlight-word': 1.33.1 - '@zag-js/hover-card': 1.33.1 - '@zag-js/i18n-utils': 1.33.1 - '@zag-js/image-cropper': 1.33.1 - '@zag-js/json-tree-utils': 1.33.1 - '@zag-js/listbox': 1.33.1 - '@zag-js/marquee': 1.33.1 - '@zag-js/menu': 1.33.1 - '@zag-js/navigation-menu': 1.33.1 - '@zag-js/number-input': 1.33.1 - '@zag-js/pagination': 1.33.1 - '@zag-js/password-input': 1.33.1 - '@zag-js/pin-input': 1.33.1 - '@zag-js/popover': 1.33.1 - '@zag-js/presence': 1.33.1 - '@zag-js/progress': 1.33.1 - '@zag-js/qr-code': 1.33.1 - '@zag-js/radio-group': 1.33.1 - '@zag-js/rating-group': 1.33.1 - '@zag-js/react': 1.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@zag-js/scroll-area': 1.33.1 - '@zag-js/select': 1.33.1 - '@zag-js/signature-pad': 1.33.1 - '@zag-js/slider': 1.33.1 - '@zag-js/splitter': 1.33.1 - '@zag-js/steps': 1.33.1 - '@zag-js/switch': 1.33.1 - '@zag-js/tabs': 1.33.1 - '@zag-js/tags-input': 1.33.1 - '@zag-js/timer': 1.33.1 - '@zag-js/toast': 1.33.1 - '@zag-js/toggle': 1.33.1 - '@zag-js/toggle-group': 1.33.1 - '@zag-js/tooltip': 1.33.1 - '@zag-js/tour': 1.33.1 - '@zag-js/tree-view': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@ark-ui/react@5.34.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@internationalized/date': 3.11.0 + '@zag-js/accordion': 1.35.3 + '@zag-js/anatomy': 1.35.3 + '@zag-js/angle-slider': 1.35.3 + '@zag-js/async-list': 1.35.3 + '@zag-js/auto-resize': 1.35.3 + '@zag-js/avatar': 1.35.3 + '@zag-js/carousel': 1.35.3 + '@zag-js/cascade-select': 1.35.3 + '@zag-js/checkbox': 1.35.3 + '@zag-js/clipboard': 1.35.3 + '@zag-js/collapsible': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/color-picker': 1.35.3 + '@zag-js/color-utils': 1.35.3 + '@zag-js/combobox': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/date-picker': 1.35.3(@internationalized/date@3.11.0) + '@zag-js/date-utils': 1.35.3(@internationalized/date@3.11.0) + '@zag-js/dialog': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/drawer': 1.35.3 + '@zag-js/editable': 1.35.3 + '@zag-js/file-upload': 1.35.3 + '@zag-js/file-utils': 1.35.3 + '@zag-js/floating-panel': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/highlight-word': 1.35.3 + '@zag-js/hover-card': 1.35.3 + '@zag-js/i18n-utils': 1.35.3 + '@zag-js/image-cropper': 1.35.3 + '@zag-js/json-tree-utils': 1.35.3 + '@zag-js/listbox': 1.35.3 + '@zag-js/marquee': 1.35.3 + '@zag-js/menu': 1.35.3 + '@zag-js/navigation-menu': 1.35.3 + '@zag-js/number-input': 1.35.3 + '@zag-js/pagination': 1.35.3 + '@zag-js/password-input': 1.35.3 + '@zag-js/pin-input': 1.35.3 + '@zag-js/popover': 1.35.3 + '@zag-js/presence': 1.35.3 + '@zag-js/progress': 1.35.3 + '@zag-js/qr-code': 1.35.3 + '@zag-js/radio-group': 1.35.3 + '@zag-js/rating-group': 1.35.3 + '@zag-js/react': 1.35.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@zag-js/scroll-area': 1.35.3 + '@zag-js/select': 1.35.3 + '@zag-js/signature-pad': 1.35.3 + '@zag-js/slider': 1.35.3 + '@zag-js/splitter': 1.35.3 + '@zag-js/steps': 1.35.3 + '@zag-js/switch': 1.35.3 + '@zag-js/tabs': 1.35.3 + '@zag-js/tags-input': 1.35.3 + '@zag-js/timer': 1.35.3 + '@zag-js/toast': 1.35.3 + '@zag-js/toggle': 1.35.3 + '@zag-js/toggle-group': 1.35.3 + '@zag-js/tooltip': 1.35.3 + '@zag-js/tour': 1.35.3 + '@zag-js/tree-view': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -3223,15 +3227,15 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@chakra-ui/react@3.33.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@chakra-ui/react@3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@ark-ui/react': 5.31.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@ark-ui/react': 5.34.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@emotion/is-prop-valid': 1.4.0 '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.4) '@emotion/utils': 1.4.2 - '@pandacss/is-valid-prop': 1.8.2 + '@pandacss/is-valid-prop': 1.9.0 csstype: 3.2.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -3422,25 +3426,25 @@ snapshots: '@eslint/core': 1.1.0 levn: 0.4.1 - '@floating-ui/core@1.7.4': + '@floating-ui/core@1.7.5': dependencies: - '@floating-ui/utils': 0.2.10 + '@floating-ui/utils': 0.2.11 - '@floating-ui/dom@1.7.5': + '@floating-ui/dom@1.7.6': dependencies: - '@floating-ui/core': 1.7.4 - '@floating-ui/utils': 0.2.10 + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 - '@floating-ui/utils@0.2.10': {} + '@floating-ui/utils@0.2.11': {} - '@hey-api/client-axios@0.9.1(@hey-api/openapi-ts@0.93.1(magicast@0.3.5)(typescript@5.9.3))(axios@1.13.6)': + '@hey-api/client-axios@0.9.1(@hey-api/openapi-ts@0.94.0(magicast@0.3.5)(typescript@5.9.3))(axios@1.13.6)': dependencies: - '@hey-api/openapi-ts': 0.93.1(magicast@0.3.5)(typescript@5.9.3) + '@hey-api/openapi-ts': 0.94.0(magicast@0.3.5)(typescript@5.9.3) axios: 1.13.6 '@hey-api/client-fetch@0.4.0': {} - '@hey-api/codegen-core@0.7.0(magicast@0.3.5)(typescript@5.9.3)': + '@hey-api/codegen-core@0.7.1(magicast@0.3.5)(typescript@5.9.3)': dependencies: '@hey-api/types': 0.1.3(typescript@5.9.3) ansi-colors: 4.1.3 @@ -3466,11 +3470,11 @@ snapshots: transitivePeerDependencies: - magicast - '@hey-api/openapi-ts@0.93.1(magicast@0.3.5)(typescript@5.9.3)': + '@hey-api/openapi-ts@0.94.0(magicast@0.3.5)(typescript@5.9.3)': dependencies: - '@hey-api/codegen-core': 0.7.0(magicast@0.3.5)(typescript@5.9.3) + '@hey-api/codegen-core': 0.7.1(magicast@0.3.5)(typescript@5.9.3) '@hey-api/json-schema-ref-parser': 1.3.1 - '@hey-api/shared': 0.2.1(magicast@0.3.5)(typescript@5.9.3) + '@hey-api/shared': 0.2.2(magicast@0.3.5)(typescript@5.9.3) '@hey-api/types': 0.1.3(typescript@5.9.3) ansi-colors: 4.1.3 color-support: 1.1.3 @@ -3479,9 +3483,9 @@ snapshots: transitivePeerDependencies: - magicast - '@hey-api/shared@0.2.1(magicast@0.3.5)(typescript@5.9.3)': + '@hey-api/shared@0.2.2(magicast@0.3.5)(typescript@5.9.3)': dependencies: - '@hey-api/codegen-core': 0.7.0(magicast@0.3.5)(typescript@5.9.3) + '@hey-api/codegen-core': 0.7.1(magicast@0.3.5)(typescript@5.9.3) '@hey-api/json-schema-ref-parser': 1.3.1 '@hey-api/types': 0.1.3(typescript@5.9.3) ansi-colors: 4.1.3 @@ -3507,13 +3511,13 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@internationalized/date@3.10.0': + '@internationalized/date@3.11.0': dependencies: - '@swc/helpers': 0.5.18 + '@swc/helpers': 0.5.19 '@internationalized/number@3.6.5': dependencies: - '@swc/helpers': 0.5.18 + '@swc/helpers': 0.5.19 '@isaacs/fs-minipass@4.0.1': dependencies: @@ -3540,7 +3544,7 @@ snapshots: '@jsdevtools/ono@7.1.3': {} - '@pandacss/is-valid-prop@1.8.2': {} + '@pandacss/is-valid-prop@1.9.0': {} '@pkgr/core@0.2.9': {} @@ -3663,7 +3667,7 @@ snapshots: '@swc/core-win32-x64-msvc@1.15.11': optional: true - '@swc/core@1.15.11(@swc/helpers@0.5.18)': + '@swc/core@1.15.11(@swc/helpers@0.5.19)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 @@ -3678,11 +3682,11 @@ snapshots: '@swc/core-win32-arm64-msvc': 1.15.11 '@swc/core-win32-ia32-msvc': 1.15.11 '@swc/core-win32-x64-msvc': 1.15.11 - '@swc/helpers': 0.5.18 + '@swc/helpers': 0.5.19 '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.18': + '@swc/helpers@0.5.19': dependencies: tslib: 2.8.1 @@ -3767,7 +3771,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@25.3.0': + '@types/node@25.3.5': dependencies: undici-types: 7.18.2 @@ -3785,7 +3789,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.3.0 + '@types/node': 25.3.5 '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: @@ -3878,15 +3882,15 @@ snapshots: '@typescript-eslint/types': 8.56.1 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react-swc@4.2.3(@swc/helpers@0.5.18)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1))': + '@vitejs/plugin-react-swc@4.2.3(@swc/helpers@0.5.19)(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - '@swc/core': 1.15.11(@swc/helpers@0.5.18) - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1) + '@swc/core': 1.15.11(@swc/helpers@0.5.19) + vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1) transitivePeerDependencies: - '@swc/helpers' - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1))': + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -3898,7 +3902,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1) + vitest: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1) '@vitest/expect@4.0.18': dependencies: @@ -3909,13 +3913,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1) + vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1) '@vitest/pretty-format@4.0.18': dependencies: @@ -3939,545 +3943,561 @@ snapshots: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 - '@zag-js/accordion@1.33.1': + '@zag-js/accordion@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/anatomy@1.35.3': {} - '@zag-js/anatomy@1.33.1': {} + '@zag-js/angle-slider@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/angle-slider@1.33.1': + '@zag-js/aria-hidden@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/rect-utils': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/dom-query': 1.35.3 - '@zag-js/aria-hidden@1.33.1': + '@zag-js/async-list@1.35.3': dependencies: - '@zag-js/dom-query': 1.33.1 + '@zag-js/core': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/async-list@1.33.1': + '@zag-js/auto-resize@1.35.3': dependencies: - '@zag-js/core': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/dom-query': 1.35.3 - '@zag-js/auto-resize@1.33.1': + '@zag-js/avatar@1.35.3': dependencies: - '@zag-js/dom-query': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/avatar@1.33.1': + '@zag-js/carousel@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/scroll-snap': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/bottom-sheet@1.33.1': + '@zag-js/cascade-select@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/aria-hidden': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dismissable': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/focus-trap': 1.33.1 - '@zag-js/remove-scroll': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/carousel@1.33.1': + '@zag-js/checkbox@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/scroll-snap': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/checkbox@1.33.1': + '@zag-js/clipboard@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/focus-visible': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/clipboard@1.33.1': + '@zag-js/collapsible@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/collapsible@1.33.1': + '@zag-js/collection@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/utils': 1.35.3 - '@zag-js/collection@1.33.1': + '@zag-js/color-picker@1.35.3': dependencies: - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/color-utils': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/color-picker@1.33.1': + '@zag-js/color-utils@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/color-utils': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dismissable': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/popper': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/utils': 1.35.3 - '@zag-js/color-utils@1.33.1': + '@zag-js/combobox@1.35.3': dependencies: - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/combobox@1.33.1': + '@zag-js/core@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/aria-hidden': 1.33.1 - '@zag-js/collection': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dismissable': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/popper': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/core@1.33.1': + '@zag-js/date-picker@1.35.3(@internationalized/date@3.11.0)': dependencies: - '@zag-js/dom-query': 1.33.1 - '@zag-js/utils': 1.33.1 + '@internationalized/date': 3.11.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/date-utils': 1.35.3(@internationalized/date@3.11.0) + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/live-region': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/date-picker@1.33.1(@internationalized/date@3.10.0)': + '@zag-js/date-utils@1.35.3(@internationalized/date@3.11.0)': dependencies: - '@internationalized/date': 3.10.0 - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/date-utils': 1.33.1(@internationalized/date@3.10.0) - '@zag-js/dismissable': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/live-region': 1.33.1 - '@zag-js/popper': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@internationalized/date': 3.11.0 - '@zag-js/date-utils@1.33.1(@internationalized/date@3.10.0)': + '@zag-js/dialog@1.35.3': dependencies: - '@internationalized/date': 3.10.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/remove-scroll': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/dialog@1.33.1': + '@zag-js/dismissable@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/aria-hidden': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dismissable': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/focus-trap': 1.33.1 - '@zag-js/remove-scroll': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/dom-query': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/dismissable@1.33.1': + '@zag-js/dom-query@1.35.3': dependencies: - '@zag-js/dom-query': 1.33.1 - '@zag-js/interact-outside': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/types': 1.35.3 - '@zag-js/dom-query@1.33.1': + '@zag-js/drawer@1.35.3': dependencies: - '@zag-js/types': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/remove-scroll': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/editable@1.33.1': + '@zag-js/editable@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/interact-outside': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/file-upload@1.33.1': + '@zag-js/file-upload@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/file-utils': 1.33.1 - '@zag-js/i18n-utils': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/file-utils': 1.35.3 + '@zag-js/i18n-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/file-utils@1.33.1': + '@zag-js/file-utils@1.35.3': dependencies: - '@zag-js/i18n-utils': 1.33.1 + '@zag-js/i18n-utils': 1.35.3 - '@zag-js/floating-panel@1.33.1': + '@zag-js/floating-panel@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/popper': 1.33.1 - '@zag-js/rect-utils': 1.33.1 - '@zag-js/store': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/store': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/focus-trap@1.33.1': + '@zag-js/focus-trap@1.35.3': dependencies: - '@zag-js/dom-query': 1.33.1 + '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-visible@1.33.1': + '@zag-js/focus-visible@1.35.3': dependencies: - '@zag-js/dom-query': 1.33.1 + '@zag-js/dom-query': 1.35.3 - '@zag-js/highlight-word@1.33.1': {} + '@zag-js/highlight-word@1.35.3': {} - '@zag-js/hover-card@1.33.1': + '@zag-js/hover-card@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dismissable': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/popper': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/i18n-utils@1.33.1': + '@zag-js/i18n-utils@1.35.3': dependencies: - '@zag-js/dom-query': 1.33.1 + '@zag-js/dom-query': 1.35.3 - '@zag-js/image-cropper@1.33.1': + '@zag-js/image-cropper@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/interact-outside@1.33.1': + '@zag-js/interact-outside@1.35.3': dependencies: - '@zag-js/dom-query': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/json-tree-utils@1.33.1': {} + '@zag-js/json-tree-utils@1.35.3': {} - '@zag-js/listbox@1.33.1': + '@zag-js/listbox@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/collection': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/focus-visible': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/live-region@1.33.1': {} + '@zag-js/live-region@1.35.3': {} - '@zag-js/marquee@1.33.1': + '@zag-js/marquee@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/menu@1.33.1': + '@zag-js/menu@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dismissable': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/popper': 1.33.1 - '@zag-js/rect-utils': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/navigation-menu@1.33.1': + '@zag-js/navigation-menu@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dismissable': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/number-input@1.33.1': + '@zag-js/number-input@1.35.3': dependencies: '@internationalized/number': 3.6.5 - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/pagination@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/password-input@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/pin-input@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/popover@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/aria-hidden': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dismissable': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/focus-trap': 1.33.1 - '@zag-js/popper': 1.33.1 - '@zag-js/remove-scroll': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/popper@1.33.1': - dependencies: - '@floating-ui/dom': 1.7.5 - '@zag-js/dom-query': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/presence@1.33.1': - dependencies: - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - - '@zag-js/progress@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/qr-code@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/pagination@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/password-input@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/pin-input@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/popover@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/remove-scroll': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/popper@1.35.3': + dependencies: + '@floating-ui/dom': 1.7.6 + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/presence@1.35.3': + dependencies: + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + + '@zag-js/progress@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/qr-code@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 proxy-memoize: 3.0.1 uqr: 0.1.2 - '@zag-js/radio-group@1.33.1': + '@zag-js/radio-group@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/focus-visible': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/rating-group@1.33.1': + '@zag-js/rating-group@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/react@1.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@zag-js/react@1.35.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@zag-js/core': 1.33.1 - '@zag-js/store': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/core': 1.35.3 + '@zag-js/store': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@zag-js/rect-utils@1.33.1': {} + '@zag-js/rect-utils@1.35.3': {} - '@zag-js/remove-scroll@1.33.1': + '@zag-js/remove-scroll@1.35.3': dependencies: - '@zag-js/dom-query': 1.33.1 + '@zag-js/dom-query': 1.35.3 - '@zag-js/scroll-area@1.33.1': + '@zag-js/scroll-area@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/scroll-snap@1.33.1': + '@zag-js/scroll-snap@1.35.3': dependencies: - '@zag-js/dom-query': 1.33.1 + '@zag-js/dom-query': 1.35.3 - '@zag-js/select@1.33.1': + '@zag-js/select@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/collection': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dismissable': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/popper': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/signature-pad@1.33.1': + '@zag-js/signature-pad@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 perfect-freehand: 1.2.3 - '@zag-js/slider@1.33.1': + '@zag-js/slider@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/splitter@1.33.1': + '@zag-js/splitter@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/steps@1.33.1': + '@zag-js/steps@1.35.3': dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/store@1.33.1': + '@zag-js/store@1.35.3': dependencies: proxy-compare: 3.0.1 - '@zag-js/switch@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/focus-visible': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/tabs@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/tags-input@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/auto-resize': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/interact-outside': 1.33.1 - '@zag-js/live-region': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/timer@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/toast@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dismissable': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/toggle-group@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/toggle@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/tooltip@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/focus-visible': 1.33.1 - '@zag-js/popper': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/tour@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dismissable': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/focus-trap': 1.33.1 - '@zag-js/interact-outside': 1.33.1 - '@zag-js/popper': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/tree-view@1.33.1': - dependencies: - '@zag-js/anatomy': 1.33.1 - '@zag-js/collection': 1.33.1 - '@zag-js/core': 1.33.1 - '@zag-js/dom-query': 1.33.1 - '@zag-js/types': 1.33.1 - '@zag-js/utils': 1.33.1 - - '@zag-js/types@1.33.1': + '@zag-js/switch@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tabs@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tags-input@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/auto-resize': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/live-region': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/timer@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/toast@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/toggle-group@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/toggle@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tooltip@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tour@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tree-view@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/types@1.35.3': dependencies: csstype: 3.2.3 - '@zag-js/utils@1.33.1': {} + '@zag-js/utils@1.35.3': {} acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -5257,7 +5277,7 @@ snapshots: node-fetch-native: 1.6.7 nypm: 0.5.4 pathe: 2.0.3 - tar: 7.5.10 + tar: 7.5.11 giget@2.0.0: dependencies: @@ -5290,9 +5310,9 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - happy-dom@20.7.0: + happy-dom@20.8.3: dependencies: - '@types/node': 25.3.0 + '@types/node': 25.3.5 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 @@ -6109,7 +6129,7 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 - tar@7.5.10: + tar@7.5.11: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -6221,11 +6241,11 @@ snapshots: dependencies: punycode: 2.3.1 - vite-plugin-css-injected-by-js@4.0.1(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)): + vite-plugin-css-injected-by-js@4.0.1(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)): dependencies: - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1) + vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1) - vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1): + vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -6234,14 +6254,14 @@ snapshots: rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.3.0 + '@types/node': 25.3.5 fsevents: 2.3.3 jiti: 2.6.1 - vitest@4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1): + vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -6258,11 +6278,11 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1) + vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.3.0 - happy-dom: 20.7.0 + '@types/node': 25.3.5 + happy-dom: 20.8.3 transitivePeerDependencies: - jiti - less diff --git a/airflow-core/src/airflow/api_fastapi/auth/tokens.py b/airflow-core/src/airflow/api_fastapi/auth/tokens.py index 4732164be7115..3375853a29a5d 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/tokens.py +++ b/airflow-core/src/airflow/api_fastapi/auth/tokens.py @@ -93,7 +93,7 @@ def _guess_best_algorithm(key: AllowedPrivateKeys): from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey if isinstance(key, RSAPrivateKey): - return "RS512" + return "RS256" if isinstance(key, Ed25519PrivateKey): return "EdDSA" raise ValueError(f"Unknown key object {type(key)}") @@ -291,14 +291,8 @@ def __attrs_post_init__(self): raise ValueError("Exactly one of private_key and secret_key must be specified") if self.algorithm == ["GUESS"]: - if self.jwks: - # TODO: We could probably populate this from the jwks document, but we don't have that at - # construction time. - raise ValueError( - "Cannot guess the algorithm when using JWKS - please specify it in the config option " - "[api_auth] jwt_algorithm" - ) - self.algorithm = ["HS512"] + if not self.jwks: + self.algorithm = ["HS512"] def _get_kid_from_header(self, unvalidated: str) -> str: header = jwt.get_unverified_header(unvalidated) @@ -326,13 +320,21 @@ async def avalidated_claims( ) -> dict[str, Any]: """Decode the JWT token, returning the validated claims or raising an exception.""" key = await self._get_validation_key(unvalidated) + algorithms = self.algorithm + validation_key: str | jwt.PyJWK | Any = key + if algorithms == ["GUESS"] and isinstance(key, jwt.PyJWK): + if not key.algorithm_name: + raise jwt.InvalidTokenError("Missing algorithm in JWK") + algorithms = [key.algorithm_name] + validation_key = key.key + claims = jwt.decode( unvalidated, - key, + validation_key, audience=self.audience, issuer=self.issuer, options={"require": list(self.required_claims)}, - algorithms=self.algorithm, + algorithms=algorithms, leeway=self.leeway, ) diff --git a/airflow-core/src/airflow/api_fastapi/common/dagbag.py b/airflow-core/src/airflow/api_fastapi/common/dagbag.py index ce81f6906b721..3ca4483ce876a 100644 --- a/airflow-core/src/airflow/api_fastapi/common/dagbag.py +++ b/airflow-core/src/airflow/api_fastapi/common/dagbag.py @@ -70,9 +70,23 @@ def get_dag_for_run(dag_bag: DBDagBag, dag_run: DagRun, session: Session) -> Ser def get_dag_for_run_or_latest_version( dag_bag: DBDagBag, dag_run: DagRun | None, dag_id: str | None, session: Session ) -> SerializedDAG: + """ + Retrieve the serialized DAG for a specific run, or the latest version if no run is given. + + When a dag_run is provided, we prefer the exact DAG version the run was created with + (``created_dag_version_id``) so that task group lookups, operator metadata, etc. match + the DAG structure at the time of the run. + + This is necessary because ``get_dag_for_run`` delegates to ``_version_from_dag_run`` + which, for unversioned bundles (e.g. ``LocalDagBundle``), falls back to the *latest* + ``DagVersion``. + """ dag: SerializedDAG | None = None if dag_run: - dag = dag_bag.get_dag_for_run(dag_run, session=session) + if dag_run.created_dag_version_id: + dag = dag_bag._get_dag(dag_run.created_dag_version_id, session=session) + if not dag: + dag = dag_bag.get_dag_for_run(dag_run, session=session) elif dag_id: dag = dag_bag.get_latest_version_of_dag(dag_id, session=session) if not dag: diff --git a/airflow-core/src/airflow/api_fastapi/common/exceptions.py b/airflow-core/src/airflow/api_fastapi/common/exceptions.py index 2495a64dd288c..12d2486253c11 100644 --- a/airflow-core/src/airflow/api_fastapi/common/exceptions.py +++ b/airflow-core/src/airflow/api_fastapi/common/exceptions.py @@ -74,22 +74,26 @@ def exception_handler(self, request: Request, exc: IntegrityError): for tb in traceback.format_tb(exc.__traceback__): stacktrace += tb - log_message = f"Error with id {exception_id}\n{stacktrace}" + log_message = f"Error with id {exception_id}, statement: {exc.statement}\n{stacktrace}" log.error(log_message) if conf.get("api", "expose_stacktrace") == "True": message = log_message + statement = str(exc.statement) + orig_error = str(exc.orig) else: message = ( "Serious error when handling your request. Check logs for more details - " f"you will find it in api server when you look for ID {exception_id}" ) + statement = "hidden" + orig_error = "hidden" raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail={ "reason": "Unique constraint violation", - "statement": str(exc.statement), - "orig_error": str(exc.orig), + "statement": statement, + "orig_error": orig_error, "message": message, }, ) diff --git a/airflow-core/src/airflow/api_fastapi/common/http_access_log.py b/airflow-core/src/airflow/api_fastapi/common/http_access_log.py new file mode 100644 index 0000000000000..581c359379011 --- /dev/null +++ b/airflow-core/src/airflow/api_fastapi/common/http_access_log.py @@ -0,0 +1,106 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""HTTP access log middleware using structlog.""" + +from __future__ import annotations + +import contextlib +import time +from typing import TYPE_CHECKING + +import structlog + +if TYPE_CHECKING: + from starlette.types import ASGIApp, Message, Receive, Scope, Send + +logger = structlog.get_logger(logger_name="http.access") + +_HEALTH_PATHS = frozenset(["/api/v2/monitor/health"]) + + +class HttpAccessLogMiddleware: + """ + Log completed HTTP requests as structured log events. + + This middleware replaces uvicorn's built-in access logger. It measures the + full round-trip duration, binds any ``x-request-id`` header value to the + structlog context for the duration of the request, and emits one log event + per completed request. + + Health-check paths are excluded to avoid log noise. + """ + + def __init__( + self, + app: ASGIApp, + request_id_header: str = "x-request-id", + ) -> None: + self.app = app + self.request_id_header = request_id_header.lower().encode("ascii") + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + start = time.monotonic_ns() + response: Message | None = None + + async def capture_send(message: Message) -> None: + nonlocal response + if message["type"] == "http.response.start": + response = message + await send(message) + + request_id: str | None = None + for name, value in scope["headers"]: + if name == self.request_id_header: + request_id = value.decode("ascii", errors="replace") + break + + ctx = ( + structlog.contextvars.bound_contextvars(request_id=request_id) + if request_id is not None + else contextlib.nullcontext() + ) + + with ctx: + try: + await self.app(scope, receive, capture_send) + except Exception: + if response is None: + response = {"status": 500} + raise + finally: + path = scope["path"] + if path not in _HEALTH_PATHS: + duration_us = (time.monotonic_ns() - start) // 1000 + status = response["status"] if response is not None else 0 + method = scope.get("method", "") + query = scope["query_string"].decode("ascii", errors="replace") + client = scope.get("client") + client_addr = f"{client[0]}:{client[1]}" if client else None + + logger.info( + "request finished", + method=method, + path=path, + query=query, + status_code=status, + duration_us=duration_us, + client_addr=client_addr, + ) diff --git a/airflow-core/src/airflow/api_fastapi/common/types.py b/airflow-core/src/airflow/api_fastapi/common/types.py index f31809f4a4f2d..269669c8ee50e 100644 --- a/airflow-core/src/airflow/api_fastapi/common/types.py +++ b/airflow-core/src/airflow/api_fastapi/common/types.py @@ -71,6 +71,28 @@ class TimeDelta(BaseModel): TimeDeltaWithValidation = Annotated[TimeDelta, BeforeValidator(_validate_timedelta_field)] +# Common validator for theme icon fields (SVG-only, http(s) or app-relative path). +def _validate_theme_icon(value: str | None) -> str | None: + if value is None: + return value + from urllib.parse import urlparse + + parsed = urlparse(value) + if parsed.scheme in ("http", "https"): + path = parsed.path or "" + elif parsed.scheme == "" and value.startswith("/"): + path = value + else: + raise ValueError("theme.icon must be http(s) URL or app-relative path starting with '/'") + if not path.lower().endswith(".svg"): + raise ValueError("theme.icon must point to an SVG file (*.svg)") + return value + + +# Alias type for theme icon fields with shared validation +ThemeIconType = Annotated[str | None, BeforeValidator(_validate_theme_icon)] + + class Mimetype(str, Enum): """Mimetype for the `Content-Type` header.""" @@ -180,3 +202,5 @@ class Theme(BaseModel): ], ] globalCss: dict[str, dict] | None = None + icon: ThemeIconType = None + icon_dark_mode: ThemeIconType = None diff --git a/airflow-core/src/airflow/api_fastapi/core_api/app.py b/airflow-core/src/airflow/api_fastapi/core_api/app.py index 5c0bc562f12ec..2622aec86b93f 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/app.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/app.py @@ -165,6 +165,7 @@ def init_error_handlers(app: FastAPI) -> None: def init_middlewares(app: FastAPI) -> None: from airflow.api_fastapi.auth.middlewares.refresh_token import JWTRefreshMiddleware + from airflow.api_fastapi.common.http_access_log import HttpAccessLogMiddleware app.add_middleware(JWTRefreshMiddleware) if conf.getboolean("core", "simple_auth_manager_all_admins"): @@ -172,8 +173,10 @@ def init_middlewares(app: FastAPI) -> None: app.add_middleware(SimpleAllAdminMiddleware) - # The GzipMiddleware should be the last middleware added as https://github.com/apache/airflow/issues/60165 points out. - # Compress responses greater than 1kB with optimal compression level as 5 - # with level ranging from 1 to 9 with 1 (fastest, least compression) - # and 9 (slowest, most compression) + # GZipMiddleware must be inside HttpAccessLogMiddleware so that access logs capture + # the full end-to-end duration including compression time. + # See https://github.com/apache/airflow/issues/60165 app.add_middleware(GZipMiddleware, minimum_size=1024, compresslevel=5) + # HttpAccessLogMiddleware must be outermost (added last) so it times the full + # request lifecycle including all inner middleware. + app.add_middleware(HttpAccessLogMiddleware) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/base.py b/airflow-core/src/airflow/api_fastapi/core_api/base.py index 887f528f197ef..600a6d896e2f2 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/base.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/base.py @@ -17,9 +17,10 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Generic, TypeVar +from copy import deepcopy +from typing import TYPE_CHECKING, Generic, TypeVar, Union, get_args, get_origin -from pydantic import BaseModel as PydanticBaseModel, ConfigDict +from pydantic import BaseModel as PydanticBaseModel, ConfigDict, create_model if TYPE_CHECKING: from sqlalchemy.sql import Select @@ -49,6 +50,25 @@ class StrictBaseModel(BaseModel): model_config = ConfigDict(from_attributes=True, populate_by_name=True, extra="forbid") +def make_partial_model(model: type[PydanticBaseModel]) -> type[PydanticBaseModel]: + """Create a version of a Pydantic model where all fields are Optional with default=None.""" + field_overrides: dict = {} + for field_name, field_info in model.model_fields.items(): + new_info = deepcopy(field_info) + ann = field_info.annotation + origin = get_origin(ann) + if not (origin is Union and type(None) in get_args(ann)): + ann = ann | None # type: ignore[operator, assignment] + new_info.default = None + field_overrides[field_name] = (ann, new_info) + + return create_model( + f"{model.__name__}Partial", + __base__=model, + **field_overrides, + ) + + class OrmClause(Generic[T], ABC): """ Base class for filtering clauses with paginated_select. diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/connections.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/connections.py index 58bf8ca8e0d00..f7cb944ebbf6b 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/connections.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/connections.py @@ -25,7 +25,7 @@ from pydantic_core.core_schema import ValidationInfo from airflow._shared.secrets_masker import redact, should_hide_value_for_key -from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel +from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel, make_partial_model # Response Models @@ -193,3 +193,6 @@ def validate_extra(cls, v: str | None) -> str | None: "but encountered non-JSON in `extra` field" ) return v + + +ConnectionBodyPartial = make_partial_model(ConnectionBody) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py index 5bda20af0bfd5..333349ad5e1a1 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py @@ -33,7 +33,7 @@ field_validator, ) -from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel +from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel, make_partial_model from airflow.api_fastapi.core_api.datamodels.dag_tags import DagTagResponse from airflow.api_fastapi.core_api.datamodels.dag_versions import DagVersionResponse from airflow.configuration import conf @@ -146,6 +146,9 @@ class DAGPatchBody(StrictBaseModel): is_paused: bool +DAGPatchBodyPartial = make_partial_model(DAGPatchBody) + + class DAGCollectionResponse(BaseModel): """DAG Collection serializer for responses.""" diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/auth.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/auth.py index 8cc6c4648a232..f5563fbd6c753 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/auth.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/auth.py @@ -17,6 +17,8 @@ from __future__ import annotations +from enum import Enum + from airflow.api_fastapi.common.types import ExtraMenuItem, MenuItem from airflow.api_fastapi.core_api.base import BaseModel @@ -33,3 +35,24 @@ class AuthenticatedMeResponse(BaseModel): id: str username: str + + +class TokenType(str, Enum): + """Type of token to generate.""" + + API = "api" + CLI = "cli" + + +class GenerateTokenBody(BaseModel): + """Request body for generating a token.""" + + token_type: TokenType = TokenType.API + + +class GenerateTokenResponse(BaseModel): + """Response for a generated token.""" + + access_token: str + token_type: TokenType + expires_in_seconds: int diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py index bc19207c29cdf..f952a5a777082 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py @@ -23,7 +23,7 @@ from pydantic import Field, JsonValue, model_validator from airflow._shared.secrets_masker import redact -from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel +from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel, make_partial_model from airflow.models.base import ID_LEN from airflow.typing_compat import Self @@ -61,6 +61,9 @@ class VariableBody(StrictBaseModel): team_name: str | None = Field(max_length=50, default=None) +VariableBodyPartial = make_partial_model(VariableBody) + + class VariableCollectionResponse(BaseModel): """Variable Collection serializer for responses.""" diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml index f60a778113021..6c38b7ce2d6c3 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml @@ -40,6 +40,35 @@ paths: security: - OAuth2PasswordBearer: [] - HTTPBearer: [] + /ui/auth/token: + post: + tags: + - Auth Links + summary: Generate Token + description: Generate a JWT token for the authenticated user. + operationId: generate_token + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateTokenBody' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateTokenResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - OAuth2PasswordBearer: [] + - HTTPBearer: [] /ui/next_run_assets/{dag_id}: get: tags: @@ -2238,6 +2267,31 @@ components: - end_date title: GanttTaskInstance description: Task instance data for Gantt chart. + GenerateTokenBody: + properties: + token_type: + $ref: '#/components/schemas/TokenType' + default: api + type: object + title: GenerateTokenBody + description: Request body for generating a token. + GenerateTokenResponse: + properties: + access_token: + type: string + title: Access Token + token_type: + $ref: '#/components/schemas/TokenType' + expires_in_seconds: + type: integer + title: Expires In Seconds + type: object + required: + - access_token + - token_type + - expires_in_seconds + title: GenerateTokenResponse + description: Response for a generated token. GridNodeResponse: properties: id: @@ -3242,11 +3296,28 @@ components: type: object - type: 'null' title: Globalcss + icon: + anyOf: + - type: string + - type: 'null' + title: Icon + icon_dark_mode: + anyOf: + - type: string + - type: 'null' + title: Icon Dark Mode type: object required: - tokens title: Theme description: JSON to modify Chakra's theme. + TokenType: + type: string + enum: + - api + - cli + title: TokenType + description: Type of token to generate. TriggerResponse: properties: id: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index d3955872f71b1..0bf3274992702 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -2285,6 +2285,14 @@ paths: items: type: integer title: Dag Version + - name: bundle_version + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Bundle Version - name: order_by in: query required: false diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py index 65e90a321b11c..68220c20e8f95 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py @@ -26,6 +26,8 @@ from sqlalchemy.orm import joinedload, subqueryload from airflow._shared.timezones import timezone +from airflow.api_fastapi.app import get_auth_manager +from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity, DagDetails from airflow.api_fastapi.common.dagbag import DagBagDep, get_latest_version_of_dag from airflow.api_fastapi.common.db.common import SessionDep, paginated_select from airflow.api_fastapi.common.parameters import ( @@ -404,6 +406,17 @@ def materialize_asset( f"More than one DAG materializes asset with ID: {asset_id}", ) + if not get_auth_manager().is_authorized_dag( + method="POST", + access_entity=DagAccessEntity.RUN, + details=DagDetails(id=dag_id), + user=user, + ): + raise HTTPException( + status.HTTP_403_FORBIDDEN, + f"User is not authorized to trigger a run for DAG: {dag_id} that materializes this asset", + ) + dag = get_latest_version_of_dag(dag_bag, dag_id, session) if dag.allowed_run_types is not None and DagRunType.ASSET_MATERIALIZATION not in dag.allowed_run_types: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py index 744dff444fd3e..05ec6b642941d 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py @@ -38,6 +38,7 @@ ) from airflow.api_fastapi.core_api.datamodels.connections import ( ConnectionBody, + ConnectionBodyPartial, ConnectionCollectionResponse, ConnectionResponse, ConnectionTestResponse, @@ -203,10 +204,17 @@ def patch_connection( status.HTTP_404_NOT_FOUND, f"The Connection with connection_id: `{connection_id}` was not found" ) - try: - ConnectionBody(**patch_body.model_dump()) - except ValidationError as e: - raise RequestValidationError(errors=e.errors()) + if update_mask: + fields_to_update = patch_body.model_fields_set & set(update_mask) + try: + ConnectionBodyPartial(**patch_body.model_dump(include=fields_to_update)) + except ValidationError as e: + raise RequestValidationError(errors=e.errors()) + else: + try: + ConnectionBody(**patch_body.model_dump()) + except ValidationError as e: + raise RequestValidationError(errors=e.errors()) update_orm_from_pydantic(connection, patch_body, update_mask) return connection diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py index d0cc597fd5123..359809458cf29 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py @@ -346,6 +346,9 @@ def get_dag_runs( run_type: QueryDagRunRunTypesFilter, state: QueryDagRunStateFilter, dag_version: QueryDagRunVersionFilter, + bundle_version: Annotated[ + FilterParam[str | None], Depends(filter_param_factory(DagRun.bundle_version, str | None)) + ], order_by: Annotated[ SortParam, Depends( @@ -407,6 +410,7 @@ def get_dag_runs( state, run_type, dag_version, + bundle_version, readable_dag_runs_filter, run_id_pattern, triggering_user_name_pattern, diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py index f4c9186cd2fca..6fbb7c831e9d9 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py @@ -62,6 +62,7 @@ DAGCollectionResponse, DAGDetailsResponse, DAGPatchBody, + DAGPatchBodyPartial, DAGResponse, ) from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc @@ -286,6 +287,10 @@ def patch_dag( status.HTTP_400_BAD_REQUEST, "Only `is_paused` field can be updated through the REST API" ) fields_to_update = fields_to_update.intersection(update_mask) + try: + DAGPatchBodyPartial(**patch_body.model_dump(include=fields_to_update)) + except ValidationError as e: + raise RequestValidationError(errors=e.errors()) else: try: DAGPatchBody(**patch_body.model_dump()) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/auth.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/auth.py index 7927f810d4936..c7d889fca52f2 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/auth.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/auth.py @@ -17,13 +17,24 @@ from __future__ import annotations +import logging + +from fastapi import Depends + from airflow.api_fastapi.app import get_auth_manager from airflow.api_fastapi.common.router import AirflowRouter from airflow.api_fastapi.core_api.datamodels.ui.auth import ( AuthenticatedMeResponse, + GenerateTokenBody, + GenerateTokenResponse, MenuItemCollectionResponse, + TokenType, ) from airflow.api_fastapi.core_api.security import GetUserDep +from airflow.api_fastapi.logging.decorators import action_logging +from airflow.configuration import conf + +log = logging.getLogger(__name__) auth_router = AirflowRouter(tags=["Auth Links"]) @@ -50,3 +61,30 @@ def get_current_user_info( id=user.get_id(), username=user.get_name(), ) + + +@auth_router.post("/auth/token", dependencies=[Depends(action_logging())]) +def generate_token( + body: GenerateTokenBody, + user: GetUserDep, +) -> GenerateTokenResponse: + """Generate a JWT token for the authenticated user.""" + if body.token_type == TokenType.CLI: + expiration_seconds = conf.getint("api_auth", "jwt_cli_expiration_time") + else: + expiration_seconds = conf.getint("api_auth", "jwt_expiration_time") + + access_token = get_auth_manager().generate_jwt(user, expiration_time_in_seconds=expiration_seconds) + + log.info( + "User %s generated a %s token (expires in %d seconds)", + user.get_name(), + body.token_type.value, + expiration_seconds, + ) + + return GenerateTokenResponse( + access_token=access_token, + token_type=body.token_type, + expires_in_seconds=expiration_seconds, + ) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py index 32b2891b9543a..02583a8355b9d 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py @@ -36,7 +36,7 @@ from airflow.api_fastapi.core_api.openapi.exceptions import ( create_openapi_http_exception_doc, ) -from airflow.api_fastapi.core_api.security import requires_access_backfill +from airflow.api_fastapi.core_api.security import ReadableBackfillsFilterDep, requires_access_backfill from airflow.models.backfill import Backfill backfills_router = AirflowRouter(tags=["Backfill"], prefix="/backfills") @@ -56,6 +56,7 @@ def list_backfills_ui( SortParam, Depends(SortParam(["id"], Backfill).dynamic_depends()), ], + readable_backfills_filter: ReadableBackfillsFilterDep, session: SessionDep, dag_id: Annotated[FilterParam[str | None], Depends(filter_param_factory(Backfill.dag_id, str | None))], active: Annotated[ @@ -65,7 +66,7 @@ def list_backfills_ui( ) -> BackfillCollectionResponse: select_stmt, total_entries = paginated_select( statement=select(Backfill).options(joinedload(Backfill.dag_model)), - filters=[dag_id, active], + filters=[dag_id, active, readable_backfills_filter], order_by=order_by, offset=offset, limit=limit, diff --git a/airflow-core/src/airflow/api_fastapi/core_api/security.py b/airflow-core/src/airflow/api_fastapi/core_api/security.py index 0e59fb3550c9f..774cfa9ed6a74 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/security.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/security.py @@ -238,6 +238,13 @@ def to_orm(self, select: Select) -> Select: return select.where(DagVersion.dag_id.in_(self.value or set())) +class PermittedBackfillFilter(PermittedDagFilter): + """A parameter that filters the permitted backfills for the user.""" + + def to_orm(self, select: Select) -> Select: + return select.where(Backfill.dag_id.in_(self.value or set())) + + def permitted_dag_filter_factory( method: ResourceMethod, filter_class=PermittedDagFilter ) -> Callable[[BaseUser, BaseAuthManager], PermittedDagFilter]: @@ -282,6 +289,9 @@ def depends_permitted_dags_filter( ReadableDagVersionsFilterDep = Annotated[ PermittedDagVersionFilter, Depends(permitted_dag_filter_factory("GET", PermittedDagVersionFilter)) ] +ReadableBackfillsFilterDep = Annotated[ + PermittedBackfillFilter, Depends(permitted_dag_filter_factory("GET", PermittedBackfillFilter)) +] def requires_access_backfill( diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/public/variables.py b/airflow-core/src/airflow/api_fastapi/core_api/services/public/variables.py index a5011768cb0aa..e363565bb92a3 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/services/public/variables.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/services/public/variables.py @@ -35,6 +35,7 @@ ) from airflow.api_fastapi.core_api.datamodels.variables import ( VariableBody, + VariableBodyPartial, ) from airflow.api_fastapi.core_api.services.public.common import BulkService from airflow.models.variable import Variable @@ -65,10 +66,17 @@ def update_orm_from_pydantic( status.HTTP_404_NOT_FOUND, f"The Variable with key: `{variable_key}` was not found" ) - try: - VariableBody(**patch_body.model_dump()) - except ValidationError as e: - raise RequestValidationError(errors=e.errors()) + if update_mask: + fields_to_update = patch_body.model_fields_set & set(update_mask) + try: + VariableBodyPartial(**patch_body.model_dump(include=fields_to_update)) + except ValidationError as e: + raise RequestValidationError(errors=e.errors()) + else: + try: + VariableBody(**patch_body.model_dump()) + except ValidationError as e: + raise RequestValidationError(errors=e.errors()) non_update_fields = {"key"} if patch_body.key != old_variable.key: diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/AGENTS.md b/airflow-core/src/airflow/api_fastapi/execution_api/AGENTS.md index 39e083345735f..32500df182e3a 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/AGENTS.md +++ b/airflow-core/src/airflow/api_fastapi/execution_api/AGENTS.md @@ -63,3 +63,7 @@ Adding a new Execution API feature touches multiple packages. All of these must - Triggerer handler: `airflow-core/src/airflow/jobs/triggerer_job_runner.py` - Task SDK generated models: `task-sdk/src/airflow/sdk/api/datamodels/_generated.py` - Full versioning guide: [`contributing-docs/19_execution_api_versioning.rst`](../../../../contributing-docs/19_execution_api_versioning.rst) + +## Token Scope Infrastructure + +Token types (`"execution"`, `"workload"`), route-level enforcement via `ExecutionAPIRoute` + `require_auth`, and the `ti:self` path-parameter validation are documented in the module docstring of `security.py`. diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/app.py b/airflow-core/src/airflow/api_fastapi/execution_api/app.py index ac0d8012a903f..c7a9593c3c82f 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/app.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/app.py @@ -220,6 +220,15 @@ def replace_any_of_with_one_of(spec): if prop.get("type") == "string" and (const := prop.pop("const", None)): prop["enum"] = [const] + # Remove internal x-airflow-* extension fields from OpenAPI spec + # These are used for runtime validation but shouldn't be exposed in the public API + for path_item in openapi_schema.get("paths", {}).values(): + for operation in path_item.values(): + if isinstance(operation, dict): + keys_to_remove = [key for key in operation.keys() if key.startswith("x-airflow-")] + for key in keys_to_remove: + del operation[key] + return openapi_schema @@ -304,23 +313,26 @@ def app(self): if not self._app: from airflow.api_fastapi.common.dagbag import create_dag_bag from airflow.api_fastapi.execution_api.app import create_task_execution_api_app - from airflow.api_fastapi.execution_api.deps import ( - JWTBearerDep, - JWTBearerTIPathDep, - ) + from airflow.api_fastapi.execution_api.datamodels.token import TIToken from airflow.api_fastapi.execution_api.routes.connections import has_connection_access from airflow.api_fastapi.execution_api.routes.variables import has_variable_access from airflow.api_fastapi.execution_api.routes.xcoms import has_xcom_access + from airflow.api_fastapi.execution_api.security import _jwt_bearer self._app = create_task_execution_api_app() # Set up dag_bag in app state for dependency injection self._app.state.dag_bag = create_dag_bag() - async def always_allow(): ... + async def always_allow(request: Request): + from uuid import UUID + + ti_id = UUID( + request.path_params.get("task_instance_id", "00000000-0000-0000-0000-000000000000") + ) + return TIToken(id=ti_id, claims={"scope": "execution"}) - self._app.dependency_overrides[JWTBearerDep.dependency] = always_allow - self._app.dependency_overrides[JWTBearerTIPathDep.dependency] = always_allow + self._app.dependency_overrides[_jwt_bearer] = always_allow self._app.dependency_overrides[has_connection_access] = always_allow self._app.dependency_overrides[has_variable_access] = always_allow self._app.dependency_overrides[has_xcom_access] = always_allow diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/deps.py b/airflow-core/src/airflow/api_fastapi/execution_api/deps.py index 9fc8c30cb926e..192309a8e403f 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/deps.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/deps.py @@ -18,23 +18,8 @@ # Disable future annotations in this file to work around https://github.com/fastapi/fastapi/issues/13056 # ruff: noqa: I002 -from typing import Any - -import structlog import svcs -from fastapi import Depends, HTTPException, Request, status -from fastapi.security import HTTPBearer -from sqlalchemy import select - -from airflow.api_fastapi.auth.tokens import JWTValidator -from airflow.api_fastapi.common.db.common import AsyncSessionDep -from airflow.api_fastapi.execution_api.datamodels.token import TIToken -from airflow.configuration import conf -from airflow.models import DagModel, TaskInstance -from airflow.models.dagbundle import DagBundleModel -from airflow.models.team import Team - -log = structlog.get_logger(logger_name=__name__) +from fastapi import Depends, Request # See https://github.com/fastapi/fastapi/issues/13056 @@ -44,76 +29,3 @@ async def _container(request: Request): DepContainer: svcs.Container = Depends(_container) - - -class JWTBearer(HTTPBearer): - """ - A FastAPI security dependency that validates JWT tokens using for the Execution API. - - This will validate the tokens are signed and that the ``sub`` is a UUID, but nothing deeper than that. - - The dependency result will be an `TIToken` object containing the ``id`` UUID (from the ``sub``) and other - validated claims. - """ - - def __init__( - self, - path_param_name: str | None = None, - required_claims: dict[str, Any] | None = None, - ): - super().__init__(auto_error=False) - self.path_param_name = path_param_name - self.required_claims = required_claims or {} - - async def __call__( # type: ignore[override] - self, - request: Request, - services=DepContainer, - ) -> TIToken | None: - creds = await super().__call__(request) - if not creds: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing auth token") - - validator: JWTValidator = await services.aget(JWTValidator) - - try: - # Example: Validate "task_instance_id" component of the path matches the one in the token - if self.path_param_name: - id = request.path_params[self.path_param_name] - validators: dict[str, Any] = { - **self.required_claims, - "sub": {"essential": True, "value": id}, - } - else: - validators = self.required_claims - claims = await validator.avalidated_claims(creds.credentials, validators) - return TIToken(id=claims["sub"], claims=claims) - except Exception as err: - log.warning( - "Failed to validate JWT", - exc_info=True, - token=creds.credentials, - ) - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Invalid auth token: {err}") - - -JWTBearerDep: TIToken = Depends(JWTBearer()) - -# This checks that the UUID in the url matches the one in the token for us. -JWTBearerTIPathDep = Depends(JWTBearer(path_param_name="task_instance_id")) - - -async def get_team_name_dep(session: AsyncSessionDep, token=JWTBearerDep) -> str | None: - """Return the team name associated to the task (if any).""" - if not conf.getboolean("core", "multi_team"): - return None - - stmt = ( - select(Team.name) - .select_from(TaskInstance) - .join(DagModel, DagModel.dag_id == TaskInstance.dag_id) - .join(DagBundleModel, DagBundleModel.name == DagModel.bundle_name) - .join(DagBundleModel.teams) - .where(TaskInstance.id == token.id) - ) - return await session.scalar(stmt) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py index 562b8588fbf2c..aeef4d092b194 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py @@ -17,9 +17,8 @@ from __future__ import annotations from cadwyn import VersionedAPIRouter -from fastapi import APIRouter +from fastapi import APIRouter, Security -from airflow.api_fastapi.execution_api.deps import JWTBearerDep from airflow.api_fastapi.execution_api.routes import ( asset_events, assets, @@ -32,12 +31,13 @@ variables, xcoms, ) +from airflow.api_fastapi.execution_api.security import require_auth execution_api_router = APIRouter() execution_api_router.include_router(health.router, prefix="/health", tags=["Health"]) # _Every_ single endpoint under here must be authenticated. Some do further checks on top of these -authenticated_router = VersionedAPIRouter(dependencies=[JWTBearerDep]) # type: ignore[list-item] +authenticated_router = VersionedAPIRouter(dependencies=[Security(require_auth)]) # type: ignore[list-item] authenticated_router.include_router(assets.router, prefix="/assets", tags=["Assets"]) authenticated_router.include_router(asset_events.router, prefix="/asset-events", tags=["Asset Events"]) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/asset_events.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/asset_events.py index 4525a9140e5d1..d0ecc3d3adaf2 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/asset_events.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/asset_events.py @@ -31,7 +31,6 @@ ) from airflow.models.asset import AssetAliasModel, AssetEvent, AssetModel -# TODO: Add dependency on JWT token router = APIRouter( responses={ status.HTTP_404_NOT_FOUND: {"description": "Asset not found"}, diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/assets.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/assets.py index 316d4fab4770d..40397e44f43b9 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/assets.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/assets.py @@ -26,7 +26,6 @@ from airflow.api_fastapi.execution_api.datamodels.asset import AssetResponse from airflow.models.asset import AssetModel -# TODO: Add dependency on JWT token router = APIRouter( responses={ status.HTTP_404_NOT_FOUND: {"description": "Asset not found"}, diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/connections.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/connections.py index 44cc3bfbd79bb..a7bb9959c6db7 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/connections.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/connections.py @@ -23,14 +23,14 @@ from fastapi import APIRouter, Depends, HTTPException, Path, status from airflow.api_fastapi.execution_api.datamodels.connection import ConnectionResponse -from airflow.api_fastapi.execution_api.deps import JWTBearerDep, get_team_name_dep +from airflow.api_fastapi.execution_api.security import CurrentTIToken, get_team_name_dep from airflow.exceptions import AirflowNotFoundException from airflow.models.connection import Connection async def has_connection_access( connection_id: str = Path(), - token=JWTBearerDep, + token=CurrentTIToken, ) -> bool: """Check if the task has access to the connection.""" # TODO: Placeholder for actual implementation diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py index 92e973a3d0015..802b09502e09b 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py @@ -19,7 +19,8 @@ from uuid import UUID import structlog -from fastapi import APIRouter, HTTPException, status +from cadwyn import VersionedAPIRouter +from fastapi import HTTPException, Security, status from sqlalchemy import select from airflow._shared.timezones import timezone @@ -29,14 +30,15 @@ HITLDetailResponse, UpdateHITLDetailPayload, ) -from airflow.api_fastapi.execution_api.deps import JWTBearerTIPathDep +from airflow.api_fastapi.execution_api.security import ExecutionAPIRoute, require_auth from airflow.models.hitl import HITLDetail -router = APIRouter( +router = VersionedAPIRouter( + route_class=ExecutionAPIRoute, dependencies=[ - # This checks that the UUID in the url matches the one in the token for us. - JWTBearerTIPathDep - ] + # Validates that the JWT sub matches the task_instance_id path parameter. + Security(require_auth, scopes=["ti:self"]), + ], ) log = structlog.get_logger(__name__) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py index f22d7c125853d..60dd868c2e335 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py @@ -28,9 +28,9 @@ import attrs import structlog from cadwyn import VersionedAPIRouter -from fastapi import Body, HTTPException, Query, status +from fastapi import Body, HTTPException, Query, Security, status from pydantic import JsonValue -from sqlalchemy import func, or_, tuple_, update +from sqlalchemy import and_, func, or_, tuple_, update from sqlalchemy.engine import CursorResult from sqlalchemy.exc import NoResultFound, SQLAlchemyError from sqlalchemy.orm import joinedload @@ -59,11 +59,12 @@ TISuccessStatePayload, TITerminalStatePayload, ) -from airflow.api_fastapi.execution_api.deps import JWTBearerTIPathDep +from airflow.api_fastapi.execution_api.security import ExecutionAPIRoute, require_auth from airflow.exceptions import TaskNotFound from airflow.models.asset import AssetActive from airflow.models.dag import DagModel from airflow.models.dagrun import DagRun as DR +from airflow.models.log import Log from airflow.models.taskinstance import TaskInstance as TI, _stop_remaining_tasks from airflow.models.taskreschedule import TaskReschedule from airflow.models.trigger import Trigger @@ -78,10 +79,10 @@ router = VersionedAPIRouter() ti_id_router = VersionedAPIRouter( + route_class=ExecutionAPIRoute, dependencies=[ - # This checks that the UUID in the url matches the one in the token for us. - JWTBearerTIPathDep - ] + Security(require_auth, scopes=["ti:self"]), + ], ) @@ -133,13 +134,17 @@ def ti_run( TI.hostname, TI.unixname, TI.pid, - # This selects the raw JSON value, by-passing the deserialization -- we want that to happen on the + # This selects the raw JSON value, bypassing the deserialization -- we want that to happen on the # client column("next_kwargs", JSON), + DR.logical_date, + DagModel.owners, ) .select_from(TI) + .join(DR, and_(TI.dag_id == DR.dag_id, TI.run_id == DR.run_id)) + .join(DagModel, TI.dag_id == DagModel.dag_id) .where(TI.id == task_instance_id) - .with_for_update() + .with_for_update(of=TI) ) try: ti = session.execute(old).one() @@ -195,6 +200,19 @@ def ti_run( ) else: log.info("Task started", previous_state=previous_state, hostname=ti_run_payload.hostname) + session.add( + Log( + event=TaskInstanceState.RUNNING.value, + task_id=ti.task_id, + dag_id=ti.dag_id, + run_id=ti.run_id, + map_index=ti.map_index, + try_number=ti.try_number, + logical_date=ti.logical_date, + owner=ti.owners, + extra=json.dumps({"host_name": ti_run_payload.hostname}) if ti_run_payload.hostname else None, + ) + ) # Ensure there is no end date set. query = query.values( end_date=None, @@ -297,9 +315,23 @@ def ti_update_state( log.debug("Updating task instance state", new_state=ti_patch_payload.state) old = ( - select(TI.state, TI.try_number, TI.max_tries, TI.dag_id) + select( + TI.state, + TI.try_number, + TI.max_tries, + TI.dag_id, + TI.task_id, + TI.run_id, + TI.map_index, + TI.hostname, + DR.logical_date, + DagModel.owners, + ) + .select_from(TI) + .join(DR, and_(TI.dag_id == DR.dag_id, TI.run_id == DR.run_id)) + .join(DagModel, TI.dag_id == DagModel.dag_id) .where(TI.id == task_instance_id) - .with_for_update() + .with_for_update(of=TI) ) try: ( @@ -307,6 +339,12 @@ def ti_update_state( try_number, max_tries, dag_id, + task_id, + run_id, + map_index, + hostname, + logical_date, + owners, ) = session.execute(old).one() log.debug( "Retrieved current task instance state", @@ -373,6 +411,19 @@ def ti_update_state( new_state=updated_state, rows_affected=getattr(result, "rowcount", 0), ) + session.add( + Log( + event=updated_state.value, + task_id=task_id, + dag_id=dag_id, + run_id=run_id, + map_index=map_index, + try_number=try_number, + logical_date=logical_date, + owner=owners, + extra=json.dumps({"host_name": hostname}) if hostname else None, + ) + ) except SQLAlchemyError as e: log.error("Error updating Task Instance state", error=str(e)) raise HTTPException( diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py index 5621b6cd081ba..1e2e2058932da 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py @@ -26,14 +26,14 @@ VariablePostBody, VariableResponse, ) -from airflow.api_fastapi.execution_api.deps import JWTBearerDep, get_team_name_dep +from airflow.api_fastapi.execution_api.security import CurrentTIToken, get_team_name_dep from airflow.models.variable import Variable async def has_variable_access( request: Request, variable_key: str = Path(), - token=JWTBearerDep, + token=CurrentTIToken, ): """Check if the task has access to the variable.""" write = request.method not in {"GET", "HEAD", "OPTIONS"} diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py index ec77b64dc4496..9b83c40db5e30 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py @@ -32,7 +32,7 @@ XComSequenceIndexResponse, XComSequenceSliceResponse, ) -from airflow.api_fastapi.execution_api.deps import JWTBearerDep +from airflow.api_fastapi.execution_api.security import CurrentTIToken from airflow.models.taskmap import TaskMap from airflow.models.xcom import XComModel from airflow.utils.db import get_query_count @@ -44,7 +44,7 @@ async def has_xcom_access( task_id: str, xcom_key: Annotated[str, Path(alias="key", min_length=1)], request: Request, - token=JWTBearerDep, + token=CurrentTIToken, ) -> bool: """Check if the task has access to the XCom.""" # TODO: Placeholder for actual implementation diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/security.py b/airflow-core/src/airflow/api_fastapi/execution_api/security.py new file mode 100644 index 0000000000000..215997d28d9c7 --- /dev/null +++ b/airflow-core/src/airflow/api_fastapi/execution_api/security.py @@ -0,0 +1,243 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Execution API security: JWT validation, token scopes, and route-level access control. + +Token types (``TokenType``): + +``"execution"`` + Default scope, accepted by all endpoints. Short-lived, automatically + refreshed by ``JWTReissueMiddleware``. + +``"workload"`` + Restricted scope, only accepted on routes that opt in via + ``Security(require_auth, scopes=["token:workload"])``. + +Tokens without a ``scope`` claim default to ``"execution"`` for backwards +compatibility (``claims.setdefault("scope", "execution")``). + +Enforcement flow: + 1. ``JWTBearer.__call__`` validates the JWT once per request (crypto + + signature verification), caching the result on the ASGI request scope. + Subsequent FastAPI dependency resolutions and Cadwyn replays return + the cache. + 2. ``require_auth`` is the Security dependency on routers. It receives + the token from ``JWTBearer`` and enforces: + - Token type against the route's ``allowed_token_types`` (precomputed + by ``ExecutionAPIRoute`` from ``token:*`` Security scopes). + - ``ti:self`` scope — checks that the JWT ``sub`` matches the + ``{task_instance_id}`` path parameter. + 3. ``ExecutionAPIRoute`` precomputes ``allowed_token_types`` from + ``token:*`` Security scopes at route registration time. Routes + without explicit ``token:*`` scopes default to execution-only. + +Why ``ExecutionAPIRoute`` is needed: + FastAPI resolves router-level ``Security()`` dependencies from outermost + to innermost. A ``token:workload`` scope on an inner endpoint would need + to *relax* the outer router's default execution-only restriction, but + ``SecurityScopes`` only accumulate additively — an outer dependency + cannot see scopes declared by inner ones. ``ExecutionAPIRoute`` solves + this by inspecting the **merged** dependency list at route registration + time (after ``include_router`` has combined all parent and child + dependencies) and precomputing the full ``allowed_token_types`` set. + ``require_auth`` then reads this precomputed set from the matched route + at request time, avoiding the ordering problem entirely. + + Any router whose routes need non-default token type policies must use + ``route_class=ExecutionAPIRoute``. Routers that only need the default + (execution-only) can use the standard route class — ``require_auth`` + falls back to ``{"execution"}`` when the attribute is absent. +""" + +# Disable future annotations in this file to work around https://github.com/fastapi/fastapi/issues/13056 +# ruff: noqa: I002 + +from typing import Any, Literal, get_args + +import structlog +from fastapi import Depends, HTTPException, Request, status +from fastapi.params import Security as SecurityParam +from fastapi.routing import APIRoute +from fastapi.security import HTTPBearer, SecurityScopes +from sqlalchemy import select + +from airflow.api_fastapi.auth.tokens import JWTValidator +from airflow.api_fastapi.common.db.common import AsyncSessionDep +from airflow.api_fastapi.execution_api.datamodels.token import TIToken +from airflow.api_fastapi.execution_api.deps import DepContainer + +log = structlog.get_logger(logger_name=__name__) + +TokenType = Literal["execution", "workload"] + +VALID_TOKEN_TYPES: frozenset[str] = frozenset(get_args(TokenType)) + +_REQUEST_SCOPE_TOKEN_KEY = "ti_token" + + +class JWTBearer(HTTPBearer): + """ + Validates JWT tokens for the Execution API. + + Performs cryptographic validation once per request and caches the result + on the ASGI request scope. Subsequent resolutions (FastAPI dependency + dedup or Cadwyn replays) return the cached token. + + This dependency handles ONLY crypto validation and token construction. + All route-specific authorization (token type, ti:self) is handled by + ``require_auth``. + """ + + def __init__(self, required_claims: dict[str, Any] | None = None): + super().__init__(auto_error=False) + self.required_claims = required_claims or {} + + async def __call__( # type: ignore[override] + self, + request: Request, + services=DepContainer, + ) -> TIToken | None: + # Return cached token (handles both FastAPI dependency dedup and Cadwyn replays). + if cached := request.scope.get(_REQUEST_SCOPE_TOKEN_KEY): + return cached + + # First resolution — full cryptographic validation. + creds = await super().__call__(request) + if not creds: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing auth token") + + validator: JWTValidator = await services.aget(JWTValidator) + + try: + claims = await validator.avalidated_claims(creds.credentials, dict(self.required_claims)) + except Exception as err: + log.warning("Failed to validate JWT", exc_info=True, token=creds.credentials) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Invalid auth token: {err}") + + claims.setdefault("scope", "execution") + + token = TIToken(id=claims["sub"], claims=claims) + request.scope[_REQUEST_SCOPE_TOKEN_KEY] = token + return token + + +_jwt_bearer = JWTBearer() + + +async def require_auth( + security_scopes: SecurityScopes, + request: Request, + token: TIToken = Depends(_jwt_bearer), +) -> TIToken: + """ + Security dependency that enforces token type and ``ti:self`` scope. + + Used via ``Security(require_auth)`` on routers. ``SecurityScopes`` are + accumulated by FastAPI from all parent ``Security()`` declarations. + + Token type enforcement reads ``route.allowed_token_types`` (precomputed + by ``ExecutionAPIRoute``) or defaults to ``{"execution"}``. + """ + token_scope = token.claims.get("scope", "execution") + + if token_scope not in VALID_TOKEN_TYPES: + log.warning("Invalid token scope in claims", token_scope=token_scope, path=request.url.path) + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Invalid token scope: {token_scope}", + ) + + route = request.scope.get("route") + allowed_token_types = getattr(route, "allowed_token_types", frozenset({"execution"})) + + if token_scope not in allowed_token_types: + log.warning( + "Token type not allowed for endpoint", + token_scope=token_scope, + allowed_types=sorted(allowed_token_types), + path=request.url.path, + ) + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Token type '{token_scope}' not allowed for this endpoint. " + f"Allowed types: {', '.join(sorted(allowed_token_types))}", + ) + + if "ti:self" in security_scopes.scopes: + ti_self_id = str(request.path_params["task_instance_id"]) + if str(token.id) != ti_self_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Token subject does not match task instance ID", + ) + + return token + + +CurrentTIToken: TIToken = Depends(require_auth) + + +class ExecutionAPIRoute(APIRoute): + """ + Custom route class that precomputes allowed token types from Security scopes. + + Scopes prefixed with ``token:`` (e.g., ``token:execution``, ``token:workload``) + are extracted at route registration time and stored as ``allowed_token_types``. + If no ``token:*`` scopes are declared, defaults to ``{"execution"}``. + + ``require_auth`` reads ``route.allowed_token_types`` at request time. + """ + + allowed_token_types: frozenset[str] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + all_scopes: set[str] = set() + for dep in self.dependencies: + if isinstance(dep, SecurityParam): + all_scopes.update(dep.scopes or []) + + token_scopes = {s.removeprefix("token:") for s in all_scopes if s.startswith("token:")} + + if token_scopes and not token_scopes <= VALID_TOKEN_TYPES: + invalid = token_scopes - VALID_TOKEN_TYPES + raise ValueError(f"Invalid token types in Security scopes: {invalid}") + + self.allowed_token_types = frozenset(token_scopes) if token_scopes else frozenset({"execution"}) + + +async def get_team_name_dep(session: AsyncSessionDep, token=CurrentTIToken) -> str | None: + """Return the team name associated to the task (if any).""" + from airflow.configuration import conf + from airflow.models import DagModel, TaskInstance + from airflow.models.dagbundle import DagBundleModel + from airflow.models.team import Team + + if not conf.getboolean("core", "multi_team"): + return None + + stmt = ( + select(Team.name) + .select_from(TaskInstance) + .join(DagModel, DagModel.dag_id == TaskInstance.dag_id) + .join(DagBundleModel, DagBundleModel.name == DagModel.bundle_name) + .join(DagBundleModel.teams) + .where(TaskInstance.id == token.id) + ) + return await session.scalar(stmt) diff --git a/airflow-core/src/airflow/api_fastapi/gunicorn_app.py b/airflow-core/src/airflow/api_fastapi/gunicorn_app.py index e52a681b006a9..c01d3e8b2aa14 100644 --- a/airflow-core/src/airflow/api_fastapi/gunicorn_app.py +++ b/airflow-core/src/airflow/api_fastapi/gunicorn_app.py @@ -30,14 +30,16 @@ from __future__ import annotations -import logging import signal import sys import time from typing import TYPE_CHECKING, Any +import structlog from gunicorn.app.base import BaseApplication from gunicorn.arbiter import Arbiter +from gunicorn.glogging import Logger as GunicornLogger +from uvicorn.workers import UvicornWorker from airflow.configuration import conf @@ -45,7 +47,39 @@ from fastapi import FastAPI from gunicorn.app.base import Application -log = logging.getLogger(__name__) +log = structlog.get_logger(__name__) + + +class AirflowGunicornLogger(GunicornLogger): + """ + Gunicorn logger that routes all output through Airflow's logging setup. + + Gunicorn's default Logger.setup() installs its own StreamHandler with a custom + formatter on ``gunicorn.error`` and ``gunicorn.access``, bypassing any root-level + handler (including our structlog ProcessorFormatter). Overriding setup() to do + nothing lets records propagate to root where Airflow's handler picks them up. + """ + + def setup(self, cfg) -> None: + self.error_log.propagate = True + self.access_log.propagate = True + + +class AirflowUvicornWorker(UvicornWorker): + """ + Uvicorn worker that preserves Airflow's structlog-based logging setup. + + Uvicorn workers normally call ``logging.config.dictConfig(LOGGING_CONFIG)`` on startup + which would override any structlog configuration applied before gunicorn starts. + Setting ``log_config=None`` prevents that. ``access_log=False`` disables uvicorn's + built-in access logger because ``HttpAccessLogMiddleware`` handles access logging. + """ + + CONFIG_KWARGS = { + **UvicornWorker.CONFIG_KWARGS, + "log_config": None, + "access_log": False, + } class AirflowArbiter(Arbiter): @@ -198,8 +232,7 @@ def run(self) -> None: try: AirflowArbiter(self).run() except RuntimeError as e: - print(f"\nError: {e}\n", file=sys.stderr) - sys.stderr.flush() + log.error("Gunicorn failed to start", error=str(e)) sys.exit(1) @@ -210,7 +243,6 @@ def create_gunicorn_app( worker_timeout: int, ssl_cert: str | None = None, ssl_key: str | None = None, - access_log: bool = True, log_level: str = "info", proxy_headers: bool = False, ) -> AirflowGunicornApp: @@ -223,18 +255,18 @@ def create_gunicorn_app( :param worker_timeout: Worker timeout in seconds :param ssl_cert: Path to SSL certificate file :param ssl_key: Path to SSL key file - :param access_log: Whether to enable access logging :param log_level: Log level (debug, info, warning, error, critical) :param proxy_headers: Whether to trust proxy headers """ options = { "bind": f"{host}:{port}", "workers": num_workers, - "worker_class": "uvicorn.workers.UvicornWorker", + "worker_class": "airflow.api_fastapi.gunicorn_app.AirflowUvicornWorker", "timeout": worker_timeout, "graceful_timeout": worker_timeout, "keepalive": worker_timeout, "loglevel": log_level, + "logger_class": "airflow.api_fastapi.gunicorn_app.AirflowGunicornLogger", "preload_app": True, # Use our gunicorn_config module for hooks (post_worker_init, worker_exit) "config": "python:airflow.api_fastapi.gunicorn_config", @@ -244,9 +276,6 @@ def create_gunicorn_app( options["certfile"] = ssl_cert options["keyfile"] = ssl_key - if access_log: - options["accesslog"] = "-" # Log to stdout - if proxy_headers: options["forwarded_allow_ips"] = "*" diff --git a/airflow-core/src/airflow/assets/manager.py b/airflow-core/src/airflow/assets/manager.py index 9c778d4a50e4d..9c287209172fe 100644 --- a/airflow-core/src/airflow/assets/manager.py +++ b/airflow-core/src/airflow/assets/manager.py @@ -348,10 +348,7 @@ def _queue_dagruns( if not dags_to_queue: return None - # TODO: AIP-76 there may be a better way to identify that timetable is partition-driven - # https://github.com/apache/airflow/issues/58445 - partition_dags = [x for x in dags_to_queue if x.timetable_summary == "Partitioned Asset"] - + partition_dags = [x for x in dags_to_queue if x.timetable_partitioned is True] cls._queue_partitioned_dags( asset_id=asset_id, partition_dags=partition_dags, @@ -361,7 +358,6 @@ def _queue_dagruns( ) non_partitioned_dags = dags_to_queue.difference(partition_dags) # don't double process - if not non_partitioned_dags: return None @@ -388,9 +384,8 @@ def _queue_partitioned_dags( ) -> None: if partition_dags and not partition_key: # TODO: AIP-76 how to best ensure users can see this? Probably add Log record. - # https://github.com/apache/airflow/issues/59060 log.warning( - "Listening dags are partition-aware but run has no partition key", + "Listening Dags are partition-aware but run has no partition key", listening_dags=[x.dag_id for x in partition_dags], asset_id=asset_id, run_id=event.source_run_id, diff --git a/airflow-core/src/airflow/cli/cli_config.py b/airflow-core/src/airflow/cli/cli_config.py index 7bc5b94f9bc92..8510eba318024 100644 --- a/airflow-core/src/airflow/cli/cli_config.py +++ b/airflow-core/src/airflow/cli/cli_config.py @@ -605,6 +605,16 @@ def string_lower_type(val): default="overwrite", choices=("overwrite", "fail", "skip"), ) +ARG_VAR_LIST_SHOW_VALUES = Arg( + ("--show-values",), + action="store_true", + help="Show variable values. By default only variable keys are listed.", +) +ARG_VAR_LIST_HIDE_SENSITIVE = Arg( + ("--hide-sensitive",), + action="store_true", + help="When used with --show-values, mask variable values.", +) # kerberos ARG_PRINCIPAL = Arg(("principal",), help="kerberos principal", nargs="?") @@ -812,6 +822,16 @@ def string_lower_type(val): required=False, action="store_true", ) +ARG_CONN_LIST_SHOW_VALUES = Arg( + ("--show-values",), + action="store_true", + help="Show connection values (host, login, URI, etc.). By default only connection IDs are listed.", +) +ARG_CONN_LIST_HIDE_SENSITIVE = Arg( + ("--hide-sensitive",), + action="store_true", + help="When used with --show-values, mask sensitive values (passwords, URI credentials, extra).", +) # providers ARG_PROVIDER_NAME = Arg( @@ -1429,7 +1449,7 @@ class GroupCommand(NamedTuple): name="list", help="List variables", func=lazy_load_command("airflow.cli.commands.variable_command.variables_list"), - args=(ARG_OUTPUT, ARG_VERBOSE), + args=(ARG_OUTPUT, ARG_VAR_LIST_SHOW_VALUES, ARG_VAR_LIST_HIDE_SENSITIVE, ARG_VERBOSE), ), ActionCommand( name="get", @@ -1604,7 +1624,7 @@ class GroupCommand(NamedTuple): name="list", help="List connections", func=lazy_load_command("airflow.cli.commands.connection_command.connections_list"), - args=(ARG_OUTPUT, ARG_VERBOSE), + args=(ARG_OUTPUT, ARG_CONN_LIST_SHOW_VALUES, ARG_CONN_LIST_HIDE_SENSITIVE, ARG_VERBOSE), ), ActionCommand( name="add", diff --git a/airflow-core/src/airflow/cli/commands/api_server_command.py b/airflow-core/src/airflow/cli/commands/api_server_command.py index dc7b4fabf9a3e..11b57305b1a66 100644 --- a/airflow-core/src/airflow/cli/commands/api_server_command.py +++ b/airflow-core/src/airflow/cli/commands/api_server_command.py @@ -18,7 +18,6 @@ from __future__ import annotations -import logging import os import sys import textwrap @@ -26,9 +25,9 @@ from functools import wraps from typing import TYPE_CHECKING, TypeVar +import structlog import uvicorn -from airflow import settings from airflow.cli.commands.daemon_utils import run_command_with_daemon_option from airflow.configuration import conf from airflow.exceptions import AirflowConfigException @@ -40,7 +39,7 @@ PS = ParamSpec("PS") RT = TypeVar("RT") -log = logging.getLogger(__name__) +log = structlog.get_logger(__name__) if TYPE_CHECKING: from argparse import Namespace @@ -68,19 +67,6 @@ def _run_api_server_with_gunicorn( ssl_cert, ssl_key = _get_ssl_cert_and_key_filepaths(args) log_level = conf.get("logging", "uvicorn_logging_level", fallback="info").lower() - access_log_enabled = log_level not in ("error", "critical", "fatal") - - log.info( - textwrap.dedent( - f"""\ - Running the API server with gunicorn: - Apps: {apps} - Workers: {num_workers} - Host: {args.host}:{args.port} - Timeout: {worker_timeout} - =================================================================""" - ) - ) gunicorn_app = create_gunicorn_app( host=args.host, @@ -89,7 +75,6 @@ def _run_api_server_with_gunicorn( worker_timeout=worker_timeout, ssl_cert=ssl_cert, ssl_key=ssl_key, - access_log=access_log_enabled, log_level=log_level, proxy_headers=proxy_headers, ) @@ -122,10 +107,7 @@ def _run_api_server_with_uvicorn( setproctitle(f"airflow api_server -- host:{args.host} port:{args.port}") - # Get uvicorn logging configuration from Airflow settings uvicorn_log_level = conf.get("logging", "uvicorn_logging_level", fallback="info").lower() - # Control access log based on uvicorn log level - disable for ERROR and above - access_log_enabled = uvicorn_log_level not in ("error", "critical", "fatal") uvicorn_kwargs = { "host": args.host, @@ -136,11 +118,13 @@ def _run_api_server_with_uvicorn( "timeout_worker_healthcheck": worker_timeout, "ssl_keyfile": ssl_key, "ssl_certfile": ssl_cert, - "access_log": access_log_enabled, + # HttpAccessLogMiddleware handles access logging; disable uvicorn's built-in access log. + "access_log": False, "log_level": uvicorn_log_level, "proxy_headers": proxy_headers, + # Prevent uvicorn from overriding our structlog-based logging setup. + "log_config": None, } - # Only set the log_config if it is provided, otherwise use the default uvicorn logging configuration. if args.log_config and args.log_config != "-": # The [api/log_config] is migrated from [api/access_logfile] and [api/access_logfile] defaults to "-" for stdout for Gunicorn. # So we need to check if the log_config is set to "-" or not; if it is set to "-", we regard it as not set. @@ -157,41 +141,50 @@ def _run_api_server(args, apps: str, num_workers: int, worker_timeout: int, prox """Run the API server using the configured server type.""" server_type = conf.get("api", "server_type", fallback="uvicorn").lower() + run = _run_api_server_with_uvicorn if server_type == "gunicorn": try: import gunicorn # noqa: F401 + + run = _run_api_server_with_gunicorn except ImportError: raise AirflowConfigException( "Gunicorn is not installed. Install it with: pip install 'apache-airflow-core[gunicorn]'" ) - _run_api_server_with_gunicorn( - args=args, + log_file = args.log_file or None + if conf.getboolean("logging", "json_logs", fallback=False): + extra = {"logfile": log_file} if log_file else {} + log.info( + "Running the API server", + server=server_type, apps=apps, - num_workers=num_workers, - worker_timeout=worker_timeout, - proxy_headers=proxy_headers, + workers=num_workers, + host=f"{args.host}:{args.port}", + timeout=worker_timeout, + **extra, ) else: - log.info( + print( textwrap.dedent( f"""\ - Running the API server with uvicorn: + Running the API server with {server_type}: Apps: {apps} Workers: {num_workers} Host: {args.host}:{args.port} Timeout: {worker_timeout} - Logfiles: {args.log_file or "-"} - =================================================================""" + Logfiles: {log_file or "-"} + =================================================================""", ) ) - _run_api_server_with_uvicorn( - args=args, - apps=apps, - num_workers=num_workers, - worker_timeout=worker_timeout, - proxy_headers=proxy_headers, - ) + + run( + args=args, + apps=apps, + num_workers=num_workers, + worker_timeout=worker_timeout, + proxy_headers=proxy_headers, + ) def with_api_apps_env(func: Callable[[Namespace], RT]) -> Callable[[Namespace], RT]: @@ -221,7 +214,7 @@ def wrapper(args: Namespace) -> RT: @with_api_apps_env def api_server(args: Namespace): """Start Airflow API server.""" - print(settings.HEADER) + cli_utils.print_banner() apps = args.apps num_workers = args.workers diff --git a/airflow-core/src/airflow/cli/commands/connection_command.py b/airflow-core/src/airflow/cli/commands/connection_command.py index 7ce4e60b2481c..8911bc80b99e2 100644 --- a/airflow-core/src/airflow/cli/commands/connection_command.py +++ b/airflow-core/src/airflow/cli/commands/connection_command.py @@ -30,7 +30,7 @@ from sqlalchemy.orm import exc from airflow.cli.simple_table import AirflowConsole -from airflow.cli.utils import is_stdout, print_export_output +from airflow.cli.utils import SENSITIVE_PLACEHOLDER, is_stdout, print_export_output from airflow.configuration import conf from airflow.exceptions import AirflowNotFoundException from airflow.models import Connection @@ -43,22 +43,84 @@ from airflow.utils.session import create_session +def _mask_uri_credentials(uri: str) -> str: + """ + Mask credentials in a URI while preserving structure. + + Examples:: + + postgresql://user:pass@host:5432/db -> postgresql://***:***@host:5432/db + mysql://host/db -> mysql://host/db (no credentials to mask) + """ + if not uri: + return uri + + try: + parsed = urlsplit(uri) + if not parsed.scheme: + return SENSITIVE_PLACEHOLDER + + if "@" in parsed.netloc: + _creds, host_port = parsed.netloc.split("@", 1) + masked_netloc = f"{SENSITIVE_PLACEHOLDER}:{SENSITIVE_PLACEHOLDER}@{host_port}" + return urlunsplit((parsed.scheme, masked_netloc, parsed.path, parsed.query, parsed.fragment)) + return uri + except Exception: + return SENSITIVE_PLACEHOLDER + + +class ConnectionDisplayMapper: + """Mapper class for formatting connection data for CLI display.""" + + @staticmethod + def full_details(conn: Connection) -> dict[str, Any]: + """Return complete connection details including all fields.""" + return { + "id": conn.id, + "conn_id": conn.conn_id, + "conn_type": conn.conn_type, + "description": conn.description, + "host": conn.host, + "schema": conn.schema, + "login": conn.login, + "password": conn.password, + "port": conn.port, + "is_encrypted": conn.is_encrypted, + "is_extra_encrypted": conn.is_encrypted, + "extra_dejson": conn.extra_dejson, + "get_uri": conn.get_uri(), + } + + @staticmethod + def ids_only(conn: Connection) -> dict[str, Any]: + """Return only connection identifiers (no sensitive values). Used by list by default.""" + return { + "conn_id": conn.conn_id, + "conn_type": conn.conn_type, + } + + @staticmethod + def masked_sensitive(conn: Connection) -> dict[str, Any]: + """Return full connection structure with password, extra, and URI credentials masked.""" + return { + "id": conn.id, + "conn_id": conn.conn_id, + "conn_type": conn.conn_type, + "description": conn.description, + "host": conn.host, + "schema": conn.schema, + "login": conn.login, + "password": SENSITIVE_PLACEHOLDER if conn.password else conn.password, + "port": conn.port, + "is_encrypted": conn.is_encrypted, + "is_extra_encrypted": conn.is_encrypted, + "extra_dejson": SENSITIVE_PLACEHOLDER if conn.extra_dejson else conn.extra_dejson, + "get_uri": _mask_uri_credentials(conn.get_uri()), + } + + def _connection_mapper(conn: Connection) -> dict[str, Any]: - return { - "id": conn.id, - "conn_id": conn.conn_id, - "conn_type": conn.conn_type, - "description": conn.description, - "host": conn.host, - "schema": conn.schema, - "login": conn.login, - "password": conn.password, - "port": conn.port, - "is_encrypted": conn.is_encrypted, - "is_extra_encrypted": conn.is_encrypted, - "extra_dejson": conn.extra_dejson, - "get_uri": conn.get_uri(), - } + return ConnectionDisplayMapper.full_details(conn) @suppress_logs_and_warning @@ -81,7 +143,25 @@ def connections_get(args): @suppress_logs_and_warning @providers_configuration_loaded def connections_list(args): - """List all connections at the command line.""" + """ + List all connections at the command line. + + By default only connection IDs and types are shown. Use --show-values to display + full connection details; use --hide-sensitive to mask passwords and URIs. + """ + show_values = getattr(args, "show_values", False) + hide_sensitive = getattr(args, "hide_sensitive", False) + + if hide_sensitive and not show_values: + raise SystemExit("--hide-sensitive can only be used with --show-values") + + if not show_values: + mapper = ConnectionDisplayMapper.ids_only + elif hide_sensitive: + mapper = ConnectionDisplayMapper.masked_sensitive + else: + mapper = ConnectionDisplayMapper.full_details + with create_session() as session: query = select(Connection) conns = session.scalars(query).all() @@ -89,7 +169,7 @@ def connections_list(args): AirflowConsole().print_as( data=conns, output=args.output, - mapper=_connection_mapper, + mapper=mapper, ) diff --git a/airflow-core/src/airflow/cli/commands/db_command.py b/airflow-core/src/airflow/cli/commands/db_command.py index f5c1a061a1a9f..9a2a199707d44 100644 --- a/airflow-core/src/airflow/cli/commands/db_command.py +++ b/airflow-core/src/airflow/cli/commands/db_command.py @@ -18,12 +18,12 @@ from __future__ import annotations -import logging import os import textwrap from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING +import structlog from packaging.version import InvalidVersion, parse as parse_version from tenacity import Retrying, stop_after_attempt, wait_fixed @@ -38,7 +38,7 @@ if TYPE_CHECKING: from tenacity import RetryCallState -log = logging.getLogger(__name__) +log = structlog.get_logger(__name__) @providers_configuration_loaded @@ -94,7 +94,7 @@ def run_db_migrate_command(args, command, revision_heads_map: dict[str, str]): :meta private: """ - print(f"DB: {settings.get_engine().url!r}") + db_url = str(settings.get_engine().url) if args.to_revision and args.to_version: raise SystemExit("Cannot supply both `--to-revision` and `--to-version`.") if args.from_version and args.from_revision: @@ -128,16 +128,16 @@ def run_db_migrate_command(args, command, revision_heads_map: dict[str, str]): to_revision = args.to_revision if not args.show_sql_only: - print(f"Performing upgrade to the metadata database {settings.get_engine().url!r}") + log.info("Performing upgrade to the metadata database", url=db_url) else: - print("Generating sql for upgrade -- upgrade commands will *not* be submitted.") + log.info("Generating sql for upgrade -- upgrade commands will *not* be submitted.") command( to_revision=to_revision, from_revision=from_revision, show_sql_only=args.show_sql_only, ) if not args.show_sql_only: - print("Database migration done!") + log.info("Database migration done!") def run_db_downgrade_command(args, command, revision_heads_map: dict[str, str]): @@ -171,10 +171,11 @@ def run_db_downgrade_command(args, command, revision_heads_map: dict[str, str]): raise SystemExit(f"Downgrading to version {args.to_version} is not supported.") elif args.to_revision: to_revision = args.to_revision + db_url = str(settings.get_engine().url) if not args.show_sql_only: - print(f"Performing downgrade with database {settings.get_engine().url!r}") + log.info("Performing downgrade with database", url=db_url) else: - print("Generating sql for downgrade -- downgrade commands will *not* be submitted.") + log.info("Generating sql for downgrade -- downgrade commands will *not* be submitted.") if args.show_sql_only or ( args.yes @@ -187,7 +188,7 @@ def run_db_downgrade_command(args, command, revision_heads_map: dict[str, str]): ): command(to_revision=to_revision, from_revision=from_revision, show_sql_only=args.show_sql_only) if not args.show_sql_only: - print("Downgrade complete") + log.info("Downgrade complete") else: raise SystemExit("Cancelled") diff --git a/airflow-core/src/airflow/cli/commands/kerberos_command.py b/airflow-core/src/airflow/cli/commands/kerberos_command.py index 827f2d6a9d062..05b4f860df527 100644 --- a/airflow-core/src/airflow/cli/commands/kerberos_command.py +++ b/airflow-core/src/airflow/cli/commands/kerberos_command.py @@ -18,7 +18,6 @@ from __future__ import annotations -from airflow import settings from airflow.cli.commands.daemon_utils import run_command_with_daemon_option from airflow.security import kerberos as krb from airflow.security.kerberos import KerberosMode @@ -30,7 +29,7 @@ @providers_configuration_loaded def kerberos(args): """Start a kerberos ticket renewer.""" - print(settings.HEADER) + cli_utils.print_banner() mode = KerberosMode.STANDARD if args.one_time: diff --git a/airflow-core/src/airflow/cli/commands/scheduler_command.py b/airflow-core/src/airflow/cli/commands/scheduler_command.py index f797d65625713..b79bc25fb016c 100644 --- a/airflow-core/src/airflow/cli/commands/scheduler_command.py +++ b/airflow-core/src/airflow/cli/commands/scheduler_command.py @@ -23,7 +23,6 @@ from contextlib import contextmanager from multiprocessing import Process -from airflow import settings from airflow.cli.commands.daemon_utils import run_command_with_daemon_option from airflow.configuration import conf from airflow.executors.executor_loader import ExecutorLoader @@ -53,7 +52,7 @@ def _run_scheduler_job(args) -> None: @providers_configuration_loaded def scheduler(args: Namespace): """Start Airflow Scheduler.""" - print(settings.HEADER) + cli_utils.print_banner() if args.only_idle and args.num_runs <= 0: raise SystemExit("The --only-idle flag requires --num-runs to be set to a positive number.") diff --git a/airflow-core/src/airflow/cli/commands/task_command.py b/airflow-core/src/airflow/cli/commands/task_command.py index 3cdd5c9f619a3..e615d63ccf437 100644 --- a/airflow-core/src/airflow/cli/commands/task_command.py +++ b/airflow-core/src/airflow/cli/commands/task_command.py @@ -31,10 +31,11 @@ from airflow._shared.timezones import timezone from airflow.cli.simple_table import AirflowConsole from airflow.cli.utils import fetch_dag_run_from_run_id_or_logical_date_string -from airflow.exceptions import AirflowConfigException, DagRunNotFound, TaskInstanceNotFound +from airflow.exceptions import AirflowConfigException, DagRunNotFound, NotMapped, TaskInstanceNotFound from airflow.models import TaskInstance from airflow.models.dag_version import DagVersion from airflow.models.dagrun import DagRun, get_or_create_dagrun +from airflow.models.expandinput import NotFullyPopulated from airflow.models.serialized_dag import SerializedDagModel from airflow.sdk.definitions.dag import DAG, _run_task from airflow.sdk.definitions.param import ParamsDict @@ -203,7 +204,18 @@ def _get_ti( f"TaskInstance for {dag.dag_id}, {task.task_id}, map={map_index} with " f"run_id or logical_date of {logical_date_or_run_id!r} not found" ) - # TODO: Validate map_index is in range? + if map_index >= 0: + try: + total = task.get_parse_time_mapped_ti_count() + if map_index >= total: + raise ValueError( + f"map_index {map_index} is out of range. " + f"Task '{task.task_id}' has {total} mapped instance(s) [0..{total - 1}]." + ) + except NotFullyPopulated: + pass # Dynamic mapping — cannot validate at parse time + except NotMapped: + raise ValueError(f"Task '{task.task_id}' is not mapped; map_index must be -1.") dag_version = DagVersion.get_latest_version(dag.dag_id, session=session) if not dag_version: # TODO: Remove this once DagVersion.get_latest_version is guaranteed to return a DagVersion/raise diff --git a/airflow-core/src/airflow/cli/commands/triggerer_command.py b/airflow-core/src/airflow/cli/commands/triggerer_command.py index ed8a08d1d730f..8b9ee178a19f8 100644 --- a/airflow-core/src/airflow/cli/commands/triggerer_command.py +++ b/airflow-core/src/airflow/cli/commands/triggerer_command.py @@ -23,7 +23,6 @@ from functools import partial from multiprocessing import Process -from airflow import settings from airflow.cli.commands.daemon_utils import run_command_with_daemon_option from airflow.configuration import conf from airflow.exceptions import AirflowConfigException @@ -68,7 +67,7 @@ def triggerer(args): SecretsMasker.enable_log_masking() - print(settings.HEADER) + cli_utils.print_banner() if args.queues and not conf.getboolean("triggerer", "queues_enabled", fallback=False): raise AirflowConfigException( "--queues option may only be used when triggerer.queues_enabled is `True`." diff --git a/airflow-core/src/airflow/cli/commands/variable_command.py b/airflow-core/src/airflow/cli/commands/variable_command.py index e20f549901b46..85166cb6e79d3 100644 --- a/airflow-core/src/airflow/cli/commands/variable_command.py +++ b/airflow-core/src/airflow/cli/commands/variable_command.py @@ -25,7 +25,7 @@ from sqlalchemy import select from airflow.cli.simple_table import AirflowConsole -from airflow.cli.utils import print_export_output +from airflow.cli.utils import SENSITIVE_PLACEHOLDER, print_export_output from airflow.exceptions import ( AirflowFileParseException, AirflowUnsupportedFileTypeException, @@ -39,13 +39,53 @@ from airflow.utils.session import create_session, provide_session +class VariableDisplayMapper: + """Mapper class for formatting variable data for CLI display.""" + + @staticmethod + def keys_only(var) -> dict[str, str]: + """Return only variable keys. Accepts Variable model or dict with 'key'.""" + key = var.key if hasattr(var, "key") else var["key"] + return {"key": key} + + @staticmethod + def with_values(var, hide_sensitive: bool = False) -> dict[str, str]: + """Return variable with value, optionally masked.""" + key = var.key if hasattr(var, "key") else var["key"] + raw = var.val if hasattr(var, "val") else var.get("val", var.get("_val")) + val = "" if raw is None else str(raw) + if hide_sensitive: + val = SENSITIVE_PLACEHOLDER + return {"key": key, "val": val} + + @suppress_logs_and_warning @providers_configuration_loaded def variables_list(args): - """Display all the variables.""" + """ + Display all the variables. + + By default only variable keys are shown. Use --show-values to display + values; use --hide-sensitive to mask all variable values (since individual + variables cannot be automatically classified as sensitive or not). + """ + show_values = getattr(args, "show_values", False) + hide_sensitive = getattr(args, "hide_sensitive", False) + + if hide_sensitive and not show_values: + raise SystemExit("--hide-sensitive can only be used with --show-values") + + def _mapper(var): + return VariableDisplayMapper.with_values(var, hide_sensitive) + with create_session() as session: - variables = session.scalars(select(Variable)).all() - AirflowConsole().print_as(data=variables, output=args.output, mapper=lambda x: {"key": x.key}) + if show_values: + variables = session.scalars(select(Variable)).all() + AirflowConsole().print_as(data=variables, output=args.output, mapper=_mapper) + else: + keys = session.scalars(select(Variable.key).distinct()).all() + variables = [{"key": key} for key in keys] + AirflowConsole().print_as(data=variables, output=args.output, mapper=None) @suppress_logs_and_warning diff --git a/airflow-core/src/airflow/cli/utils.py b/airflow-core/src/airflow/cli/utils.py index b221521e01d7e..870f045071b7d 100644 --- a/airflow-core/src/airflow/cli/utils.py +++ b/airflow-core/src/airflow/cli/utils.py @@ -20,6 +20,9 @@ import sys from typing import TYPE_CHECKING +# Placeholder for masking sensitive values in CLI output +SENSITIVE_PLACEHOLDER = "***" + if TYPE_CHECKING: import datetime from collections.abc import Collection diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml index 7c66eb3938f03..0c002f5276cfe 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -832,6 +832,16 @@ logging: type: string example: ~ default: "WARNING" + json_logs: + description: | + Enable JSON-structured logging for all output. When ``True``, every log line emitted by + the process is a single-line JSON object (including HTTP access logs from the API server). + Recommended for production / cloud-native deployments where logs are collected by a log + aggregator. + version_added: "3.2.0" + type: boolean + example: "True" + default: "False" uvicorn_logging_level: description: | Logging level for uvicorn (API server and serve-logs). @@ -917,11 +927,24 @@ logging: description: | Determines the formatter class used by Airflow for structuring its log messages The default formatter class is timezone-aware, which means that timestamps attached to log entries - will be adjusted to reflect the local timezone of the Airflow instance + will be adjusted to reflect the local timezone of the Airflow instance. + Note: This setting does NOT affect component logs (scheduler, api-server, triggerer, etc.) since + those use structlog directly. Use ``log_timestamp_format`` to control timestamps for those logs. version_added: 2.3.4 type: string example: ~ default: "airflow.utils.log.timezone_aware.TimezoneAware" + log_timestamp_format: + description: | + Timestamp format for component logs (scheduler, api-server, triggerer, etc.). + Use ``iso`` for ISO 8601 format (default), or a Python strftime format string + such as ``%Y-%m-%d %H:%M:%S``. + Note: This setting only applies to component logs rendered via structlog. + Task/DAG logs use ``log_format`` instead. + version_added: 3.0.4 + type: string + example: "%Y-%m-%d %H:%M:%S" + default: "iso" secret_mask_adapter: description: | An import path to a function to add adaptations of each secret added with @@ -1414,9 +1437,21 @@ traces: description: | If True, then traces from Airflow internal methods are exported. Defaults to False. version_added: 3.1.0 + version_deprecated: 3.2.0 + deprecation_reason: | + This parameter is no longer used. type: string example: ~ default: "False" + task_runner_flush_timeout_milliseconds: + description: | + Timeout in milliseconds to wait for the OpenTelemetry span exporter to flush pending spans + when a task runner process exits. If the exporter does not finish within this time, any + buffered spans may be dropped. + version_added: 3.2.0 + type: integer + example: ~ + default: "30000" secrets: description: ~ options: @@ -1435,6 +1470,10 @@ secrets: Example for AWS Systems Manager ParameterStore: ``{"connections_prefix": "/airflow/connections", "profile_name": "default"}`` + + You can also set individual kwargs via ``AIRFLOW__SECRETS__BACKEND_KWARG__=value`` + environment variables. Per-key variables override the same key in this JSON setting. + Values are raw strings (not JSON-parsed). version_added: 1.10.10 type: string sensitive: true @@ -1480,7 +1519,9 @@ api: theme: description: | JSON config to customize the Chakra UI theme. - Currently only supports ``brand`` color customization and ``globalCss``. + Supports ``brand`` color customization, ``globalCss``, and optional navigation icons ``icon`` + (for light mode) and ``icon_dark_mode`` (for dark mode). Icons must be SVG files and can be + either absolute http(s) URLs or app-relative paths starting with ``/``. Must supply ``50``-``950`` OKLCH color values for ``brand`` color. @@ -1793,6 +1834,10 @@ workers: Example for AWS Systems Manager ParameterStore: ``{"connections_prefix": "/airflow/connections", "profile_name": "default"}`` + + You can also set individual kwargs via ``AIRFLOW__WORKERS__SECRETS_BACKEND_KWARG__=value`` + environment variables. Per-key variables override the same key in this JSON setting. + Values are raw strings (not JSON-parsed). version_added: 3.0.0 type: string sensitive: true diff --git a/airflow-core/src/airflow/configuration.py b/airflow-core/src/airflow/configuration.py index 556821d71fb0e..69d936be01d82 100644 --- a/airflow-core/src/airflow/configuration.py +++ b/airflow-core/src/airflow/configuration.py @@ -491,6 +491,7 @@ def _validate_sqlite3_version(self): ) def mask_secrets(self): + from airflow._shared.configuration.parser import _build_kwarg_env_prefix, _collect_kwarg_env_vars from airflow._shared.secrets_masker import mask_secret as mask_secret_core from airflow.sdk.log import mask_secret as mask_secret_sdk @@ -508,6 +509,17 @@ def mask_secrets(self): mask_secret_core(value) mask_secret_sdk(value) + # Mask per-key backend kwarg env vars (AIRFLOW__SECRETS__BACKEND_KWARG__* etc.). + # These are not in sensitive_config_values but may contain sensitive values. + for _section, _kwargs_key in [ + ("secrets", "backend_kwargs"), + ("workers", "secrets_backend_kwargs"), + ]: + _prefix = _build_kwarg_env_prefix(_section, _kwargs_key) + for _value in _collect_kwarg_env_vars(_prefix).values(): + mask_secret_core(_value) + mask_secret_sdk(_value) + def load_test_config(self): """ Use test configuration rather than the configuration coming from airflow defaults. diff --git a/airflow-core/src/airflow/dag_processing/collection.py b/airflow-core/src/airflow/dag_processing/collection.py index 5ea7d83315110..7754080ae803f 100644 --- a/airflow-core/src/airflow/dag_processing/collection.py +++ b/airflow-core/src/airflow/dag_processing/collection.py @@ -84,12 +84,17 @@ def _create_orm_dags( bundle_name: str, + bundle_version: str | None, dags: Iterable[LazyDeserializedDAG], *, session: Session, ) -> Iterator[DagModel]: for dag in dags: - orm_dag = DagModel(dag_id=dag.dag_id, bundle_name=bundle_name) + orm_dag = DagModel( + dag_id=dag.dag_id, + bundle_name=bundle_name, + bundle_version=bundle_version, + ) if dag.is_paused_upon_creation is not None: orm_dag.is_paused = dag.is_paused_upon_creation log.info("Creating ORM DAG for %s", dag.dag_id) @@ -529,6 +534,7 @@ def add_dags(self, *, session: Session) -> dict[str, DagModel]: (model.dag_id, model) for model in _create_orm_dags( bundle_name=self.bundle_name, + bundle_version=self.bundle_version, dags=(dag for dag_id, dag in self.dags.items() if dag_id not in orm_dags), session=session, ) diff --git a/airflow-core/src/airflow/dag_processing/manager.py b/airflow-core/src/airflow/dag_processing/manager.py index 6131248980e9d..5ad3618048f13 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -1159,10 +1159,11 @@ def _kill_timed_out_processors(self): duration = now - processor.start_time if duration > self.processor_timeout: self.log.error( - "Processor for %s with PID %s started %d ago killing it.", + "Processor for %s with PID %s has been running for %.2f seconds, exceeding the timeout of %.2f seconds. Killing it!", file, processor.pid, duration, + self.processor_timeout, ) file_name = str(file.rel_path) Stats.decr("dag_processing.processes", tags={"file_path": file_name, "action": "timeout"}) diff --git a/airflow-core/src/airflow/example_dags/example_asset_partition.py b/airflow-core/src/airflow/example_dags/example_asset_partition.py index 69c9d883e63ad..ecc9d7427af0c 100644 --- a/airflow-core/src/airflow/example_dags/example_asset_partition.py +++ b/airflow-core/src/airflow/example_dags/example_asset_partition.py @@ -21,6 +21,7 @@ from airflow.sdk import ( DAG, + AllowedKeyMapper, Asset, CronPartitionTimetable, DailyMapper, @@ -184,3 +185,43 @@ def aggregate_sales(dag_run=None): print(dag_run.partition_key) aggregate_sales() + + +region_raw_stats = Asset(uri="file://incoming/player-stats/by-region.csv", name="region_raw_stats") + + +with DAG( + dag_id="ingest_region_stats", + schedule=None, + tags=["player-stats", "regional"], +): + """ + Ingest player statistics per region. + + Externally triggered with partition_key set to a region code (``us``, ``eu``, ``apac``). + """ + + @task(outlets=[region_raw_stats]) + def ingest_region(): + """Materialize player statistics for a single region partition.""" + pass + + ingest_region() + + +@asset( + uri="file://analytics/player-stats/regional-breakdown.csv", + schedule=PartitionedAssetTimetable( + assets=region_raw_stats, + default_partition_mapper=AllowedKeyMapper(["us", "eu", "apac"]), + ), + tags=["player-stats", "regional"], +) +def regional_stats_breakdown(): + """ + Aggregate regional player statistics. + + This asset demonstrates AllowedKeyMapper, which validates that upstream partition + keys belong to a fixed set of allowed values (``us``, ``eu``, ``apac``) rather than time-based partitions. + """ + pass diff --git a/airflow-core/src/airflow/example_dags/example_xcom.py b/airflow-core/src/airflow/example_dags/example_xcom.py index e17ad56d8cab1..304e7fa0c6569 100644 --- a/airflow-core/src/airflow/example_dags/example_xcom.py +++ b/airflow-core/src/airflow/example_dags/example_xcom.py @@ -77,12 +77,22 @@ def pull_value_from_bash_push(ti=None): 'echo "value_by_return"', ) + # This example shows a safe way of passing XCom values to a BashOperator via environment variables. + # The values are templated into the bash_command and then set as environment variables for the + # command to use. This is a recommended pattern for passing XCom values to BashOperator, as it avoids + # issues with quoting and escaping that can arise when trying to directly template XCom values + # into. bash_pull = BashOperator( task_id="bash_pull", bash_command='echo "bash pull demo" && ' - f'echo "The xcom pushed manually is {XComArg(bash_push, key="manually_pushed_value")}" && ' - f'echo "The returned_value xcom is {XComArg(bash_push)}" && ' + "echo \"The xcom pushed manually is '$MANUALLY_PUSHED_VALUE'\" && " + "echo \"The returned_value xcom is '$RETURNED_VALUE'\" && " 'echo "finished"', + env={ + "MANUALLY_PUSHED_VALUE": str(XComArg(bash_push, key="manually_pushed_value")), + "RETURNED_VALUE": str(XComArg(bash_push)), + }, + append_env=True, do_xcom_push=False, ) diff --git a/airflow-core/src/airflow/executors/base_executor.py b/airflow-core/src/airflow/executors/base_executor.py index 2997d55d8bb3b..d67c25c7bafaa 100644 --- a/airflow-core/src/airflow/executors/base_executor.py +++ b/airflow-core/src/airflow/executors/base_executor.py @@ -32,14 +32,11 @@ from airflow.configuration import conf from airflow.executors import workloads from airflow.executors.executor_loader import ExecutorLoader -from airflow.executors.workloads.task import TaskInstanceDTO from airflow.models import Log from airflow.models.callback import CallbackKey from airflow.observability.metrics import stats_utils -from airflow.observability.trace import Trace from airflow.utils.log.logging_mixin import LoggingMixin from airflow.utils.state import TaskInstanceState -from airflow.utils.thread_safe_dict import ThreadSafeDict PARALLELISM: int = conf.getint("core", "PARALLELISM") @@ -143,8 +140,6 @@ class BaseExecutor(LoggingMixin): :param parallelism: how many jobs should run at one time. """ - active_spans = ThreadSafeDict() - supports_ad_hoc_ti_run: bool = False supports_callbacks: bool = False supports_multi_team: bool = False @@ -217,10 +212,6 @@ def __repr__(self): _repr += ")" return _repr - @classmethod - def set_active_spans(cls, active_spans: ThreadSafeDict): - cls.active_spans = active_spans - def start(self): # pragma: no cover """Executors may need to get things started.""" @@ -340,17 +331,6 @@ def _emit_metrics(self, open_slots, num_running_tasks, num_queued_tasks): queued_tasks_metric_name = self._get_metric_name("executor.queued_tasks") running_tasks_metric_name = self._get_metric_name("executor.running_tasks") - span = Trace.get_current_span() - if span.is_recording(): - span.add_event( - name="executor", - attributes={ - open_slots_metric_name: open_slots, - queued_tasks_metric_name: num_queued_tasks, - running_tasks_metric_name: num_running_tasks, - }, - ) - self.log.debug("%s running task instances for executor %s", num_running_tasks, name) self.log.debug("%s in queue for executor %s", num_queued_tasks, name) if open_slots == 0: @@ -415,30 +395,6 @@ def trigger_tasks(self, open_slots: int) -> None: if key in self.attempts: del self.attempts[key] - if isinstance(workload, workloads.ExecuteTask) and hasattr(workload, "ti"): - ti = workload.ti - - # If it's None, then the span for the current id hasn't been started. - if self.active_spans is not None and self.active_spans.get("ti:" + str(ti.id)) is None: - if isinstance(ti, TaskInstanceDTO): - parent_context = Trace.extract(ti.parent_context_carrier) - else: - parent_context = Trace.extract(ti.dag_run.context_carrier) - # Start a new span using the context from the parent. - # Attributes will be set once the task has finished so that all - # values will be available (end_time, duration, etc.). - - span = Trace.start_child_span( - span_name=f"{ti.task_id}", - parent_context=parent_context, - component="task", - start_as_current=False, - ) - self.active_spans.set("ti:" + str(ti.id), span) - # Inject the current context into the carrier. - carrier = Trace.inject() - ti.context_carrier = carrier - workload_list.append(workload) if workload_list: diff --git a/airflow-core/src/airflow/executors/workloads/callback.py b/airflow-core/src/airflow/executors/workloads/callback.py index 2563f9a78f553..273c55953675b 100644 --- a/airflow-core/src/airflow/executors/workloads/callback.py +++ b/airflow-core/src/airflow/executors/workloads/callback.py @@ -90,7 +90,7 @@ def make( name=dag_run.dag_model.bundle_name, version=dag_run.bundle_version, ) - fname = f"executor_callbacks/{callback.id}" # TODO: better log file template + fname = f"executor_callbacks/{dag_run.dag_id}/{dag_run.run_id}/{callback.id}" return cls( callback=CallbackDTO.model_validate(callback, from_attributes=True), diff --git a/airflow-core/src/airflow/executors/workloads/task.py b/airflow-core/src/airflow/executors/workloads/task.py index d691dcb6f0968..a5939cf424412 100644 --- a/airflow-core/src/airflow/executors/workloads/task.py +++ b/airflow-core/src/airflow/executors/workloads/task.py @@ -86,7 +86,7 @@ def make( from airflow.utils.helpers import log_filename_template_renderer ser_ti = TaskInstanceDTO.model_validate(ti, from_attributes=True) - ser_ti.parent_context_carrier = ti.dag_run.context_carrier + ser_ti.context_carrier = ti.dag_run.context_carrier if not bundle_info: bundle_info = BundleInfo( name=ti.dag_model.bundle_name, diff --git a/airflow-core/src/airflow/jobs/scheduler_job_runner.py b/airflow-core/src/airflow/jobs/scheduler_job_runner.py index 367447968c2e0..b078cb183bc9c 100644 --- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py +++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py @@ -32,20 +32,8 @@ from functools import lru_cache, partial from itertools import groupby from typing import TYPE_CHECKING, Any -from uuid import UUID - -from sqlalchemy import ( - and_, - delete, - exists, - func, - inspect, - or_, - select, - text, - tuple_, - update, -) + +from sqlalchemy import CTE, and_, delete, exists, func, inspect, or_, select, text, tuple_, update from sqlalchemy.exc import OperationalError from sqlalchemy.orm import joinedload, lazyload, load_only, make_transient, selectinload from sqlalchemy.sql import expression @@ -58,7 +46,6 @@ from airflow.assets.evaluation import AssetEvaluator from airflow.callbacks.callback_requests import ( DagCallbackRequest, - DagRunContext, EmailRequest, TaskCallbackRequest, ) @@ -99,17 +86,14 @@ from airflow.models.team import Team from airflow.models.trigger import TRIGGER_FAIL_REPR, Trigger, TriggerFailureReason from airflow.observability.metrics import stats_utils -from airflow.observability.trace import Trace from airflow.serialization.definitions.assets import SerializedAssetUniqueKey from airflow.serialization.definitions.notset import NOTSET from airflow.ti_deps.dependencies_states import EXECUTION_STATES from airflow.timetables.simple import AssetTriggeredTimetable -from airflow.utils.dates import datetime_to_nano from airflow.utils.event_scheduler import EventScheduler from airflow.utils.log.logging_mixin import LoggingMixin from airflow.utils.retries import MAX_DB_RETRIES, retry_db_transaction, run_with_db_retries from airflow.utils.session import NEW_SESSION, create_session, provide_session -from airflow.utils.span_status import SpanStatus from airflow.utils.sqlalchemy import ( get_dialect_name, is_lock_not_available_error, @@ -117,7 +101,6 @@ with_row_locks, ) from airflow.utils.state import CallbackState, DagRunState, State, TaskInstanceState -from airflow.utils.thread_safe_dict import ThreadSafeDict from airflow.utils.types import DagRunTriggeredByType, DagRunType if TYPE_CHECKING: @@ -168,6 +151,41 @@ def _eager_load_dag_run_for_validation() -> tuple[LoaderOption, LoaderOption]: ) +def _ensure_ti_has_dag_version_id(ti: TaskInstance, session: Session, log: Logger) -> bool: + """ + Ensure a TaskInstance has a valid dag_version_id for Pydantic serialisation. + + Legacy tasks migrated from Airflow 2 may have dag_version_id = None. + The Pydantic TaskInstance datamodel requires dag_version_id to be a strict + uuid.UUID, so we must backfill it before constructing TaskCallbackRequest + or EmailRequest. + + Returns True if dag_version_id is present (or was successfully backfilled), + False if it could not be resolved (caller should skip the callback). + """ + if ti.dag_version_id is not None: + return True + + latest_version = DagVersion.get_latest_version(ti.dag_id, session=session) + if latest_version is None: + log.warning( + "TaskInstance %s has no dag_version_id and no DagVersion could be found " + "for dag_id=%s. Skipping callback. " + "This can happen for tasks migrated from Airflow 2 with no subsequent DAG parse.", + ti, + ti.dag_id, + ) + return False + + ti.dag_version_id = latest_version.id + log.info( + "Backfilled dag_version_id for legacy TaskInstance %s from latest DagVersion %s.", + ti, + latest_version.id, + ) + return True + + class ConcurrencyMap: """ Dataclass to represent concurrency maps. @@ -239,14 +257,6 @@ class SchedulerJobRunner(BaseJobRunner, LoggingMixin): job_type = "SchedulerJob" - # For a dagrun span - # - key: dag_run.run_id | value: span - # - dagrun keys will be prefixed with 'dr:'. - # For a ti span - # - key: ti.id | value: span - # - taskinstance keys will be prefixed with 'ti:'. - active_spans = ThreadSafeDict() - def __init__( self, job: Job, @@ -400,9 +410,6 @@ def _get_workload_team_name(self, workload: SchedulerWorkload, session: Session) def _exit_gracefully(self, signum: int, frame: FrameType | None) -> None: """Clean up processor_agent to avoid leaving orphan processes.""" - if self._is_tracing_enabled(): - self._end_active_spans() - if not _is_parent_process(): # Only the parent process should perform the cleanup. return @@ -804,7 +811,11 @@ def _executable_task_instances_to_queued(self, max_tis: int, session: Session) - task_instance, ) starved_tasks_task_dagrun_concurrency.add( - (task_instance.dag_id, task_instance.run_id, task_instance.task_id) + ( + task_instance.dag_id, + task_instance.run_id, + task_instance.task_id, + ) ) continue @@ -1277,18 +1288,6 @@ def process_executor_events( ti.pid, ) - if (active_ti_span := cls.active_spans.get("ti:" + str(ti.id))) is not None: - cls.set_ti_span_attrs(span=active_ti_span, state=state, ti=ti) - # End the span and remove it from the active_spans dict. - active_ti_span.end(end_time=datetime_to_nano(ti.end_date)) - cls.active_spans.delete("ti:" + str(ti.id)) - ti.span_status = SpanStatus.ENDED - else: - if ti.span_status == SpanStatus.ACTIVE: - # Another scheduler has started the span. - # Update the SpanStatus to let the process know that it must end it. - ti.span_status = SpanStatus.SHOULD_END - # There are two scenarios why the same TI with the same try_number is queued # after executor is finished with it: # 1) the TI was killed externally and it had no time to mark itself failed @@ -1345,10 +1344,20 @@ def process_executor_events( # Only log the error/extra info here, since the `ti.handle_failure()` path will log it # too, which would lead to double logging cls.logger().error(msg) + # Safely extract bundle info: prefer dag_version when available, + # fall back to dag_model/dag_run for legacy tasks migrated from + # Airflow 2 where dag_version may be None (AIP-66). + _bundle_name = ti.dag_version.bundle_name if ti.dag_version else ti.dag_model.bundle_name + _bundle_version = ( + ti.dag_version.bundle_version if ti.dag_version else ti.dag_run.bundle_version + ) + # Backfill dag_version_id for legacy tasks (Pydantic requires uuid.UUID). + if not _ensure_ti_has_dag_version_id(ti, session, cls.logger()): + continue request = TaskCallbackRequest( filepath=ti.dag_model.relative_fileloc or "", - bundle_name=ti.dag_version.bundle_name, - bundle_version=ti.dag_version.bundle_version, + bundle_name=_bundle_name, + bundle_version=_bundle_version, ti=ti, msg=msg, task_callback_type=( @@ -1382,10 +1391,21 @@ def process_executor_events( "Sending email request for task %s to DAG Processor", ti, ) + # Safely extract bundle info with fallback for legacy tasks + # (dag_version may be None after Airflow 2 → 3 migration). + _email_bundle_name = ( + ti.dag_version.bundle_name if ti.dag_version else ti.dag_model.bundle_name + ) + _email_bundle_version = ( + ti.dag_version.bundle_version if ti.dag_version else ti.dag_run.bundle_version + ) + # Backfill dag_version_id for legacy tasks (Pydantic requires uuid.UUID). + if not _ensure_ti_has_dag_version_id(ti, session, cls.logger()): + continue email_request = EmailRequest( filepath=ti.dag_model.relative_fileloc or "", - bundle_name=ti.dag_version.bundle_name, - bundle_version=ti.dag_version.bundle_version, + bundle_name=_email_bundle_name, + bundle_version=_email_bundle_version, ti=ti, msg=msg, email_type="retry" if ti.is_eligible_to_retry() else "failure", @@ -1404,39 +1424,6 @@ def process_executor_events( return len(event_buffer) - @classmethod - def set_ti_span_attrs(cls, span, state, ti): - span.set_attributes( - { - "airflow.category": "scheduler", - "airflow.task.id": ti.id, - "airflow.task.task_id": ti.task_id, - "airflow.task.dag_id": ti.dag_id, - "airflow.task.state": ti.state, - "airflow.task.error": state == TaskInstanceState.FAILED, - "airflow.task.start_date": str(ti.start_date), - "airflow.task.end_date": str(ti.end_date), - "airflow.task.duration": ti.duration, - "airflow.task.executor_config": str(ti.executor_config), - "airflow.task.logical_date": str(ti.logical_date), - "airflow.task.hostname": ti.hostname, - "airflow.task.log_url": ti.log_url, - "airflow.task.operator": str(ti.operator), - "airflow.task.try_number": ti.try_number, - "airflow.task.executor_state": state, - "airflow.task.pool": ti.pool, - "airflow.task.queue": ti.queue, - "airflow.task.priority_weight": ti.priority_weight, - "airflow.task.queued_dttm": str(ti.queued_dttm), - "airflow.task.queued_by_job_id": ti.queued_by_job_id, - "airflow.task.pid": ti.pid, - } - ) - if span.is_recording(): - span.add_event(name="airflow.task.queued", timestamp=datetime_to_nano(ti.queued_dttm)) - span.add_event(name="airflow.task.started", timestamp=datetime_to_nano(ti.start_date)) - span.add_event(name="airflow.task.ended", timestamp=datetime_to_nano(ti.end_date)) - def _execute(self) -> int | None: import os @@ -1460,12 +1447,6 @@ def _execute(self) -> int | None: executor.start() # local import due to type_checking. - from airflow.executors.base_executor import BaseExecutor - - # Pass a reference to the dictionary. - # Any changes made by a dag_run instance, will be reflected to the dictionary of this class. - DagRun.set_active_spans(active_spans=self.active_spans) - BaseExecutor.set_active_spans(active_spans=self.active_spans) stats_factory = stats_utils.get_stats_factory(Stats) Stats.initialize(factory=stats_factory) @@ -1516,162 +1497,6 @@ def _update_dag_run_state_for_paused_dags(self, session: Session = NEW_SESSION) except Exception as e: # should not fail the scheduler self.log.exception("Failed to update dag run state for paused dags due to %s", e) - @provide_session - def _end_active_spans(self, session: Session = NEW_SESSION): - # No need to do a commit for every update. The annotation will commit all of them once at the end. - for prefixed_key, span in self.active_spans.get_all().items(): - # Use partition to split on the first occurrence of ':'. - prefix, sep, key = prefixed_key.partition(":") - - if prefix == "ti": - ti_result = session.get(TaskInstance, UUID(key)) - if ti_result is None: - continue - ti: TaskInstance = ti_result - - if ti.state in State.finished: - self.set_ti_span_attrs(span=span, state=ti.state, ti=ti) - span.end(end_time=datetime_to_nano(ti.end_date)) - ti.span_status = SpanStatus.ENDED - else: - span.end() - ti.span_status = SpanStatus.NEEDS_CONTINUANCE - elif prefix == "dr": - dag_run: DagRun | None = session.scalars( - select(DagRun).where(DagRun.id == int(key)) - ).one_or_none() - if dag_run is None: - continue - if dag_run.state in State.finished_dr_states: - dag_run.set_dagrun_span_attrs(span=span) - - span.end(end_time=datetime_to_nano(dag_run.end_date)) - dag_run.span_status = SpanStatus.ENDED - else: - span.end() - dag_run.span_status = SpanStatus.NEEDS_CONTINUANCE - initial_dag_run_context = Trace.extract(dag_run.context_carrier) - with Trace.start_child_span( - span_name="current_scheduler_exited", parent_context=initial_dag_run_context - ) as s: - s.set_attribute("trace_status", "needs continuance") - else: - self.log.error("Found key with unknown prefix: '%s'", prefixed_key) - - # Even if there is a key with an unknown prefix, clear the dict. - # If this method has been called, the scheduler is exiting. - self.active_spans.clear() - - def _end_spans_of_externally_ended_ops(self, session: Session): - # The scheduler that starts a dag_run or a task is also the one that starts the spans. - # Each scheduler should end the spans that it has started. - # - # Otel spans are implemented in a certain way so that the objects - # can't be shared between processes or get recreated. - # It is done so that the process that starts a span, is also the one that ends it. - # - # If another scheduler has finished processing a dag_run or a task and there is a reference - # on the active_spans dictionary, then the current scheduler started the span, - # and therefore must end it. - dag_runs_should_end: list[DagRun] = list( - session.scalars(select(DagRun).where(DagRun.span_status == SpanStatus.SHOULD_END)) - ) - tis_should_end: list[TaskInstance] = list( - session.scalars(select(TaskInstance).where(TaskInstance.span_status == SpanStatus.SHOULD_END)) - ) - - for dag_run in dag_runs_should_end: - active_dagrun_span = self.active_spans.get("dr:" + str(dag_run.id)) - if active_dagrun_span is not None: - if dag_run.state in State.finished_dr_states: - dag_run.set_dagrun_span_attrs(span=active_dagrun_span) - - active_dagrun_span.end(end_time=datetime_to_nano(dag_run.end_date)) - else: - active_dagrun_span.end() - self.active_spans.delete("dr:" + str(dag_run.id)) - dag_run.span_status = SpanStatus.ENDED - - for ti in tis_should_end: - active_ti_span = self.active_spans.get(f"ti:{ti.id}") - if active_ti_span is not None: - if ti.state in State.finished: - self.set_ti_span_attrs(span=active_ti_span, state=ti.state, ti=ti) - active_ti_span.end(end_time=datetime_to_nano(ti.end_date)) - else: - active_ti_span.end() - self.active_spans.delete(f"ti:{ti.id}") - ti.span_status = SpanStatus.ENDED - - def _recreate_unhealthy_scheduler_spans_if_needed(self, dag_run: DagRun, session: Session): - # There are two scenarios: - # 1. scheduler is unhealthy but managed to update span_status - # 2. scheduler is unhealthy and didn't manage to make any updates - # Check the span_status first, in case the 2nd db query can be avoided (scenario 1). - - # If the dag_run is scheduled by a different scheduler, and it's still running and the span is active, - # then check the Job table to determine if the initial scheduler is still healthy. - if ( - dag_run.scheduled_by_job_id != self.job.id - and dag_run.state in State.unfinished_dr_states - and dag_run.span_status == SpanStatus.ACTIVE - ): - initial_scheduler_id = dag_run.scheduled_by_job_id - job: Job | None = session.scalars( - select(Job).where( - Job.id == initial_scheduler_id, - Job.job_type == "SchedulerJob", - ) - ).one_or_none() - if job is None: - return - - if not job.is_alive(): - # Start a new span for the dag_run. - dr_span = Trace.start_root_span( - span_name=f"{dag_run.dag_id}_recreated", - component="dag", - start_time=dag_run.queued_at, - start_as_current=False, - ) - carrier = Trace.inject() - # Update the context_carrier and leave the SpanStatus as ACTIVE. - dag_run.context_carrier = carrier - self.active_spans.set("dr:" + str(dag_run.id), dr_span) - - tis = dag_run.get_task_instances(session=session) - - # At this point, any tis will have been adopted by the current scheduler, - # and ti.queued_by_job_id will point to the current id. - # Any tis that have been executed by the unhealthy scheduler, will need a new span - # so that it can be associated with the new dag_run span. - tis_needing_spans = [ - ti - for ti in tis - # If it has started and there is a reference on the active_spans dict, - # then it was started by the current scheduler. - if ti.start_date is not None and self.active_spans.get(f"ti:{ti.id}") is None - ] - - dr_context = Trace.extract(dag_run.context_carrier) - for ti in tis_needing_spans: - ti_span = Trace.start_child_span( - span_name=f"{ti.task_id}_recreated", - parent_context=dr_context, - start_time=ti.queued_dttm, - start_as_current=False, - ) - ti_carrier = Trace.inject() - ti.context_carrier = ti_carrier - - if ti.state in State.finished: - self.set_ti_span_attrs(span=ti_span, state=ti.state, ti=ti) - ti_span.end(end_time=datetime_to_nano(ti.end_date)) - ti.span_status = SpanStatus.ENDED - else: - ti.span_status = SpanStatus.ACTIVE - self.active_spans.set(f"ti:{ti.id}", ti_span) - def _run_scheduler_loop(self) -> None: """ Harvest DAG parsing results, queue tasks, and perform executor heartbeat; the actual scheduler loop. @@ -1764,9 +1589,6 @@ def _run_scheduler_loop(self) -> None: for loop_count in itertools.count(start=1): with Stats.timer("scheduler.scheduler_loop_duration") as timer: with create_session() as session: - if self._is_tracing_enabled(): - self._end_spans_of_externally_ended_ops(session) - # This will schedule for as many executors as possible. num_queued_tis = self._do_scheduling(session) # Don't keep any objects alive -- we've possibly just looked at 500+ ORM objects! @@ -2091,7 +1913,7 @@ def _create_dag_runs(self, dag_models: Collection[DagModel], session: Session) - active_runs=active_runs_of_dags.get(dag_model.dag_id), ) continue - if dag_model.next_dagrun is None: + if dag_model.next_dagrun is None and dag_model.timetable_partitioned is False: self.log.error( "dag_model.next_dagrun is None; expected datetime", dag_id=dag_model.dag_id, @@ -2302,16 +2124,6 @@ def _start_queued_dagruns(self, session: Session) -> None: active_runs_of_dags = Counter({(dag_id, br_id): num for dag_id, br_id, num in session.execute(query)}) def _update_state(dag: SerializedDAG, dag_run: DagRun): - span = Trace.get_current_span() - span.set_attributes( - { - "state": str(DagRunState.RUNNING), - "run_id": dag_run.run_id, - "type": dag_run.run_type, - "dag_id": dag_run.dag_id, - } - ) - dag_run.state = DagRunState.RUNNING dag_run.start_date = timezone.utcnow() if ( @@ -2328,18 +2140,12 @@ def _update_state(dag: SerializedDAG, dag_run: DagRun): tags={}, extra_tags={"dag_id": dag.dag_id}, ) - if span.is_recording(): - span.add_event( - name="schedule_delay", - attributes={"dag_id": dag.dag_id, "schedule_delay": str(schedule_delay)}, - ) # cache saves time during scheduling of many dag_runs for same dag cached_get_dag: Callable[[DagRun], SerializedDAG | None] = lru_cache()( partial(self.scheduler_dag_bag.get_dag_for_run, session=session) ) - span = Trace.get_current_span() for dag_run in dag_runs: dag_id = dag_run.dag_id run_id = dag_run.run_id @@ -2379,15 +2185,6 @@ def _update_state(dag: SerializedDAG, dag_run: DagRun): dag_run.run_id, ) continue - if span.is_recording(): - span.add_event( - name="dag_run", - attributes={ - "run_id": dag_run.run_id, - "dag_id": dag_run.dag_id, - "conf": str(dag_run.conf), - }, - ) active_runs_of_dags[(dag_run.dag_id, backfill_id)] += 1 _update_state(dag, dag_run) dag_run.notify_dagrun_state_changed(msg="started") @@ -2437,7 +2234,12 @@ def _schedule_dag_run( select(TI) .where(TI.dag_id == dag_run.dag_id) .where(TI.run_id == dag_run.run_id) - .where(TI.state.in_(State.unfinished)) + .where(TI.state.in_(State.unfinished) | (TI.state.is_(None))) + ).all() + last_unfinished_ti = max( + unfinished_task_instances, + key=lambda ti: ti.start_date or timezone.make_aware(datetime.min), + default=None, ) for task_instance in unfinished_task_instances: task_instance.state = TaskInstanceState.SKIPPED @@ -2465,18 +2267,12 @@ def _schedule_dag_run( self.log.error("DagRun %s was deleted unexpectedly", dag_run.id) return None dag_run = dag_run_reloaded - callback_to_execute = DagCallbackRequest( - filepath=dag_model.relative_fileloc or "", - dag_id=dag.dag_id, - run_id=dag_run.run_id, - bundle_name=dag_model.bundle_name, - bundle_version=dag_run.bundle_version, - context_from_server=DagRunContext( - dag_run=dag_run, - last_ti=dag_run.get_last_ti(dag=dag, session=session), - ), - is_failure_callback=True, - msg="timed_out", + callback_to_execute = dag_run.produce_dag_callback( + dag=dag, + success=False, + relevant_ti=last_unfinished_ti, + reason="timed_out", + execute=False, ) dag_run.notify_dagrun_state_changed(msg="timed_out") @@ -2500,17 +2296,6 @@ def _schedule_dag_run( self.log.warning("The DAG disappeared before verifying integrity: %s. Skipping.", dag_run.dag_id) return callback - if ( - self._is_tracing_enabled() - and dag_run.scheduled_by_job_id is not None - and dag_run.scheduled_by_job_id != self.job.id - and self.active_spans.get("dr:" + str(dag_run.id)) is None - ): - # If the dag_run has been previously scheduled by another job and there is no active span, - # then check if the job is still healthy. - # If it's not healthy, then recreate the spans. - self._recreate_unhealthy_scheduler_spans_if_needed(dag_run, session) - dag_run.scheduled_by_job_id = self.job.id # TODO[HA]: Rename update_state -> schedule_dag_run, ?? something else? @@ -2672,21 +2457,33 @@ def _maybe_requeue_stuck_ti(self, *, ti, session, executor): if task.has_on_failure_callback: if inspect(ti).detached: ti = session.merge(ti) - request = TaskCallbackRequest( - filepath=ti.dag_model.relative_fileloc, - bundle_name=ti.dag_version.bundle_name, - bundle_version=ti.dag_version.bundle_version, - ti=ti, - msg=msg, - context_from_server=TIRunContext( - dag_run=ti.dag_run, - max_tries=ti.max_tries, - variables=[], - connections=[], - xcom_keys_to_clear=[], - ), + # Safely extract bundle info with fallback for legacy tasks + # (dag_version may be None after Airflow 2 → 3 migration). + _stuck_bundle_name = ( + ti.dag_version.bundle_name if ti.dag_version else ti.dag_model.bundle_name ) - executor.send_callback(request) + _stuck_bundle_version = ( + ti.dag_version.bundle_version if ti.dag_version else ti.dag_run.bundle_version + ) + # Backfill dag_version_id for legacy tasks (Pydantic requires uuid.UUID). + # Note: we cannot use `continue` here because this method is not + # inside a loop. If backfilling fails we simply skip the callback. + if _ensure_ti_has_dag_version_id(ti, session, self.log): + request = TaskCallbackRequest( + filepath=ti.dag_model.relative_fileloc or "", + bundle_name=_stuck_bundle_name, + bundle_version=_stuck_bundle_version, + ti=ti, + msg=msg, + context_from_server=TIRunContext( + dag_run=ti.dag_run, + max_tries=ti.max_tries, + variables=[], + connections=[], + xcom_keys_to_clear=[], + ), + ) + executor.send_callback(request) finally: ti.set_state(TaskInstanceState.FAILED, session=session) executor.fail(ti.key) @@ -3027,17 +2824,19 @@ def _purge_task_instances_without_heartbeats( task_instance_heartbeat_timeout_message_details = ( self._generate_task_instance_heartbeat_timeout_message_details(ti) ) - if not ti.dag_version: - # If old ti from Airflow 2 and dag_version is None, skip heartbeat timeout handling. - self.log.warning( - "DAG Version not found for TaskInstance %s. Skipping heartbeat timeout handling.", - ti, - ) + # Safely extract bundle info with fallback for legacy tasks + # (dag_version may be None after Airflow 2 → 3 migration). + _hb_bundle_name = ti.dag_version.bundle_name if ti.dag_version else ti.dag_model.bundle_name + _hb_bundle_version = ( + ti.dag_version.bundle_version if ti.dag_version else ti.dag_run.bundle_version + ) + # Backfill dag_version_id for legacy tasks (Pydantic requires uuid.UUID). + if not _ensure_ti_has_dag_version_id(ti, session, self.log): continue request = TaskCallbackRequest( filepath=ti.dag_model.relative_fileloc or "", - bundle_name=ti.dag_version.bundle_name, - bundle_version=ti.dag_run.bundle_version, + bundle_name=_hb_bundle_name, + bundle_version=_hb_bundle_version, ti=ti, msg=str(task_instance_heartbeat_timeout_message_details), context_from_server=TIRunContext( @@ -3134,44 +2933,41 @@ def _update_asset_orphanage(self, session: Session = NEW_SESSION) -> None: ) == 0 ).label("orphaned") - asset_reference_query = session.execute( - select(orphaned, AssetModel) + asset_reference_query = ( + select(AssetModel) .outerjoin(DagScheduleAssetReference) .outerjoin(TaskOutletAssetReference) .outerjoin(TaskInletAssetReference) .group_by(AssetModel.id) - .order_by(orphaned) ) - asset_orphanation: dict[bool, Collection[AssetModel]] = { - orphaned: [asset for _, asset in group] - for orphaned, group in itertools.groupby(asset_reference_query, key=operator.itemgetter(0)) - } - self._orphan_unreferenced_assets(asset_orphanation.get(True, ()), session=session) - self._activate_referenced_assets(asset_orphanation.get(False, ()), session=session) + + orphan_query = asset_reference_query.having(orphaned).cte() + activate_query = asset_reference_query.having(~orphaned).cte() + + self._orphan_unreferenced_assets(orphan_query, session=session) + self._activate_referenced_assets(activate_query, session=session) @staticmethod - def _orphan_unreferenced_assets(assets: Collection[AssetModel], *, session: Session) -> None: - if assets: - session.execute( - delete(AssetActive).where( - tuple_(AssetActive.name, AssetActive.uri).in_((a.name, a.uri) for a in assets) + def _orphan_unreferenced_assets(assets_query: CTE, *, session: Session) -> None: + deleted_orphaned_assets = session.execute( + delete(AssetActive).where( + exists().where( + and_(AssetActive.name == assets_query.c.name, AssetActive.uri == assets_query.c.uri) ) ) - Stats.gauge("asset.orphaned", len(assets)) + ) - @staticmethod - def _activate_referenced_assets(assets: Collection[AssetModel], *, session: Session) -> None: - if not assets: - return + Stats.gauge("asset.orphaned", max(getattr(deleted_orphaned_assets, "rowcount", 0), 0)) - active_assets = set( - session.execute( - select(AssetActive.name, AssetActive.uri).where( - tuple_(AssetActive.name, AssetActive.uri).in_((a.name, a.uri) for a in assets) - ) - ) + @staticmethod + def _activate_referenced_assets(assets_query: CTE, *, session: Session) -> None: + active_assets_query = select(AssetActive.name, AssetActive.uri).join( + assets_query, + and_(AssetActive.name == assets_query.c.name, AssetActive.uri == assets_query.c.uri), ) + active_assets = session.execute(active_assets_query).all() + active_name_to_uri: dict[str, str] = {name: uri for name, uri in active_assets} active_uri_to_name: dict[str, str] = {uri: name for name, uri in active_assets} @@ -3196,9 +2992,24 @@ def _generate_warning_message( def _activate_assets_generate_warnings() -> Iterator[tuple[str, str]]: incoming_name_to_uri: dict[str, str] = {} incoming_uri_to_name: dict[str, str] = {} - for asset in assets: - if (asset.name, asset.uri) in active_assets: - continue + + inactive_assets_query = ( + select(AssetModel) + .join( + assets_query, + and_( + assets_query.c.name == AssetModel.name, + assets_query.c.uri == AssetModel.uri, + ), + ) + .where( + ~active_assets_query.where( + and_(AssetActive.name == AssetModel.name, AssetActive.uri == AssetModel.uri) + ).exists() + ) + ) + + for asset in session.scalars(inactive_assets_query): existing_uri = active_name_to_uri.get(asset.name) or incoming_name_to_uri.get(asset.name) if existing_uri is not None and existing_uri != asset.uri: yield from _generate_warning_message(asset, "name", existing_uri) diff --git a/airflow-core/src/airflow/jobs/triggerer_job_runner.py b/airflow-core/src/airflow/jobs/triggerer_job_runner.py index ca11647811099..1406283c05cb3 100644 --- a/airflow-core/src/airflow/jobs/triggerer_job_runner.py +++ b/airflow-core/src/airflow/jobs/triggerer_job_runner.py @@ -50,7 +50,6 @@ from airflow.jobs.job import perform_heartbeat from airflow.models.trigger import Trigger from airflow.observability.metrics import stats_utils -from airflow.observability.trace import Trace from airflow.sdk.api.datamodels._generated import HITLDetailResponse from airflow.sdk.execution_time.comms import ( CommsDecoder, @@ -627,15 +626,6 @@ def emit_metrics(self): extra_tags={"hostname": self.job.hostname}, ) - span = Trace.get_current_span() - span.set_attributes( - { - "trigger host": self.job.hostname, - "triggers running": len(self.running_triggers), - "capacity left": capacity_left, - } - ) - def update_triggers(self, requested_trigger_ids: set[int]): """ Request that we update what triggers we're running. diff --git a/airflow-core/src/airflow/logging_config.py b/airflow-core/src/airflow/logging_config.py index b0aa945e6b41e..0da017d9b0826 100644 --- a/airflow-core/src/airflow/logging_config.py +++ b/airflow-core/src/airflow/logging_config.py @@ -115,13 +115,30 @@ def configure_logging(): log_format=getattr(logging_config, "LOG_FORMAT", conf.get("logging", "log_format", fallback="")), callsite_params=conf.getlist("logging", "callsite_parameters", fallback=[]), ) + json_output = conf.getboolean("logging", "json_logs", fallback=False) + + stdlib_config = dict(logging_config) + # Route uvicorn/gunicorn error loggers explicitly through our handler so their output + # is formatted correctly regardless of what propagation state those loggers end up in. + # Suppress the built-in access loggers; HttpAccessLogMiddleware and + # AirflowUvicornWorker.CONFIG_KWARGS take over access logging instead. + extra_loggers = { + "uvicorn.access": {"handlers": [], "propagate": False}, + "gunicorn.access": {"handlers": [], "propagate": False}, + "uvicorn.error": {"handlers": ["default"], "propagate": False}, + "gunicorn.error": {"handlers": ["default"], "propagate": False}, + } + stdlib_config = {**stdlib_config, "loggers": {**stdlib_config.get("loggers", {}), **extra_loggers}} + configure_logging( log_level=level, namespace_log_levels=conf.get("logging", "namespace_levels", fallback=None), - stdlib_config=logging_config, + stdlib_config=stdlib_config, log_format=log_fmt, + log_timestamp_format=conf.get("logging", "log_timestamp_format", fallback="iso"), callsite_parameters=callsite_params, colors=colors, + json_output=json_output, ) except (ValueError, KeyError) as e: log.error("Unable to load the config, contains a configuration error.") diff --git a/airflow-core/src/airflow/migrations/versions/0087_3_1_8_change_signed_url_template_from_varchar_.py b/airflow-core/src/airflow/migrations/versions/0087_3_1_8_change_signed_url_template_from_varchar_.py index 9fae0722c8ca3..7966e10d1f2f2 100644 --- a/airflow-core/src/airflow/migrations/versions/0087_3_1_8_change_signed_url_template_from_varchar_.py +++ b/airflow-core/src/airflow/migrations/versions/0087_3_1_8_change_signed_url_template_from_varchar_.py @@ -30,6 +30,8 @@ import sqlalchemy as sa from alembic import op +from airflow.migrations.utils import disable_sqlite_fkeys + # revision identifiers, used by Alembic. revision = "509b94a1042d" down_revision = "82dbd68e6171" @@ -40,27 +42,23 @@ def upgrade(): """Apply Change signed_url_template from VARCHAR(200) to TEXT.""" - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("dag_bundle", schema=None) as batch_op: - batch_op.alter_column( - "signed_url_template", - existing_type=sa.VARCHAR(length=200), - type_=sa.Text(), - existing_nullable=True, - ) - - # ### end Alembic commands ### + with disable_sqlite_fkeys(op): + with op.batch_alter_table("dag_bundle", schema=None) as batch_op: + batch_op.alter_column( + "signed_url_template", + existing_type=sa.VARCHAR(length=200), + type_=sa.Text(), + existing_nullable=True, + ) def downgrade(): """Unapply Change signed_url_template from VARCHAR(200) to TEXT.""" - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("dag_bundle", schema=None) as batch_op: - batch_op.alter_column( - "signed_url_template", - existing_type=sa.Text(), - type_=sa.VARCHAR(length=200), - existing_nullable=True, - ) - - # ### end Alembic commands ### + with disable_sqlite_fkeys(op): + with op.batch_alter_table("dag_bundle", schema=None) as batch_op: + batch_op.alter_column( + "signed_url_template", + existing_type=sa.Text(), + type_=sa.VARCHAR(length=200), + existing_nullable=True, + ) diff --git a/airflow-core/src/airflow/migrations/versions/0096_3_2_0_remove_team_id.py b/airflow-core/src/airflow/migrations/versions/0096_3_2_0_remove_team_id.py index 1b122d42d91e0..b3c132d5b0234 100644 --- a/airflow-core/src/airflow/migrations/versions/0096_3_2_0_remove_team_id.py +++ b/airflow-core/src/airflow/migrations/versions/0096_3_2_0_remove_team_id.py @@ -94,6 +94,8 @@ def upgrade(): def downgrade(): + import uuid as uuid_mod + # Drop FKs pointing to name for table in ("connection", "variable", "slot_pool"): with op.batch_alter_table(table) as batch_op: @@ -103,11 +105,23 @@ def downgrade(): batch_op.drop_constraint("dag_bundle_team_team_name_fkey", type_="foreignkey") batch_op.drop_index("idx_dag_bundle_team_team_name") - # Add back team.id + # Add back team.id — nullable first so existing rows don't fail with op.batch_alter_table("team") as batch_op: batch_op.drop_constraint("team_pkey", type_="primary") - batch_op.add_column(sa.Column("id", sa.String(36), nullable=False)) + batch_op.add_column(sa.Column("id", sa.String(36), nullable=True)) batch_op.create_unique_constraint("team_name_uq", ["name"]) + + # Generate UUIDs for existing team rows + conn = op.get_bind() + teams = conn.execute(sa.text("SELECT name FROM team")).fetchall() + for team in teams: + conn.execute( + sa.text("UPDATE team SET id = :id WHERE name = :name"), + {"id": str(uuid_mod.uuid4()), "name": team.name}, + ) + + with op.batch_alter_table("team") as batch_op: + batch_op.alter_column("id", existing_type=sa.String(36), nullable=False) batch_op.create_primary_key("team_pkey", ["id"]) # Rename team_name → team_id @@ -128,6 +142,21 @@ def downgrade(): nullable=False, ) + # Convert team name values back to generated UUIDs in referencing tables + for table in ("connection", "variable", "slot_pool"): + conn.execute( + sa.text( + f"UPDATE {table} SET team_id = " + f"(SELECT id FROM team WHERE name = {table}.team_id) " + f"WHERE team_id IS NOT NULL" + ) + ) + conn.execute( + sa.text( + "UPDATE dag_bundle_team SET team_id = (SELECT id FROM team WHERE name = dag_bundle_team.team_id)" + ) + ) + # Re-create FK on old id for table in ("connection", "variable", "slot_pool"): with op.batch_alter_table(table) as batch_op: @@ -139,6 +168,7 @@ def downgrade(): ) with op.batch_alter_table("dag_bundle_team") as batch_op: + batch_op.create_index("idx_dag_bundle_team_team_id", ["team_id"]) batch_op.create_foreign_key( "dag_bundle_team_team_id_fkey", "team", diff --git a/airflow-core/src/airflow/migrations/versions/0101_3_2_0_ui_improvements_for_deadlines.py b/airflow-core/src/airflow/migrations/versions/0101_3_2_0_ui_improvements_for_deadlines.py index 4188254ca2eba..92bfc558d4726 100644 --- a/airflow-core/src/airflow/migrations/versions/0101_3_2_0_ui_improvements_for_deadlines.py +++ b/airflow-core/src/airflow/migrations/versions/0101_3_2_0_ui_improvements_for_deadlines.py @@ -37,6 +37,7 @@ from typing import TYPE_CHECKING import sqlalchemy as sa +import structlog import uuid6 from alembic import context, op @@ -46,6 +47,9 @@ from airflow.utils.hashlib_wrapper import md5 from airflow.utils.sqlalchemy import UtcDateTime +log = structlog.get_logger(__name__) + + if TYPE_CHECKING: from typing import Any @@ -285,7 +289,7 @@ def validate_written_data( ).fetchone() if not validation_result: - print(f"ERROR: Failed to read back deadline_alert for DeadlineAlert {deadline_alert_id}") + log.error("Failed to read back deadline_alert", deadline_alert_id=deadline_alert_id) return False checks = [ @@ -296,7 +300,7 @@ def validate_written_data( for name, actual, expected in checks: if actual != expected: - print(f"ERROR: Written {name} does not match expected! Written: {actual}, Expected: {expected}") + log.error("Written value does not match expected", field=name, actual=actual, expected=expected) return False return True @@ -304,11 +308,9 @@ def validate_written_data( def report_errors(errors: ErrorDict, operation: str = "migration") -> None: if errors: - print(f"{len(errors)} Dags encountered errors: ") - for dag_id, error in errors.items(): - print(f" {dag_id}: {'; '.join(error)}") + log.warning("Dags encountered errors", operation=operation, count=len(errors), errors=dict(errors)) else: - print(f"No Dags encountered errors during {operation}.") + log.info("No Dags encountered errors", operation=operation) def hash_dag(dag_data): @@ -355,14 +357,10 @@ def _sort_serialized_dag_dict(serialized_dag: Any): def migrate_existing_deadline_alert_data_from_serialized_dag() -> None: """Extract DeadlineAlert data from serialized Dag data and populate deadline_alert table.""" if context.is_offline_mode(): - print( - """ - ------------ - -- WARNING: Unable to migrate DeadlineAlert data while in offline mode! - -- The deadline_alert table will remain empty in offline mode. - -- Run the migration in online mode to populate the deadline_alert table. - ------------ - """ + log.warning( + "Unable to migrate DeadlineAlert data while in offline mode -- " + "the deadline_alert table will remain empty. " + "Run the migration in online mode to populate the deadline_alert table." ) return @@ -383,8 +381,7 @@ def migrate_existing_deadline_alert_data_from_serialized_dag() -> None: ).scalar() total_batches = (total_dags + BATCH_SIZE - 1) // BATCH_SIZE - print(f"Using migration_batch_size of {BATCH_SIZE} as set in Airflow configuration.") - print(f"Starting migration of {total_dags} Dags in {total_batches} batches.\n") + log.info("Starting migration", batch_size=BATCH_SIZE, total_dags=total_dags, total_batches=total_batches) while True: batch_num += 1 @@ -424,7 +421,7 @@ def migrate_existing_deadline_alert_data_from_serialized_dag() -> None: if not batch_results: break - print(f"Processing batch {batch_num}...") + log.info("Processing batch", batch_num=batch_num, total_batches=total_batches) for serialized_dag_id, dag_id, data, data_compressed, created_at in batch_results: processed_dags.append(dag_id) @@ -555,13 +552,15 @@ def migrate_existing_deadline_alert_data_from_serialized_dag() -> None: dags_with_errors[dag_id].append(f"Could not process serialized Dag: {e}") savepoint.rollback() - print(f"Batch {batch_num} of {total_batches} complete.") + log.info("Batch complete", batch_num=batch_num, total_batches=total_batches) - print( - f"\nProcessed {len(processed_dags)} serialized_dag records ({len(set(processed_dags))} " - f"unique Dags), {len(dags_with_deadlines)} had DeadlineAlerts." + log.info( + "Migration complete", + processed_records=len(processed_dags), + unique_dags=len(set(processed_dags)), + dags_with_deadlines=len(dags_with_deadlines), + migrated_alerts=migrated_alerts_count, ) - print(f"Migrated {migrated_alerts_count} DeadlineAlert configurations.") report_errors(dags_with_errors, "migration") @@ -570,14 +569,10 @@ def migrate_deadline_alert_data_back_to_serialized_dag() -> None: from alembic import context if context.is_offline_mode(): - print( - """ - ------------ - -- WARNING: Unable to restore DeadlineAlert data while in offline mode! - -- The downgrade will skip data restoration in offline mode. - -- Run the migration in online mode to restore the deadline_alert data. - ------------ - """ + log.warning( + "Unable to restore DeadlineAlert data while in offline mode -- " + "the downgrade will skip data restoration. " + "Run the migration in online mode to restore the deadline_alert data." ) return @@ -603,8 +598,7 @@ def migrate_deadline_alert_data_back_to_serialized_dag() -> None: total_batches = (total_dags + BATCH_SIZE - 1) // BATCH_SIZE - print(f"Using migration_batch_size of {BATCH_SIZE} as set in Airflow configuration.") - print(f"Starting downgrade of {total_dags} Dags with DeadlineAlerts in {total_batches} batches.\n") + log.info("Starting downgrade", batch_size=BATCH_SIZE, total_dags=total_dags, total_batches=total_batches) while True: batch_num += 1 @@ -643,7 +637,7 @@ def migrate_deadline_alert_data_back_to_serialized_dag() -> None: batch_results = list(result) if not batch_results: break - print(f"Processing batch {batch_num}...") + log.info("Processing batch", batch_num=batch_num, total_batches=total_batches) for serialized_dag_id, dag_id, data, data_compressed in batch_results: processed_dags.append(dag_id) @@ -660,7 +654,7 @@ def migrate_deadline_alert_data_back_to_serialized_dag() -> None: continue if not all(isinstance(uuid_val, str) for uuid_val in deadline_uuids): - print(f"WARNING: Dag {dag_id} has non-string deadline values, skipping") + log.warning("Dag has non-string deadline values, skipping", dag_id=dag_id) continue dags_with_deadlines.add(dag_id) @@ -704,11 +698,13 @@ def migrate_deadline_alert_data_back_to_serialized_dag() -> None: dags_with_errors[dag_id].append(f"Could not restore deadline: {e}") savepoint.rollback() - print(f"Batch {batch_num} of {total_batches} complete.") + log.info("Batch complete", batch_num=batch_num, total_batches=total_batches) - print( - f"\nProcessed {len(processed_dags)} serialized_dag records ({len(set(processed_dags))} " - f"unique Dags), {len(dags_with_deadlines)} had DeadlineAlerts." + log.info( + "Downgrade complete", + processed_records=len(processed_dags), + unique_dags=len(set(processed_dags)), + dags_with_deadlines=len(dags_with_deadlines), + restored_alerts=restored_alerts_count, ) - print(f"Restored {restored_alerts_count} DeadlineAlert configurations to original format.") report_errors(dags_with_errors, "downgrade") diff --git a/airflow-core/src/airflow/models/dagbag.py b/airflow-core/src/airflow/models/dagbag.py index 3d20c323e031c..e04f77d06df34 100644 --- a/airflow-core/src/airflow/models/dagbag.py +++ b/airflow-core/src/airflow/models/dagbag.py @@ -21,9 +21,8 @@ from typing import TYPE_CHECKING, Any from uuid import UUID -from sqlalchemy import String, inspect, select +from sqlalchemy import String, select from sqlalchemy.orm import Mapped, joinedload, mapped_column -from sqlalchemy.orm.attributes import NO_VALUE from airflow.models.base import Base, StringID from airflow.models.dag_version import DagVersion @@ -66,23 +65,16 @@ def _get_dag(self, version_id: UUID, session: Session) -> SerializedDAG | None: return self._read_dag(serdag) @staticmethod - def _version_from_dag_run(dag_run: DagRun, *, session: Session) -> DagVersion | None: + def _version_from_dag_run(dag_run: DagRun, *, session: Session) -> UUID | None: if not dag_run.bundle_version: if dag_version := DagVersion.get_latest_version(dag_id=dag_run.dag_id, session=session): - return dag_version + return dag_version.id - # Check if created_dag_version relationship is already loaded to avoid DetachedInstanceError - info: Any = inspect(dag_run) - if info.attrs.created_dag_version.loaded_value is not NO_VALUE: - # Relationship is already loaded, safe to access - return dag_run.created_dag_version - - # Relationship not loaded, fetch it explicitly from current session - return session.get(DagVersion, dag_run.created_dag_version_id) + return dag_run.created_dag_version_id def get_dag_for_run(self, dag_run: DagRun, session: Session) -> SerializedDAG | None: - if version := self._version_from_dag_run(dag_run=dag_run, session=session): - return self._get_dag(version_id=version.id, session=session) + if version_id := self._version_from_dag_run(dag_run=dag_run, session=session): + return self._get_dag(version_id=version_id, session=session) return None def iter_all_latest_version_dags(self, *, session: Session) -> Generator[SerializedDAG, None, None]: diff --git a/airflow-core/src/airflow/models/dagrun.py b/airflow-core/src/airflow/models/dagrun.py index 0de1a784fa409..61242e45390d6 100644 --- a/airflow-core/src/airflow/models/dagrun.py +++ b/airflow-core/src/airflow/models/dagrun.py @@ -28,7 +28,9 @@ from uuid import UUID import structlog -from natsort import natsorted +from opentelemetry import context, trace +from opentelemetry.trace import StatusCode +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator from sqlalchemy import ( JSON, Enum, @@ -73,12 +75,11 @@ from airflow.models.taskinstancehistory import TaskInstanceHistory as TIH from airflow.models.tasklog import LogTemplate from airflow.models.taskmap import TaskMap -from airflow.observability.trace import Trace +from airflow.observability.traces import new_dagrun_trace_carrier, override_ids from airflow.serialization.definitions.deadline import SerializedReferenceModels from airflow.serialization.definitions.notset import NOTSET, ArgNotSet, is_arg_set from airflow.ti_deps.dep_context import DepContext from airflow.ti_deps.dependencies_states import SCHEDULEABLE_STATES -from airflow.utils.dates import datetime_to_nano from airflow.utils.helpers import chunks, is_container, prune_dict from airflow.utils.log.logging_mixin import LoggingMixin from airflow.utils.retries import retry_db_transaction @@ -93,19 +94,16 @@ ) from airflow.utils.state import DagRunState, State, TaskInstanceState from airflow.utils.strings import get_random_string -from airflow.utils.thread_safe_dict import ThreadSafeDict from airflow.utils.types import DagRunTriggeredByType, DagRunType if TYPE_CHECKING: from typing import Literal, TypeAlias - from opentelemetry.sdk.trace import Span from pydantic import NonNegativeInt from sqlalchemy.engine import ScalarResult from sqlalchemy.orm import Session from sqlalchemy.sql.elements import Case, ColumnElement - from airflow._shared.observability.traces.base_tracer import EmptySpan from airflow.models.dag_version import DagVersion from airflow.models.taskinstancekey import TaskInstanceKey from airflow.sdk import DAG as SDKDAG @@ -121,6 +119,8 @@ log = structlog.get_logger(__name__) +tracer = trace.get_tracer(__name__) + class TISchedulingDecision(NamedTuple): """Type of return for DagRun.task_instance_scheduling_decisions.""" @@ -154,8 +154,6 @@ class DagRun(Base, LoggingMixin): external trigger (i.e. manual runs). """ - active_spans = ThreadSafeDict() - __tablename__ = "dag_run" id: Mapped[int] = mapped_column(Integer, primary_key=True) @@ -369,7 +367,8 @@ def __init__( self.triggered_by = triggered_by self.triggering_user_name = triggering_user_name self.scheduled_by_job_id = None - self.context_carrier = {} + self.context_carrier: dict[str, str] = new_dagrun_trace_carrier() + if not isinstance(partition_key, str | None): raise ValueError( f"Expected partition_key to be a `str` or `None` but got `{partition_key.__class__.__name__}`" @@ -462,10 +461,6 @@ def check_version_id_exists_in_dr(self, dag_version_id: UUID, session: Session = def stats_tags(self) -> dict[str, str]: return prune_dict({"dag_id": self.dag_id, "run_type": self.run_type}) - @classmethod - def set_active_spans(cls, active_spans: ThreadSafeDict): - cls.active_spans = active_spans - def get_state(self): return self._state @@ -1020,131 +1015,28 @@ def is_effective_leaf(task): leaf_tis = {ti for ti in tis if ti.task_id in leaf_task_ids if ti.state != TaskInstanceState.REMOVED} return leaf_tis - def set_dagrun_span_attrs(self, span: Span | EmptySpan): - if self._state == DagRunState.FAILED: - span.set_attribute("airflow.dag_run.error", True) - - # Explicitly set the value type to Union[...] to avoid a mypy error. - attributes: dict[str, AttributeValueType] = { - "airflow.category": "DAG runs", - "airflow.dag_run.dag_id": str(self.dag_id), - "airflow.dag_run.logical_date": str(self.logical_date), - "airflow.dag_run.run_id": str(self.run_id), - "airflow.dag_run.queued_at": str(self.queued_at), - "airflow.dag_run.run_start_date": str(self.start_date), - "airflow.dag_run.run_end_date": str(self.end_date), - "airflow.dag_run.run_duration": str( - (self.end_date - self.start_date).total_seconds() if self.start_date and self.end_date else 0 - ), - "airflow.dag_run.state": str(self._state), - "airflow.dag_run.run_type": str(self.run_type), - "airflow.dag_run.data_interval_start": str(self.data_interval_start), - "airflow.dag_run.data_interval_end": str(self.data_interval_end), - "airflow.dag_run.conf": str(self.conf), - } - if span.is_recording(): - span.add_event(name="airflow.dag_run.queued", timestamp=datetime_to_nano(self.queued_at)) - span.add_event(name="airflow.dag_run.started", timestamp=datetime_to_nano(self.start_date)) - span.add_event(name="airflow.dag_run.ended", timestamp=datetime_to_nano(self.end_date)) - span.set_attributes(attributes) - - def start_dr_spans_if_needed(self, tis: list[TI]): - # If there is no value in active_spans, then the span hasn't already been started. - if self.active_spans is not None and self.active_spans.get("dr:" + str(self.id)) is None: - if self.span_status == SpanStatus.NOT_STARTED or self.span_status == SpanStatus.NEEDS_CONTINUANCE: - dr_span = None - continue_ti_spans = False - if self.span_status == SpanStatus.NOT_STARTED: - dr_span = Trace.start_root_span( - span_name=f"{self.dag_id}", - component="dag", - start_time=self.queued_at, # This is later converted to nano. - start_as_current=False, - ) - elif self.span_status == SpanStatus.NEEDS_CONTINUANCE: - # Use the existing context_carrier to set the initial dag_run span as the parent. - parent_context = Trace.extract(self.context_carrier) - with Trace.start_child_span( - span_name="new_scheduler", parent_context=parent_context - ) as s: - s.set_attribute("trace_status", "continued") - - dr_span = Trace.start_child_span( - span_name=f"{self.dag_id}_continued", - parent_context=parent_context, - component="dag", - # No start time - start_as_current=False, - ) - # After this span is started, the context_carrier will be replaced by the new one. - # New task span will use this span as the parent. - continue_ti_spans = True - carrier = Trace.inject() - self.context_carrier = carrier - self.span_status = SpanStatus.ACTIVE - # Set the span in a synchronized dictionary, so that the variable can be used to end the span. - self.active_spans.set("dr:" + str(self.id), dr_span) - self.log.debug( - "DagRun span has been started and the injected context_carrier is: %s", - self.context_carrier, - ) - # Start TI spans that also need continuance. - if continue_ti_spans: - new_dagrun_context = Trace.extract(self.context_carrier) - for ti in tis: - if ti.span_status == SpanStatus.NEEDS_CONTINUANCE: - ti_span = Trace.start_child_span( - span_name=f"{ti.task_id}_continued", - parent_context=new_dagrun_context, - start_as_current=False, - ) - ti_carrier = Trace.inject() - ti.context_carrier = ti_carrier - ti.span_status = SpanStatus.ACTIVE - self.active_spans.set(f"ti:{ti.id}", ti_span) - else: - self.log.debug( - "Found span_status '%s', while updating state for dag_run '%s'", - self.span_status, - self.run_id, - ) - - def end_dr_span_if_needed(self): - if self.active_spans is not None: - active_span = self.active_spans.get("dr:" + str(self.id)) - if active_span is not None: - self.log.debug( - "Found active span with span_id: %s, for dag_id: %s, run_id: %s, state: %s", - active_span.get_span_context().span_id, - self.dag_id, - self.run_id, - self.state, - ) - - self.set_dagrun_span_attrs(span=active_span) - active_span.end(end_time=datetime_to_nano(self.end_date)) - # Remove the span from the dict. - self.active_spans.delete("dr:" + str(self.id)) - self.span_status = SpanStatus.ENDED - else: - if self.span_status == SpanStatus.ACTIVE: - # Another scheduler has started the span. - # Update the DB SpanStatus to notify the owner to end it. - self.span_status = SpanStatus.SHOULD_END - elif self.span_status == SpanStatus.NEEDS_CONTINUANCE: - # This is a corner case where the scheduler exited gracefully - # while the dag_run was almost done. - # Since it reached this point, the dag has finished but there has been no time - # to create a new span for the current scheduler. - # There is no need for more spans, update the status on the db. - self.span_status = SpanStatus.ENDED - else: - self.log.debug( - "No active span has been found for dag_id: %s, run_id: %s, state: %s", - self.dag_id, - self.run_id, - self.state, - ) + def _emit_dagrun_span(self, state: DagRunState): + ctx = TraceContextTextMapPropagator().extract(self.context_carrier) + span = trace.get_current_span(context=ctx) + span_context = span.get_span_context() + with override_ids(span_context.trace_id, span_context.span_id): + attributes = { + "airflow.dag_id": str(self.dag_id), + "airflow.dag_run.run_id": self.run_id, + } + if self.logical_date: + attributes["airflow.dag_run.logical_date"] = str(self.logical_date) + if self.partition_key: + attributes["airflow.dag_run.partition_key"] = str(self.partition_key) + span = tracer.start_span( + name=f"dag_run.{self.dag_id}", + start_time=int((self.start_date or timezone.utcnow()).timestamp() * 1e9), + attributes=attributes, + context=context.Context(), + ) + status_code = StatusCode.OK if state == DagRunState.SUCCESS else StatusCode.ERROR + span.set_status(status_code) + span.end() @provide_session def update_state( @@ -1220,21 +1112,18 @@ def recalculate(self) -> _UnfinishedStates: self.set_state(DagRunState.FAILED) self.notify_dagrun_state_changed(msg="task_failure") - if execute_callbacks and dag.has_on_failure_callback: - self.handle_dag_callback(dag=cast("SDKDAG", dag), success=False, reason="task_failure") - elif dag.has_on_failure_callback: - callback = DagCallbackRequest( - filepath=self.dag_model.relative_fileloc, - dag_id=self.dag_id, - run_id=self.run_id, - bundle_name=self.dag_model.bundle_name, - bundle_version=self.bundle_version, - context_from_server=DagRunContext( - dag_run=self, - last_ti=self.get_last_ti(dag=dag, session=session), - ), - is_failure_callback=True, - msg="task_failure", + if dag.has_on_failure_callback: + ti_causing_failure = max( + (ti for ti in tis if ti.state == TaskInstanceState.FAILED), + key=lambda ti: ti.end_date or timezone.make_aware(datetime.min), + default=None, + ) + callback = self.produce_dag_callback( + dag=dag, + success=False, + relevant_ti=ti_causing_failure, + reason="task_failure", + execute=execute_callbacks, ) # Check if the max_consecutive_failed_dag_runs has been provided and not 0 @@ -1253,21 +1142,18 @@ def recalculate(self) -> _UnfinishedStates: self.set_state(DagRunState.SUCCESS) self.notify_dagrun_state_changed(msg="success") - if execute_callbacks and dag.has_on_success_callback: - self.handle_dag_callback(dag=cast("SDKDAG", dag), success=True, reason="success") - elif dag.has_on_success_callback: - callback = DagCallbackRequest( - filepath=self.dag_model.relative_fileloc, - dag_id=self.dag_id, - run_id=self.run_id, - bundle_name=self.dag_model.bundle_name, - bundle_version=self.bundle_version, - context_from_server=DagRunContext( - dag_run=self, - last_ti=self.get_last_ti(dag=dag, session=session), - ), - is_failure_callback=False, - msg="success", + if dag.has_on_success_callback: + last_succeeded_ti: TI | None = max( + (ti for ti in tis if ti.state == TaskInstanceState.SUCCESS), + key=lambda ti: ti.end_date or timezone.make_aware(datetime.min), + default=None, + ) + callback = self.produce_dag_callback( + dag=dag, + success=True, + relevant_ti=last_succeeded_ti, + reason="success", + execute=execute_callbacks, ) if dag.deadline: @@ -1288,32 +1174,27 @@ def recalculate(self) -> _UnfinishedStates: self.set_state(DagRunState.FAILED) self.notify_dagrun_state_changed(msg="all_tasks_deadlocked") - if execute_callbacks and dag.has_on_failure_callback: - self.handle_dag_callback( - dag=cast("SDKDAG", dag), + if dag.has_on_failure_callback: + finished_task_ids = {ti.task_id for ti in finished_tis} + blocking_ti = next( + ( + ti + for ti in unfinished.tis + if ti.task + and not (ti.task.get_direct_relative_ids(upstream=True).isdisjoint(finished_task_ids)) + ), + None, + ) + callback = self.produce_dag_callback( + dag=dag, success=False, + relevant_ti=blocking_ti, reason="all_tasks_deadlocked", - ) - elif dag.has_on_failure_callback: - callback = DagCallbackRequest( - filepath=self.dag_model.relative_fileloc, - dag_id=self.dag_id, - run_id=self.run_id, - bundle_name=self.dag_model.bundle_name, - bundle_version=self.bundle_version, - context_from_server=DagRunContext( - dag_run=self, - last_ti=self.get_last_ti(dag=dag, session=session), - ), - is_failure_callback=True, - msg="all_tasks_deadlocked", + execute=execute_callbacks, ) # finally, if the leaves aren't done, the dag is still running else: - # It might need to start TI spans as well. - self.start_dr_spans_if_needed(tis=tis) - self.set_state(DagRunState.RUNNING) if self._state == DagRunState.FAILED or self._state == DagRunState.SUCCESS: @@ -1340,10 +1221,8 @@ def recalculate(self) -> _UnfinishedStates: self.data_interval_start, self.data_interval_end, ) - - self.end_dr_span_if_needed() - session.flush() + self._emit_dagrun_span(state=self.state) self._emit_true_scheduling_delay_stats_for_finished_state(finished_tis) self._emit_duration_stats_for_finished_state() @@ -1417,27 +1296,40 @@ def notify_dagrun_state_changed(self, msg: str): # we can't get all the state changes on SchedulerJob, # or LocalTaskJob, so we don't want to "falsely advertise" we notify about that - @provide_session - def get_last_ti(self, dag: SerializedDAG, session: Session = NEW_SESSION) -> TI | None: - """Get Last TI from the dagrun to build and pass Execution context object from server to then run callbacks.""" - tis = self.get_task_instances(session=session) - # tis from a dagrun may not be a part of dag.partial_subset, - # since dag.partial_subset is a subset of the dag. - # This ensures that we will only use the accessible TI - # context for the callback. - if dag.partial: - tis = [ti for ti in tis if not ti.state == State.NONE] - # filter out removed tasks - tis = natsorted( - (ti for ti in tis if ti.state != TaskInstanceState.REMOVED), - key=lambda ti: ti.task_id, + def produce_dag_callback( + self, + dag: SerializedDAG, + success: bool = True, + relevant_ti: TI | None = None, + reason: str = "success", + execute: bool = False, + ) -> DagCallbackRequest | None: + """Create a callback request for the DAG, or execute the callbacks directly if instructed, and return None.""" + if not execute: + return DagCallbackRequest( + filepath=self.dag_model.relative_fileloc, + dag_id=self.dag_id, + run_id=self.run_id, + bundle_name=self.dag_model.bundle_name, + bundle_version=self.bundle_version, + context_from_server=DagRunContext( + dag_run=self, + last_ti=relevant_ti, + ), + is_failure_callback=(not success), + msg=reason, + ) + self.execute_dag_callbacks( + dag=cast("SDKDAG", dag), + success=success, + relevant_ti=relevant_ti, + reason=reason, ) - if not tis: - return None - ti = tis[-1] # get last TaskInstance of DagRun - return ti + return None - def handle_dag_callback(self, dag: SDKDAG, success: bool = True, reason: str = "success"): + def execute_dag_callbacks( + self, dag: SDKDAG, success: bool = True, relevant_ti: TI | None = None, reason: str = "success" + ): """Only needed for `dag.test` where `execute_callbacks=True` is passed to `update_state`.""" from airflow.api_fastapi.execution_api.datamodels.taskinstance import ( DagRun as DRDataModel, @@ -1446,10 +1338,9 @@ def handle_dag_callback(self, dag: SDKDAG, success: bool = True, reason: str = " ) from airflow.sdk.execution_time.task_runner import RuntimeTaskInstance - last_ti = self.get_last_ti(cast("SerializedDAG", dag)) - if last_ti: - last_ti_model = TIDataModel.model_validate(last_ti, from_attributes=True) - task = dag.get_task(last_ti.task_id) + if relevant_ti: + last_ti_model = TIDataModel.model_validate(relevant_ti, from_attributes=True) + task = dag.get_task(relevant_ti.task_id) dag_run_data = DRDataModel( dag_id=self.dag_id, @@ -1472,12 +1363,12 @@ def handle_dag_callback(self, dag: SDKDAG, success: bool = True, reason: str = " task=task, _ti_context_from_server=TIRunContext( dag_run=dag_run_data, - max_tries=last_ti.max_tries, + max_tries=relevant_ti.max_tries, variables=[], connections=[], xcom_keys_to_clear=[], ), - max_tries=last_ti.max_tries, + max_tries=relevant_ti.max_tries, ) context = runtime_ti.get_template_context() else: diff --git a/airflow-core/src/airflow/models/deadline.py b/airflow-core/src/airflow/models/deadline.py index debfe949b314c..ec3ab5ad99cae 100644 --- a/airflow-core/src/airflow/models/deadline.py +++ b/airflow-core/src/airflow/models/deadline.py @@ -314,7 +314,11 @@ def evaluate_with(self, *, session: Session, interval: timedelta, **kwargs: Any) ) if extra_kwargs := kwargs.keys() - filtered_kwargs.keys(): - self.log.debug("Ignoring unexpected parameters: %s", ", ".join(extra_kwargs)) + self.log.debug( + "%s ignoring unexpected parameters: %s", + self.reference_name, + ", ".join(extra_kwargs), + ) base_time = self._evaluate_with(session=session, **filtered_kwargs) return base_time + interval if base_time is not None else None diff --git a/airflow-core/src/airflow/models/deadline_alert.py b/airflow-core/src/airflow/models/deadline_alert.py index 8afc35d756073..0b8a8eba9b1b1 100644 --- a/airflow-core/src/airflow/models/deadline_alert.py +++ b/airflow-core/src/airflow/models/deadline_alert.py @@ -86,9 +86,10 @@ def matches_definition(self, other: DeadlineAlert) -> bool: @property def reference_class(self) -> type[SerializedReferenceModels.SerializedBaseDeadlineReference]: """Return the deserialized reference class.""" - return SerializedReferenceModels.get_reference_class( - self.reference[SerializedReferenceModels.REFERENCE_TYPE_FIELD] - ) + ref_name = self.reference.get(SerializedReferenceModels.REFERENCE_TYPE_FIELD) + if ref_name and SerializedReferenceModels.is_builtin_reference(ref_name): + return SerializedReferenceModels.get_reference_class(ref_name) + return SerializedReferenceModels.SerializedCustomReference @classmethod @provide_session diff --git a/airflow-core/src/airflow/models/taskinstance.py b/airflow-core/src/airflow/models/taskinstance.py index 8112d955582fd..c8a9c05bd9fd3 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -228,6 +228,85 @@ def _recalculate_dagrun_queued_at_deadlines( # These changes are committed by the calling function. +def _get_new_task_ids( + dag_id: str, + run_id: str, + session: Session, +) -> list[str]: + """ + Get task ids for newly added tasks in the latest DAG version. + + This is a read-only operation that compares the current DAG version + with the latest DAG version to identify new tasks. + + :param dag_id: The dag_id for the DAG + :param run_id: The run_id for the DAG run + :param session: SQLAlchemy session + :return: List of task IDs for newly added tasks + """ + from airflow.models.dagbag import DBDagBag + from airflow.models.dagrun import DagRun + + dag_run = session.scalar(select(DagRun).filter_by(dag_id=dag_id, run_id=run_id)) + if not dag_run: + raise ValueError(f"DagRun with run_id '{run_id}' not found") + + scheduler_dagbag = DBDagBag(load_op_links=False) + latest_dag = scheduler_dagbag.get_latest_version_of_dag(dag_id, session=session) + + if not latest_dag: + raise ValueError(f"Latest DAG version for '{dag_id}' not found") + + current_dag = scheduler_dagbag.get_dag_for_run(dag_run=dag_run, session=session) + new_task_ids = set(latest_dag.task_ids) - set(current_dag.task_ids) if current_dag else set() + + return list(new_task_ids) + + +def _update_dagrun_to_latest_version( + dag_id: str, + run_id: str, + session: Session, +) -> None: + """ + Update the DAG run to the latest DAG version and create task instances for new tasks. + + This mutates the DAG run by updating its created_dag_version_id, bundle_version, + updating all existing task instances to the new dag_version_id, + and creating task instances for newly added tasks. + + :param dag_id: The dag_id for the DAG + :param run_id: The run_id for the DAG run + :param session: SQLAlchemy session + """ + from airflow.models.dagbag import DBDagBag + from airflow.models.dagrun import DagRun + + dag_run = session.scalar(select(DagRun).filter_by(dag_id=dag_id, run_id=run_id)) + if not dag_run: + raise ValueError(f"DagRun with run_id '{run_id}' not found") + + dag_version = DagVersion.get_latest_version(dag_id, session=session) + if not dag_version: + return + + scheduler_dagbag = DBDagBag(load_op_links=False) + latest_dag = scheduler_dagbag.get_latest_version_of_dag(dag_id, session=session) + if not latest_dag: + raise ValueError(f"Latest DAG version for '{dag_id}' not found") + + dag_run.created_dag_version_id = dag_version.id + dag_run.bundle_version = dag_version.bundle_version + dag_run.dag = latest_dag + + for ti in dag_run.get_task_instances(session=session): + ti.dag_version_id = dag_version.id + + dag_run.verify_integrity(session=session, dag_version_id=dag_version.id) + + session.flush() + + def clear_task_instances( tis: list[TaskInstance], session: Session, @@ -1683,6 +1762,7 @@ def xcom_pull( ) -> Any: """:meta private:""" # noqa: D400 # This is only kept for compatibility in tests for now while AIP-72 is in progress. + if dag_id is None: dag_id = self.dag_id if run_id is None: @@ -1714,12 +1794,15 @@ def xcom_pull( ).first() if first is None: # No matching XCom at all. return default + if map_indexes is not None or first.map_index < 0: return XComModel.deserialize_value(first) - # raise RuntimeError("Nothing should hit this anymore") - - # TODO: TaskSDK: We should remove this, but many tests still currently call `ti.run()`. See #45549 + return LazyXComSelectSequence.from_select( + query.with_only_columns(XComModel.value).order_by(None), + order_by=[XComModel.map_index.expression], + session=session, + ) # At this point either task_ids or map_indexes is explicitly multi-value. # Order return values to match task_ids and map_indexes ordering. diff --git a/airflow-core/src/airflow/observability/trace.py b/airflow-core/src/airflow/observability/trace.py deleted file mode 100644 index 6033502c1dcd7..0000000000000 --- a/airflow-core/src/airflow/observability/trace.py +++ /dev/null @@ -1,86 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from __future__ import annotations - -import logging -from collections.abc import Callable -from socket import socket -from typing import TYPE_CHECKING - -from airflow._shared.observability.traces.base_tracer import EmptyTrace, Tracer -from airflow.configuration import conf - -log = logging.getLogger(__name__) - - -class _TraceMeta(type): - factory: Callable[[], Tracer] | None = None - instance: Tracer | EmptyTrace | None = None - - def __new__(cls, name, bases, attrs): - return super().__new__(cls, name, bases, attrs) - - def __getattr__(cls, name: str): - if not cls.factory: - # Lazy initialization of the factory - cls.configure_factory() - if not cls.instance: - cls._initialize_instance() - return getattr(cls.instance, name) - - def _initialize_instance(cls): - """Initialize the trace instance.""" - try: - cls.instance = cls.factory() - except (socket.gaierror, ImportError) as e: - log.error("Could not configure Trace: %s. Using EmptyTrace instead.", e) - cls.instance = EmptyTrace() - - def __call__(cls, *args, **kwargs): - """Ensure the class behaves as a singleton.""" - if not cls.instance: - cls._initialize_instance() - return cls.instance - - def configure_factory(cls): - """Configure the trace factory based on settings.""" - otel_on = conf.getboolean("traces", "otel_on") - - if otel_on: - from airflow.observability.traces import otel_tracer - - cls.factory = staticmethod( - lambda use_simple_processor=False: otel_tracer.get_otel_tracer(cls, use_simple_processor) - ) - else: - # EmptyTrace is a class and not inherently callable. - # Using a lambda ensures it can be invoked as a callable factory. - # staticmethod ensures the lambda is treated as a standalone function - # and avoids passing `cls` as an implicit argument. - cls.factory = staticmethod(lambda: EmptyTrace()) - - def get_constant_tags(cls) -> str | None: - """Get constant tags to add to all traces.""" - return conf.get("traces", "tags", fallback=None) - - -if TYPE_CHECKING: - Trace: EmptyTrace -else: - - class Trace(metaclass=_TraceMeta): - """Empty class for Trace - we use metaclass to inject the right one.""" diff --git a/airflow-core/src/airflow/observability/traces/__init__.py b/airflow-core/src/airflow/observability/traces/__init__.py index 217e5db960782..6bf0019f74708 100644 --- a/airflow-core/src/airflow/observability/traces/__init__.py +++ b/airflow-core/src/airflow/observability/traces/__init__.py @@ -15,3 +15,137 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +from __future__ import annotations + +import logging +import os +from contextlib import contextmanager +from importlib.metadata import entry_points + +from opentelemetry import context, trace +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter +from opentelemetry.sdk.trace.id_generator import RandomIdGenerator +from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + +from airflow.configuration import conf + +log = logging.getLogger(__name__) + +OVERRIDE_SPAN_ID_KEY = context.create_key("override_span_id") +OVERRIDE_TRACE_ID_KEY = context.create_key("override_trace_id") + + +class OverrideableRandomIdGenerator(RandomIdGenerator): + """Lets you override the span id.""" + + def generate_span_id(self): + override = context.get_value(OVERRIDE_SPAN_ID_KEY) + if override is not None: + return override + return super().generate_span_id() + + def generate_trace_id(self): + override = context.get_value(OVERRIDE_TRACE_ID_KEY) + if override is not None: + return override + return super().generate_trace_id() + + +def new_dagrun_trace_carrier() -> dict[str, str]: + """Generate a fresh W3C traceparent carrier without creating a recordable span.""" + gen = RandomIdGenerator() + span_ctx = SpanContext( + trace_id=gen.generate_trace_id(), + span_id=gen.generate_span_id(), + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + ctx = trace.set_span_in_context(NonRecordingSpan(span_ctx)) + carrier: dict[str, str] = {} + TraceContextTextMapPropagator().inject(carrier, context=ctx) + return carrier + + +@contextmanager +def override_ids(trace_id, span_id, ctx=None): + ctx = context.set_value(OVERRIDE_TRACE_ID_KEY, trace_id, context=ctx) + ctx = context.set_value(OVERRIDE_SPAN_ID_KEY, span_id, context=ctx) + token = context.attach(ctx) + try: + yield + finally: + context.detach(token) + + +def _get_backcompat_config() -> tuple[str | None, Resource | None]: + """ + Possibly get deprecated Airflow configs for otel. + + Ideally we return (None, None) here. But if the old configuration is there, + then we will use it. + """ + resource = None + if not os.environ.get("OTEL_SERVICE_NAME") and not os.environ.get("OTEL_RESOURCE_ATTRIBUTES"): + service_name = conf.get("traces", "otel_service", fallback=None) + if service_name: + resource = Resource({"service.name": service_name}) + + endpoint = None + if not os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") and not os.environ.get( + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" + ): + # this is only for backcompat! + host = conf.get("traces", "otel_host", fallback=None) + port = conf.get("traces", "otel_port", fallback=None) + ssl_active = conf.getboolean("traces", "otel_ssl_active", fallback=False) + if host and port: + scheme = "https" if ssl_active else "http" + endpoint = f"{scheme}://{host}:{port}/v1/traces" + return endpoint, resource + + +def _load_exporter_from_env() -> SpanExporter: + """ + Load a span exporter using the OTEL_TRACES_EXPORTER env var. + + Mirrors the entry-point mechanism used by the OTEL SDK auto-instrumentation + configurator. Supported values (from installed packages): + - ``otlp`` (default) — OTLP/gRPC + - ``otlp_proto_http`` — OTLP/HTTP + - ``console`` — stdout (useful for debugging) + """ + exporter_name = os.environ.get("OTEL_TRACES_EXPORTER", "otlp") + eps = entry_points(group="opentelemetry_traces_exporter", name=exporter_name) + ep = next(iter(eps), None) + if ep is None: + raise RuntimeError( + f"No span exporter found for OTEL_TRACES_EXPORTER={exporter_name!r}. " + f"Available: {[e.name for e in entry_points(group='opentelemetry_traces_exporter')]}" + ) + return ep.load()() + + +def configure_otel(): + otel_on = conf.getboolean("traces", "otel_on", fallback=False) + if not otel_on: + return + + # ideally both endpoint and resource are None here + # they would only be something other than None if user is using deprecated + # Airflow-defined otel configs + backcompat_endpoint, resource = _get_backcompat_config() + + # backcompat: if old-style host/port config provided an endpoint, set the + # env var so the exporter (loaded below) picks it up automatically + + otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") + otlp_traces_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") + if backcompat_endpoint and not (otlp_endpoint or otlp_traces_endpoint): + os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = backcompat_endpoint + + provider = TracerProvider(id_generator=OverrideableRandomIdGenerator(), resource=resource) + provider.add_span_processor(BatchSpanProcessor(_load_exporter_from_env())) + trace.set_tracer_provider(provider) diff --git a/airflow-core/src/airflow/observability/traces/otel_tracer.py b/airflow-core/src/airflow/observability/traces/otel_tracer.py deleted file mode 100644 index 8e19f4c69830e..0000000000000 --- a/airflow-core/src/airflow/observability/traces/otel_tracer.py +++ /dev/null @@ -1,49 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from __future__ import annotations - -from typing import TYPE_CHECKING - -from airflow._shared.observability.traces import otel_tracer -from airflow.configuration import conf - -if TYPE_CHECKING: - from airflow._shared.observability.traces.otel_tracer import OtelTrace - - -def get_otel_tracer(cls, use_simple_processor: bool = False) -> OtelTrace: - # The config values have been deprecated and therefore, - # if the user hasn't added them to the config, the default values won't be used. - # A fallback is needed to avoid an exception. - port = None - if conf.has_option("traces", "otel_port"): - port = conf.getint("traces", "otel_port") - - return otel_tracer.get_otel_tracer( - cls, - use_simple_processor, - host=conf.get("traces", "otel_host", fallback=None), - port=port, - ssl_active=conf.getboolean("traces", "otel_ssl_active", fallback=False), - otel_service=conf.get("traces", "otel_service", fallback=None), - debug=conf.getboolean("traces", "otel_debugging_on", fallback=False), - ) - - -def get_otel_tracer_for_task(cls) -> OtelTrace: - return get_otel_tracer(cls, use_simple_processor=True) diff --git a/airflow-core/src/airflow/partition_mappers/allowed_key.py b/airflow-core/src/airflow/partition_mappers/allowed_key.py new file mode 100644 index 0000000000000..8b560f426aa84 --- /dev/null +++ b/airflow-core/src/airflow/partition_mappers/allowed_key.py @@ -0,0 +1,41 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +from typing import Any + +from airflow.partition_mappers.base import PartitionMapper + + +class AllowedKeyMapper(PartitionMapper): + """Partition mapper that validates keys against a set of allowed keys.""" + + def __init__(self, allowed_keys: list[str]) -> None: + self.allowed_keys = allowed_keys + + def to_downstream(self, key: str) -> str: + if key not in self.allowed_keys: + raise ValueError(f"Key {key!r} not in allowed keys {self.allowed_keys}") + return key + + def serialize(self) -> dict[str, Any]: + return {"allowed_keys": self.allowed_keys} + + @classmethod + def deserialize(cls, data: dict[str, Any]) -> PartitionMapper: + return cls(allowed_keys=data["allowed_keys"]) diff --git a/airflow-core/src/airflow/provider.yaml.schema.json b/airflow-core/src/airflow/provider.yaml.schema.json index 1eb74514015c4..ac6b05f30c87b 100644 --- a/airflow-core/src/airflow/provider.yaml.schema.json +++ b/airflow-core/src/airflow/provider.yaml.schema.json @@ -84,29 +84,35 @@ "type": "string" }, "tags": { - "description": "List of tags describing the integration. While we're using RST, only one tag is supported per integration.", + "description": "List of tags describing the integration.", "type": "array", "items": { "type": "string", "enum": [ "ai", + "ai-inference", "alibaba", "apache", "aws", "azure", "dbt", + "embeddings", "gcp", + "generative-ai", "gmp", "google", "kafka", + "llm", + "machine-learning", + "openai", "protocol", "service", "software", + "text-generation", "yandex" ] }, - "minItems": 1, - "maxItems": 1 + "minItems": 1 } }, "additionalProperties": false, diff --git a/airflow-core/src/airflow/secrets/metastore.py b/airflow-core/src/airflow/secrets/metastore.py index bde8423158d3d..e7f25a9f9179f 100644 --- a/airflow-core/src/airflow/secrets/metastore.py +++ b/airflow-core/src/airflow/secrets/metastore.py @@ -57,7 +57,8 @@ def get_connection( ) .limit(1) ) - session.expunge_all() + if conn: + session.expunge(conn) return conn @provide_session @@ -79,7 +80,7 @@ def get_variable( .where(Variable.key == key, or_(Variable.team_name == team_name, Variable.team_name.is_(None))) .limit(1) ) - session.expunge_all() if var_value: + session.expunge(var_value) return var_value.val return None diff --git a/airflow-core/src/airflow/serialization/decoders.py b/airflow-core/src/airflow/serialization/decoders.py index a438010424ffc..8d19a196e9238 100644 --- a/airflow-core/src/airflow/serialization/decoders.py +++ b/airflow-core/src/airflow/serialization/decoders.py @@ -136,6 +136,18 @@ def decode_asset_like(var: dict[str, Any]) -> SerializedAssetBase: raise ValueError(f"deserialization not implemented for DAT {data_type!r}") +def decode_deadline_reference(reference_data: dict): + """Decode a previously serialized deadline reference.""" + ref_name = reference_data.get(SerializedReferenceModels.REFERENCE_TYPE_FIELD) + + if ref_name and SerializedReferenceModels.is_builtin_reference(ref_name): + reference_class = SerializedReferenceModels.get_reference_class(ref_name) + else: + reference_class = SerializedReferenceModels.SerializedCustomReference + + return reference_class.deserialize_reference(reference_data) + + def decode_deadline_alert(encoded_data: dict): """ Decode a previously serialized deadline alert. @@ -147,10 +159,7 @@ def decode_deadline_alert(encoded_data: dict): data = encoded_data.get(Encoding.VAR, encoded_data) reference_data = data[DeadlineAlertFields.REFERENCE] - reference_type = reference_data[SerializedReferenceModels.REFERENCE_TYPE_FIELD] - - reference_class = SerializedReferenceModels.get_reference_class(reference_type) - reference = reference_class.deserialize_reference(reference_data) + reference = decode_deadline_reference(reference_data) return SerializedDeadlineAlert( reference=reference, diff --git a/airflow-core/src/airflow/serialization/definitions/dag.py b/airflow-core/src/airflow/serialization/definitions/dag.py index 4cb32ed35d4d8..a8fa92d12ce9a 100644 --- a/airflow-core/src/airflow/serialization/definitions/dag.py +++ b/airflow-core/src/airflow/serialization/definitions/dag.py @@ -919,6 +919,24 @@ def clear( run_id: str, only_failed: bool = False, only_running: bool = False, + only_new: Literal[True], + dag_run_state: DagRunState = DagRunState.QUEUED, + session: Session = NEW_SESSION, + exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(), + exclude_run_ids: frozenset[str] | None = frozenset(), + run_on_latest_version: bool = False, + ) -> set[str]: ... # pragma: no cover + + @overload + def clear( + self, + *, + dry_run: Literal[True], + task_ids: Collection[str | tuple[str, int]] | None = None, + run_id: str, + only_failed: bool = False, + only_running: bool = False, + only_new: Literal[False] = False, dag_run_state: DagRunState = DagRunState.QUEUED, session: Session = NEW_SESSION, exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(), @@ -934,6 +952,7 @@ def clear( run_id: str, only_failed: bool = False, only_running: bool = False, + only_new: bool = False, dag_run_state: DagRunState = DagRunState.QUEUED, dry_run: Literal[False] = False, session: Session = NEW_SESSION, @@ -986,13 +1005,14 @@ def clear( end_date: datetime.datetime | None = None, only_failed: bool = False, only_running: bool = False, + only_new: bool = False, dag_run_state: DagRunState = DagRunState.QUEUED, dry_run: bool = False, session: Session = NEW_SESSION, exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(), exclude_run_ids: frozenset[str] | None = frozenset(), run_on_latest_version: bool = False, - ) -> int | Iterable[TaskInstance]: + ) -> int | Iterable[TaskInstance] | set[str]: """ Clear a set of task instances associated with the current dag for a specified date range. @@ -1002,6 +1022,7 @@ def clear( :param end_date: The maximum logical_date to clear :param only_failed: Only clear failed tasks :param only_running: Only clear running tasks. + :param only_new: Only newly added tasks in the latest version without clearing existing tasks :param dag_run_state: state to set DagRun to. If set to False, dagrun state will not be changed. :param dry_run: Find the tasks to clear but don't clear them. @@ -1011,7 +1032,27 @@ def clear( tuples that should not be cleared :param exclude_run_ids: A set of ``run_id`` or (``run_id``) """ - from airflow.models.taskinstance import clear_task_instances + from airflow.models.taskinstance import ( + _get_new_task_ids, + _update_dagrun_to_latest_version, + clear_task_instances, + ) + + if only_new: + if task_ids is not None: + raise ValueError("only_new and task_ids are mutually exclusive") + if only_failed: + raise ValueError("only_new and only_failed are mutually exclusive") + if only_running: + raise ValueError("only_new and only_running are mutually exclusive") + if not run_id: + raise ValueError("only_new requires run_id to be specified") + task_ids = _get_new_task_ids(self.dag_id, run_id, session) + + if dry_run: + return set(task_ids) + if task_ids: + _update_dagrun_to_latest_version(self.dag_id, run_id, session) state: list[TaskInstanceState] = [] if only_failed: diff --git a/airflow-core/src/airflow/serialization/definitions/deadline.py b/airflow-core/src/airflow/serialization/definitions/deadline.py index 78adc6b9a7666..93af9ef19e7cc 100644 --- a/airflow-core/src/airflow/serialization/definitions/deadline.py +++ b/airflow-core/src/airflow/serialization/definitions/deadline.py @@ -20,6 +20,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from datetime import datetime, timedelta +from inspect import isclass from typing import TYPE_CHECKING, Any import attrs @@ -62,6 +63,15 @@ class SerializedReferenceModels: REFERENCE_TYPE_FIELD = "reference_type" + @classmethod + def is_builtin_reference(cls, ref_name: str) -> bool: + """Check if a reference type is a built-in reference.""" + return any( + r.__name__ == ref_name + for r in vars(cls).values() + if isclass(r) and issubclass(r, cls.SerializedBaseDeadlineReference) + ) + @classmethod def get_reference_class(cls, reference_name: str) -> type[SerializedBaseDeadlineReference]: """ @@ -99,7 +109,11 @@ def evaluate_with(self, *, session: Session, interval: timedelta, **kwargs: Any) ) if extra_kwargs := kwargs.keys() - filtered_kwargs.keys(): - self.log.debug("Ignoring unexpected parameters: %s", ", ".join(extra_kwargs)) + self.log.debug( + "%s ignoring unexpected parameters: %s", + self.reference_name, + ", ".join(extra_kwargs), + ) base_time = self._evaluate_with(session=session, **filtered_kwargs) return base_time + interval if base_time is not None else None @@ -225,8 +239,19 @@ def _evaluate_with(self, *, session: Session, **kwargs: Any) -> datetime | None: ) return None - avg_duration_seconds = sum(durations) / len(durations) - return timezone.utcnow() + timedelta(seconds=avg_duration_seconds) + # Convert to float to handle Decimal types from MySQL while preserving precision + # Use Decimal arithmetic for higher precision, then convert to float + from decimal import Decimal + + decimal_durations = [Decimal(str(d)) for d in durations] + avg_seconds = float(sum(decimal_durations) / len(decimal_durations)) + logger.info( + "Average runtime for dag_id %s (from %d runs): %.2f seconds", + dag_id, + len(durations), + avg_seconds, + ) + return timezone.utcnow() + timedelta(seconds=avg_seconds) def serialize_reference(self) -> dict: return { @@ -239,6 +264,62 @@ def serialize_reference(self) -> dict: def deserialize_reference(cls, reference_data: dict): return cls(max_runs=reference_data["max_runs"], min_runs=reference_data.get("min_runs")) + class SerializedCustomReference(SerializedBaseDeadlineReference): + """ + Wrapper for custom deadline references. + + This class dynamically delegates to the wrapped reference for required_kwargs and evaluation logic. + """ + + def __init__(self, inner_ref): + self.inner_ref = inner_ref + + @property + def reference_name(self) -> str: + return self.inner_ref.reference_name + + def evaluate_with(self, *, session: Session, interval: timedelta, **kwargs: Any) -> datetime | None: + """Validate the provided kwargs and evaluate this deadline with the given conditions.""" + required_kwargs: set[str] = getattr(self.inner_ref, "required_kwargs", set()) + filtered_kwargs = {k: v for k, v in kwargs.items() if k in required_kwargs} + + if missing_kwargs := required_kwargs - filtered_kwargs.keys(): + raise ValueError( + f"{self.inner_ref.__class__.__name__} is missing required parameters: {', '.join(missing_kwargs)}" + ) + + if extra_kwargs := kwargs.keys() - filtered_kwargs.keys(): + self.log.debug( + "%s ignoring unexpected parameters: %s", + self.reference_name, + ", ".join(extra_kwargs), + ) + + deadline = self.inner_ref._evaluate_with(session=session, **filtered_kwargs) + return deadline + interval if deadline is not None else None + + def _evaluate_with(self, *, session: Session, **kwargs: Any) -> datetime | None: + return self.inner_ref._evaluate_with(session=session, **kwargs) + + def serialize_reference(self) -> dict: + return self.inner_ref.serialize_reference() + + @classmethod + def deserialize_reference(cls, reference_data: dict): + from airflow._shared.module_loading import import_string + + custom_class = import_string(reference_data["__class_path"]) + inner_ref = custom_class.deserialize_reference(reference_data) + return cls(inner_ref) + + def __eq__(self, other) -> bool: + if not isinstance(other, SerializedReferenceModels.SerializedCustomReference): + return False + return self.inner_ref == other.inner_ref + + def __hash__(self) -> int: + return hash(self.inner_ref) + class TYPES: """Collection of SerializedDeadlineReference types for type checking.""" @@ -259,7 +340,9 @@ class TYPES: ) SerializedReferenceModels.TYPES.DAGRUN_QUEUED = (SerializedReferenceModels.DagRunQueuedAtDeadline,) SerializedReferenceModels.TYPES.DAGRUN = ( - SerializedReferenceModels.TYPES.DAGRUN_CREATED + SerializedReferenceModels.TYPES.DAGRUN_QUEUED + *SerializedReferenceModels.TYPES.DAGRUN_CREATED, + *SerializedReferenceModels.TYPES.DAGRUN_QUEUED, + SerializedReferenceModels.SerializedCustomReference, ) diff --git a/airflow-core/src/airflow/serialization/encoders.py b/airflow-core/src/airflow/serialization/encoders.py index 0242b6ee61d37..239b2b1b97bcd 100644 --- a/airflow-core/src/airflow/serialization/encoders.py +++ b/airflow-core/src/airflow/serialization/encoders.py @@ -28,6 +28,7 @@ from airflow._shared.module_loading import qualname from airflow.partition_mappers.base import PartitionMapper as CorePartitionMapper from airflow.sdk import ( + AllowedKeyMapper, Asset, AssetAlias, AssetAll, @@ -201,19 +202,43 @@ def encode_deadline_alert(d: DeadlineAlert | SerializedDeadlineAlert) -> dict[st from airflow.sdk.serde import serialize return { - "reference": d.reference.serialize_reference(), + "reference": encode_deadline_reference(d.reference), "interval": d.interval.total_seconds(), "callback": serialize(d.callback), } +_BUILTIN_DEADLINE_MODULES = ( + "airflow.sdk.definitions.deadline", + "airflow.serialization.definitions.deadline", + # Include airflow.models.deadline to treat core's deadline references as builtins. + # This is to maintain backcompat with 3.1.x custom refs that inherit from + # airflow.models.deadline.ReferenceModels.BaseDeadlineReference. + "airflow.models.deadline", +) + + def encode_deadline_reference(ref) -> dict[str, Any]: """ Encode a deadline reference. + For custom (non-builtin) deadline references, includes the class path + so the decoder can import the user's class at runtime. + :meta private: """ - return ref.serialize_reference() + from airflow._shared.module_loading import qualname + + serialized = ref.serialize_reference() + + # Custom types (not built-in) need __class_path so the decoder can import them. + # Unlike built-in types which are looked up in SerializedReferenceModels, + # custom types are discovered via import_string(__class_path) at deserialization time. + module = type(ref).__module__ + if module not in _BUILTIN_DEADLINE_MODULES: + serialized["__class_path"] = qualname(ref) + + return serialized def _get_serialized_timetable_import_path(var: BaseTimetable | CoreTimetable) -> str: @@ -375,6 +400,7 @@ def _(self, timetable: PartitionedAssetTimetable) -> dict[str, Any]: QuarterlyMapper: "airflow.partition_mappers.temporal.QuarterlyMapper", YearlyMapper: "airflow.partition_mappers.temporal.YearlyMapper", ProductMapper: "airflow.partition_mappers.product.ProductMapper", + AllowedKeyMapper: "airflow.partition_mappers.allowed_key.AllowedKeyMapper", } @functools.singledispatchmethod @@ -416,6 +442,10 @@ def _(self, partition_mapper: ProductMapper) -> dict[str, Any]: "mappers": [encode_partition_mapper(m) for m in partition_mapper.mappers], } + @serialize_partition_mapper.register + def _(self, partition_mapper: AllowedKeyMapper) -> dict[str, Any]: + return {"allowed_keys": partition_mapper.allowed_keys} + _serializer = _Serializer() diff --git a/airflow-core/src/airflow/settings.py b/airflow-core/src/airflow/settings.py index b8bc480ef156f..49d46f652c6fb 100644 --- a/airflow-core/src/airflow/settings.py +++ b/airflow-core/src/airflow/settings.py @@ -38,6 +38,8 @@ ) from sqlalchemy.orm import scoped_session, sessionmaker +from airflow.observability.traces import configure_otel + try: from sqlalchemy.ext.asyncio import async_sessionmaker except ImportError: @@ -722,7 +724,7 @@ def initialize(): load_policy_plugins(policy_mgr) import_local_settings() configure_logging() - + configure_otel() configure_adapters() # The webservers import this file from models.py with the default settings. diff --git a/airflow-core/src/airflow/ti_deps/deps/not_previously_skipped_dep.py b/airflow-core/src/airflow/ti_deps/deps/not_previously_skipped_dep.py index 5238ca97d1c11..907d9f027135d 100644 --- a/airflow-core/src/airflow/ti_deps/deps/not_previously_skipped_dep.py +++ b/airflow-core/src/airflow/ti_deps/deps/not_previously_skipped_dep.py @@ -58,8 +58,15 @@ def _get_dep_statuses(self, ti, session, dep_context): # This can happen if the parent task has not yet run. continue + # Use the parent's map context to look up the XCom. An unmapped parent + # (e.g. LatestOnlyOperator) writes XCom with map_index=-1, so we must + # query with -1 instead of the child's map_index. + xcom_map_index = ti.map_index if parent.is_mapped else -1 prev_result = ti.xcom_pull( - task_ids=parent.task_id, key=XCOM_SKIPMIXIN_KEY, session=session, map_indexes=ti.map_index + task_ids=parent.task_id, + key=XCOM_SKIPMIXIN_KEY, + session=session, + map_indexes=xcom_map_index, ) if prev_result is None: diff --git a/airflow-core/src/airflow/timetables/simple.py b/airflow-core/src/airflow/timetables/simple.py index 082cf90e56dc1..01fb12f81dd0c 100644 --- a/airflow-core/src/airflow/timetables/simple.py +++ b/airflow-core/src/airflow/timetables/simple.py @@ -161,14 +161,12 @@ def next_dagrun_info( last_automated_data_interval: DataInterval | None, restriction: TimeRestriction, ) -> DagRunInfo | None: - if restriction.earliest is None: # No start date, won't run. - return None - current_time = timezone.coerce_datetime(timezone.utcnow()) + start_date = restriction.earliest or current_time if last_automated_data_interval is not None: # has already run once if last_automated_data_interval.end > current_time: # start date is future - start = restriction.earliest + start = start_date elapsed = last_automated_data_interval.end - last_automated_data_interval.start end = start + elapsed.as_timedelta() @@ -176,8 +174,8 @@ def next_dagrun_info( start = last_automated_data_interval.end end = current_time else: # first run - start = restriction.earliest - end = max(restriction.earliest, current_time) + start = start_date + end = max(start_date, current_time) if restriction.latest is not None and end > restriction.latest: return None diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts index 4f32c3773cf30..d74622f5a8ca2 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -143,7 +143,8 @@ export const UseDagRunServiceGetUpstreamAssetEventsKeyFn = ({ dagId, dagRunId }: export type DagRunServiceGetDagRunsDefaultResponse = Awaited>; export type DagRunServiceGetDagRunsQueryResult = UseQueryResult; export const useDagRunServiceGetDagRunsKey = "DagRunServiceGetDagRuns"; -export const UseDagRunServiceGetDagRunsKeyFn = ({ confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }: { +export const UseDagRunServiceGetDagRunsKeyFn = ({ bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }: { + bundleVersion?: string; confContains?: string; dagId: string; dagIdPattern?: string; @@ -180,7 +181,7 @@ export const UseDagRunServiceGetDagRunsKeyFn = ({ confContains, dagId, dagIdPatt updatedAtGte?: string; updatedAtLt?: string; updatedAtLte?: string; -}, queryKey?: Array) => [useDagRunServiceGetDagRunsKey, ...(queryKey ?? [{ confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }])]; +}, queryKey?: Array) => [useDagRunServiceGetDagRunsKey, ...(queryKey ?? [{ bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }])]; export type DagRunServiceWaitDagRunUntilFinishedDefaultResponse = Awaited>; export type DagRunServiceWaitDagRunUntilFinishedQueryResult = UseQueryResult; export const useDagRunServiceWaitDagRunUntilFinishedKey = "DagRunServiceWaitDagRunUntilFinished"; @@ -929,6 +930,7 @@ export type TaskInstanceServicePostClearTaskInstancesMutationResult = Awaited>; export type XcomServiceCreateXcomEntryMutationResult = Awaited>; export type VariableServicePostVariableMutationResult = Awaited>; +export type AuthLinksServiceGenerateTokenMutationResult = Awaited>; export type BackfillServicePauseBackfillMutationResult = Awaited>; export type BackfillServiceUnpauseBackfillMutationResult = Awaited>; export type BackfillServiceCancelBackfillMutationResult = Awaited>; diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts index 2b61c294b2e00..8ccaf76a47a83 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -293,6 +293,7 @@ export const ensureUseDagRunServiceGetUpstreamAssetEventsData = (queryClient: Qu * @param data.runType * @param data.state * @param data.dagVersion +* @param data.bundleVersion * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, dag_id, run_id, logical_date, run_after, start_date, end_date, updated_at, conf, duration, dag_run_id` * @param data.runIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). or the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * @param data.triggeringUserNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). or the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. @@ -301,7 +302,8 @@ export const ensureUseDagRunServiceGetUpstreamAssetEventsData = (queryClient: Qu * @returns DAGRunCollectionResponse Successful Response * @throws ApiError */ -export const ensureUseDagRunServiceGetDagRunsData = (queryClient: QueryClient, { confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }: { +export const ensureUseDagRunServiceGetDagRunsData = (queryClient: QueryClient, { bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }: { + bundleVersion?: string; confContains?: string; dagId: string; dagIdPattern?: string; @@ -338,7 +340,7 @@ export const ensureUseDagRunServiceGetDagRunsData = (queryClient: QueryClient, { updatedAtGte?: string; updatedAtLt?: string; updatedAtLte?: string; -}) => queryClient.ensureQueryData({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }), queryFn: () => DagRunService.getDagRuns({ confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }) }); +}) => queryClient.ensureQueryData({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }), queryFn: () => DagRunService.getDagRuns({ bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }) }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. * 🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state. diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts index 43f9ad49c88ff..57c4174683dfa 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -293,6 +293,7 @@ export const prefetchUseDagRunServiceGetUpstreamAssetEvents = (queryClient: Quer * @param data.runType * @param data.state * @param data.dagVersion +* @param data.bundleVersion * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, dag_id, run_id, logical_date, run_after, start_date, end_date, updated_at, conf, duration, dag_run_id` * @param data.runIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). or the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * @param data.triggeringUserNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). or the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. @@ -301,7 +302,8 @@ export const prefetchUseDagRunServiceGetUpstreamAssetEvents = (queryClient: Quer * @returns DAGRunCollectionResponse Successful Response * @throws ApiError */ -export const prefetchUseDagRunServiceGetDagRuns = (queryClient: QueryClient, { confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }: { +export const prefetchUseDagRunServiceGetDagRuns = (queryClient: QueryClient, { bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }: { + bundleVersion?: string; confContains?: string; dagId: string; dagIdPattern?: string; @@ -338,7 +340,7 @@ export const prefetchUseDagRunServiceGetDagRuns = (queryClient: QueryClient, { c updatedAtGte?: string; updatedAtLt?: string; updatedAtLte?: string; -}) => queryClient.prefetchQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }), queryFn: () => DagRunService.getDagRuns({ confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }) }); +}) => queryClient.prefetchQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }), queryFn: () => DagRunService.getDagRuns({ bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }) }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. * 🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state. diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts index fd4924351c416..33a5ca388165a 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -2,7 +2,7 @@ import { UseMutationOptions, UseQueryOptions, useMutation, useQuery } from "@tanstack/react-query"; import { AssetService, AuthLinksService, BackfillService, CalendarService, ConfigService, ConnectionService, DagParsingService, DagRunService, DagService, DagSourceService, DagStatsService, DagVersionService, DagWarningService, DashboardService, DeadlinesService, DependenciesService, EventLogService, ExperimentalService, ExtraLinksService, GanttService, GridService, ImportErrorService, JobService, LoginService, MonitorService, PartitionedDagRunService, PluginService, PoolService, ProviderService, StructureService, TaskInstanceService, TaskService, TeamsService, VariableService, VersionService, XcomService } from "../requests/services.gen"; -import { BackfillPostBody, BulkBody_BulkTaskInstanceBody_, BulkBody_ConnectionBody_, BulkBody_PoolBody_, BulkBody_VariableBody_, ClearTaskInstancesBody, ConnectionBody, CreateAssetEventsBody, DAGPatchBody, DAGRunClearBody, DAGRunPatchBody, DAGRunsBatchBody, DagRunState, DagWarningType, PatchTaskInstanceBody, PoolBody, PoolPatchBody, TaskInstancesBatchBody, TriggerDAGRunPostBody, UpdateHITLDetailPayload, VariableBody, XComCreateBody, XComUpdateBody } from "../requests/types.gen"; +import { BackfillPostBody, BulkBody_BulkTaskInstanceBody_, BulkBody_ConnectionBody_, BulkBody_PoolBody_, BulkBody_VariableBody_, ClearTaskInstancesBody, ConnectionBody, CreateAssetEventsBody, DAGPatchBody, DAGRunClearBody, DAGRunPatchBody, DAGRunsBatchBody, DagRunState, DagWarningType, GenerateTokenBody, PatchTaskInstanceBody, PoolBody, PoolPatchBody, TaskInstancesBatchBody, TriggerDAGRunPostBody, UpdateHITLDetailPayload, VariableBody, XComCreateBody, XComUpdateBody } from "../requests/types.gen"; import * as Common from "./common"; /** * Get Assets @@ -293,6 +293,7 @@ export const useDagRunServiceGetUpstreamAssetEvents = = unknown[]>({ confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }: { +export const useDagRunServiceGetDagRuns = = unknown[]>({ bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }: { + bundleVersion?: string; confContains?: string; dagId: string; dagIdPattern?: string; @@ -338,7 +340,7 @@ export const useDagRunServiceGetDagRuns = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }, queryKey), queryFn: () => DagRunService.getDagRuns({ confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }) as TData, ...options }); +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }, queryKey), queryFn: () => DagRunService.getDagRuns({ bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }) as TData, ...options }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. * 🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state. @@ -1988,6 +1990,19 @@ export const useVariableServicePostVariable = ({ mutationFn: ({ requestBody }) => VariableService.postVariable({ requestBody }) as unknown as Promise, ...options }); /** +* Generate Token +* Generate a JWT token for the authenticated user. +* @param data The data for the request. +* @param data.requestBody +* @returns GenerateTokenResponse Successful Response +* @throws ApiError +*/ +export const useAuthLinksServiceGenerateToken = (options?: Omit, "mutationFn">) => useMutation({ mutationFn: ({ requestBody }) => AuthLinksService.generateToken({ requestBody }) as unknown as Promise, ...options }); +/** * Pause Backfill * @param data The data for the request. * @param data.backfillId diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts index 87d9580e0317c..2d01abdb58f4f 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -293,6 +293,7 @@ export const useDagRunServiceGetUpstreamAssetEventsSuspense = = unknown[]>({ confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }: { +export const useDagRunServiceGetDagRunsSuspense = = unknown[]>({ bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }: { + bundleVersion?: string; confContains?: string; dagId: string; dagIdPattern?: string; @@ -338,7 +340,7 @@ export const useDagRunServiceGetDagRunsSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }, queryKey), queryFn: () => DagRunService.getDagRuns({ confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }) as TData, ...options }); +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }, queryKey), queryFn: () => DagRunService.getDagRuns({ bundleVersion, confContains, dagId, dagIdPattern, dagVersion, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, partitionKeyPattern, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }) as TData, ...options }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. * 🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state. diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts index 0b41a478c253e..6c540964655d6 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -8087,6 +8087,38 @@ export const $GanttTaskInstance = { description: 'Task instance data for Gantt chart.' } as const; +export const $GenerateTokenBody = { + properties: { + token_type: { + '$ref': '#/components/schemas/TokenType', + default: 'api' + } + }, + type: 'object', + title: 'GenerateTokenBody', + description: 'Request body for generating a token.' +} as const; + +export const $GenerateTokenResponse = { + properties: { + access_token: { + type: 'string', + title: 'Access Token' + }, + token_type: { + '$ref': '#/components/schemas/TokenType' + }, + expires_in_seconds: { + type: 'integer', + title: 'Expires In Seconds' + } + }, + type: 'object', + required: ['access_token', 'token_type', 'expires_in_seconds'], + title: 'GenerateTokenResponse', + description: 'Response for a generated token.' +} as const; + export const $GridNodeResponse = { properties: { id: { @@ -8921,6 +8953,28 @@ export const $Theme = { } ], title: 'Globalcss' + }, + icon: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Icon' + }, + icon_dark_mode: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Icon Dark Mode' } }, type: 'object', @@ -8929,6 +8983,13 @@ export const $Theme = { description: "JSON to modify Chakra's theme." } as const; +export const $TokenType = { + type: 'string', + enum: ['api', 'cli'], + title: 'TokenType', + description: 'Type of token to generate.' +} as const; + export const $UIAlert = { properties: { text: { diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts index 6318b4678f372..d36012e372c2a 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts @@ -3,7 +3,7 @@ import type { CancelablePromise } from './core/CancelablePromise'; import { OpenAPI } from './core/OpenAPI'; import { request as __request } from './core/request'; -import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, DeleteDagAssetQueuedEventsData, DeleteDagAssetQueuedEventsResponse, GetDagAssetQueuedEventData, GetDagAssetQueuedEventResponse, DeleteDagAssetQueuedEventData, DeleteDagAssetQueuedEventResponse, NextRunAssetsData, NextRunAssetsResponse, ListBackfillsData, ListBackfillsResponse, CreateBackfillData, CreateBackfillResponse, GetBackfillData, GetBackfillResponse, PauseBackfillData, PauseBackfillResponse, UnpauseBackfillData, UnpauseBackfillResponse, CancelBackfillData, CancelBackfillResponse, CreateBackfillDryRunData, CreateBackfillDryRunResponse, ListBackfillsUiData, ListBackfillsUiResponse, DeleteConnectionData, DeleteConnectionResponse, GetConnectionData, GetConnectionResponse, PatchConnectionData, PatchConnectionResponse, GetConnectionsData, GetConnectionsResponse, PostConnectionData, PostConnectionResponse, BulkConnectionsData, BulkConnectionsResponse, TestConnectionData, TestConnectionResponse, CreateDefaultConnectionsResponse, HookMetaDataResponse, GetDagRunData, GetDagRunResponse, DeleteDagRunData, DeleteDagRunResponse, PatchDagRunData, PatchDagRunResponse, GetUpstreamAssetEventsData, GetUpstreamAssetEventsResponse, ClearDagRunData, ClearDagRunResponse, GetDagRunsData, GetDagRunsResponse, TriggerDagRunData, TriggerDagRunResponse, WaitDagRunUntilFinishedData, WaitDagRunUntilFinishedResponse, GetListDagRunsBatchData, GetListDagRunsBatchResponse, GetDagSourceData, GetDagSourceResponse, GetDagStatsData, GetDagStatsResponse, GetConfigData, GetConfigResponse, GetConfigValueData, GetConfigValueResponse, GetConfigsResponse, ListDagWarningsData, ListDagWarningsResponse, GetDagsData, GetDagsResponse, PatchDagsData, PatchDagsResponse, GetDagData, GetDagResponse, PatchDagData, PatchDagResponse, DeleteDagData, DeleteDagResponse, GetDagDetailsData, GetDagDetailsResponse, FavoriteDagData, FavoriteDagResponse, UnfavoriteDagData, UnfavoriteDagResponse, GetDagTagsData, GetDagTagsResponse, GetDagsUiData, GetDagsUiResponse, GetLatestRunInfoData, GetLatestRunInfoResponse, GetEventLogData, GetEventLogResponse, GetEventLogsData, GetEventLogsResponse, GetExtraLinksData, GetExtraLinksResponse, GetTaskInstanceData, GetTaskInstanceResponse, PatchTaskInstanceData, PatchTaskInstanceResponse, DeleteTaskInstanceData, DeleteTaskInstanceResponse, GetMappedTaskInstancesData, GetMappedTaskInstancesResponse, GetTaskInstanceDependenciesByMapIndexData, GetTaskInstanceDependenciesByMapIndexResponse, GetTaskInstanceDependenciesData, GetTaskInstanceDependenciesResponse, GetTaskInstanceTriesData, GetTaskInstanceTriesResponse, GetMappedTaskInstanceTriesData, GetMappedTaskInstanceTriesResponse, GetMappedTaskInstanceData, GetMappedTaskInstanceResponse, PatchTaskInstanceByMapIndexData, PatchTaskInstanceByMapIndexResponse, GetTaskInstancesData, GetTaskInstancesResponse, BulkTaskInstancesData, BulkTaskInstancesResponse, GetTaskInstancesBatchData, GetTaskInstancesBatchResponse, GetTaskInstanceTryDetailsData, GetTaskInstanceTryDetailsResponse, GetMappedTaskInstanceTryDetailsData, GetMappedTaskInstanceTryDetailsResponse, PostClearTaskInstancesData, PostClearTaskInstancesResponse, PatchTaskInstanceDryRunByMapIndexData, PatchTaskInstanceDryRunByMapIndexResponse, PatchTaskInstanceDryRunData, PatchTaskInstanceDryRunResponse, GetLogData, GetLogResponse, GetExternalLogUrlData, GetExternalLogUrlResponse, UpdateHitlDetailData, UpdateHitlDetailResponse, GetHitlDetailData, GetHitlDetailResponse, GetHitlDetailTryDetailData, GetHitlDetailTryDetailResponse, GetHitlDetailsData, GetHitlDetailsResponse, GetImportErrorData, GetImportErrorResponse, GetImportErrorsData, GetImportErrorsResponse, GetJobsData, GetJobsResponse, GetPluginsData, GetPluginsResponse, ImportErrorsResponse, DeletePoolData, DeletePoolResponse, GetPoolData, GetPoolResponse, PatchPoolData, PatchPoolResponse, GetPoolsData, GetPoolsResponse, PostPoolData, PostPoolResponse, BulkPoolsData, BulkPoolsResponse, GetProvidersData, GetProvidersResponse, GetXcomEntryData, GetXcomEntryResponse, UpdateXcomEntryData, UpdateXcomEntryResponse, DeleteXcomEntryData, DeleteXcomEntryResponse, GetXcomEntriesData, GetXcomEntriesResponse, CreateXcomEntryData, CreateXcomEntryResponse, GetTasksData, GetTasksResponse, GetTaskData, GetTaskResponse, DeleteVariableData, DeleteVariableResponse, GetVariableData, GetVariableResponse, PatchVariableData, PatchVariableResponse, GetVariablesData, GetVariablesResponse, PostVariableData, PostVariableResponse, BulkVariablesData, BulkVariablesResponse, ReparseDagFileData, ReparseDagFileResponse, GetDagVersionData, GetDagVersionResponse, GetDagVersionsData, GetDagVersionsResponse, GetHealthResponse, GetVersionResponse, LoginData, LoginResponse, LogoutResponse, GetAuthMenusResponse, GetCurrentUserInfoResponse, GetPartitionedDagRunsData, GetPartitionedDagRunsResponse, GetPendingPartitionedDagRunData, GetPendingPartitionedDagRunResponse, GetDependenciesData, GetDependenciesResponse, HistoricalMetricsData, HistoricalMetricsResponse, DagStatsResponse2, GetDagRunDeadlinesData, GetDagRunDeadlinesResponse, StructureDataData, StructureDataResponse2, GetDagStructureData, GetDagStructureResponse, GetGridRunsData, GetGridRunsResponse, GetGridTiSummariesData, GetGridTiSummariesResponse, GetGanttDataData, GetGanttDataResponse, GetCalendarData, GetCalendarResponse, ListTeamsData, ListTeamsResponse } from './types.gen'; +import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, DeleteDagAssetQueuedEventsData, DeleteDagAssetQueuedEventsResponse, GetDagAssetQueuedEventData, GetDagAssetQueuedEventResponse, DeleteDagAssetQueuedEventData, DeleteDagAssetQueuedEventResponse, NextRunAssetsData, NextRunAssetsResponse, ListBackfillsData, ListBackfillsResponse, CreateBackfillData, CreateBackfillResponse, GetBackfillData, GetBackfillResponse, PauseBackfillData, PauseBackfillResponse, UnpauseBackfillData, UnpauseBackfillResponse, CancelBackfillData, CancelBackfillResponse, CreateBackfillDryRunData, CreateBackfillDryRunResponse, ListBackfillsUiData, ListBackfillsUiResponse, DeleteConnectionData, DeleteConnectionResponse, GetConnectionData, GetConnectionResponse, PatchConnectionData, PatchConnectionResponse, GetConnectionsData, GetConnectionsResponse, PostConnectionData, PostConnectionResponse, BulkConnectionsData, BulkConnectionsResponse, TestConnectionData, TestConnectionResponse, CreateDefaultConnectionsResponse, HookMetaDataResponse, GetDagRunData, GetDagRunResponse, DeleteDagRunData, DeleteDagRunResponse, PatchDagRunData, PatchDagRunResponse, GetUpstreamAssetEventsData, GetUpstreamAssetEventsResponse, ClearDagRunData, ClearDagRunResponse, GetDagRunsData, GetDagRunsResponse, TriggerDagRunData, TriggerDagRunResponse, WaitDagRunUntilFinishedData, WaitDagRunUntilFinishedResponse, GetListDagRunsBatchData, GetListDagRunsBatchResponse, GetDagSourceData, GetDagSourceResponse, GetDagStatsData, GetDagStatsResponse, GetConfigData, GetConfigResponse, GetConfigValueData, GetConfigValueResponse, GetConfigsResponse, ListDagWarningsData, ListDagWarningsResponse, GetDagsData, GetDagsResponse, PatchDagsData, PatchDagsResponse, GetDagData, GetDagResponse, PatchDagData, PatchDagResponse, DeleteDagData, DeleteDagResponse, GetDagDetailsData, GetDagDetailsResponse, FavoriteDagData, FavoriteDagResponse, UnfavoriteDagData, UnfavoriteDagResponse, GetDagTagsData, GetDagTagsResponse, GetDagsUiData, GetDagsUiResponse, GetLatestRunInfoData, GetLatestRunInfoResponse, GetEventLogData, GetEventLogResponse, GetEventLogsData, GetEventLogsResponse, GetExtraLinksData, GetExtraLinksResponse, GetTaskInstanceData, GetTaskInstanceResponse, PatchTaskInstanceData, PatchTaskInstanceResponse, DeleteTaskInstanceData, DeleteTaskInstanceResponse, GetMappedTaskInstancesData, GetMappedTaskInstancesResponse, GetTaskInstanceDependenciesByMapIndexData, GetTaskInstanceDependenciesByMapIndexResponse, GetTaskInstanceDependenciesData, GetTaskInstanceDependenciesResponse, GetTaskInstanceTriesData, GetTaskInstanceTriesResponse, GetMappedTaskInstanceTriesData, GetMappedTaskInstanceTriesResponse, GetMappedTaskInstanceData, GetMappedTaskInstanceResponse, PatchTaskInstanceByMapIndexData, PatchTaskInstanceByMapIndexResponse, GetTaskInstancesData, GetTaskInstancesResponse, BulkTaskInstancesData, BulkTaskInstancesResponse, GetTaskInstancesBatchData, GetTaskInstancesBatchResponse, GetTaskInstanceTryDetailsData, GetTaskInstanceTryDetailsResponse, GetMappedTaskInstanceTryDetailsData, GetMappedTaskInstanceTryDetailsResponse, PostClearTaskInstancesData, PostClearTaskInstancesResponse, PatchTaskInstanceDryRunByMapIndexData, PatchTaskInstanceDryRunByMapIndexResponse, PatchTaskInstanceDryRunData, PatchTaskInstanceDryRunResponse, GetLogData, GetLogResponse, GetExternalLogUrlData, GetExternalLogUrlResponse, UpdateHitlDetailData, UpdateHitlDetailResponse, GetHitlDetailData, GetHitlDetailResponse, GetHitlDetailTryDetailData, GetHitlDetailTryDetailResponse, GetHitlDetailsData, GetHitlDetailsResponse, GetImportErrorData, GetImportErrorResponse, GetImportErrorsData, GetImportErrorsResponse, GetJobsData, GetJobsResponse, GetPluginsData, GetPluginsResponse, ImportErrorsResponse, DeletePoolData, DeletePoolResponse, GetPoolData, GetPoolResponse, PatchPoolData, PatchPoolResponse, GetPoolsData, GetPoolsResponse, PostPoolData, PostPoolResponse, BulkPoolsData, BulkPoolsResponse, GetProvidersData, GetProvidersResponse, GetXcomEntryData, GetXcomEntryResponse, UpdateXcomEntryData, UpdateXcomEntryResponse, DeleteXcomEntryData, DeleteXcomEntryResponse, GetXcomEntriesData, GetXcomEntriesResponse, CreateXcomEntryData, CreateXcomEntryResponse, GetTasksData, GetTasksResponse, GetTaskData, GetTaskResponse, DeleteVariableData, DeleteVariableResponse, GetVariableData, GetVariableResponse, PatchVariableData, PatchVariableResponse, GetVariablesData, GetVariablesResponse, PostVariableData, PostVariableResponse, BulkVariablesData, BulkVariablesResponse, ReparseDagFileData, ReparseDagFileResponse, GetDagVersionData, GetDagVersionResponse, GetDagVersionsData, GetDagVersionsResponse, GetHealthResponse, GetVersionResponse, LoginData, LoginResponse, LogoutResponse, GetAuthMenusResponse, GetCurrentUserInfoResponse, GenerateTokenData, GenerateTokenResponse2, GetPartitionedDagRunsData, GetPartitionedDagRunsResponse, GetPendingPartitionedDagRunData, GetPendingPartitionedDagRunResponse, GetDependenciesData, GetDependenciesResponse, HistoricalMetricsData, HistoricalMetricsResponse, DagStatsResponse2, GetDagRunDeadlinesData, GetDagRunDeadlinesResponse, StructureDataData, StructureDataResponse2, GetDagStructureData, GetDagStructureResponse, GetGridRunsData, GetGridRunsResponse, GetGridTiSummariesData, GetGridTiSummariesResponse, GetGanttDataData, GetGanttDataResponse, GetCalendarData, GetCalendarResponse, ListTeamsData, ListTeamsResponse } from './types.gen'; export class AssetService { /** @@ -1004,6 +1004,7 @@ export class DagRunService { * @param data.runType * @param data.state * @param data.dagVersion + * @param data.bundleVersion * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, dag_id, run_id, logical_date, run_after, start_date, end_date, updated_at, conf, duration, dag_run_id` * @param data.runIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). or the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * @param data.triggeringUserNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). or the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. @@ -1050,6 +1051,7 @@ export class DagRunService { run_type: data.runType, state: data.state, dag_version: data.dagVersion, + bundle_version: data.bundleVersion, order_by: data.orderBy, run_id_pattern: data.runIdPattern, triggering_user_name_pattern: data.triggeringUserNamePattern, @@ -3847,6 +3849,26 @@ export class AuthLinksService { }); } + /** + * Generate Token + * Generate a JWT token for the authenticated user. + * @param data The data for the request. + * @param data.requestBody + * @returns GenerateTokenResponse Successful Response + * @throws ApiError + */ + public static generateToken(data: GenerateTokenData): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/ui/auth/token', + body: data.requestBody, + mediaType: 'application/json', + errors: { + 422: 'Validation Error' + } + }); + } + } export class PartitionedDagRunService { diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index db34d1f254885..038c02ee2cd0b 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -1989,6 +1989,22 @@ export type GanttTaskInstance = { is_mapped?: boolean; }; +/** + * Request body for generating a token. + */ +export type GenerateTokenBody = { + token_type?: TokenType; +}; + +/** + * Response for a generated token. + */ +export type GenerateTokenResponse = { + access_token: string; + token_type: TokenType; + expires_in_seconds: number; +}; + /** * Base Node serializer for responses. */ @@ -2207,8 +2223,15 @@ export type Theme = { [key: string]: unknown; }; } | null; + icon?: string | null; + icon_dark_mode?: string | null; }; +/** + * Type of token to generate. + */ +export type TokenType = 'api' | 'cli'; + /** * Optional alert to be shown at the top of the page. */ @@ -2512,6 +2535,7 @@ export type ClearDagRunData = { export type ClearDagRunResponse = TaskInstanceCollectionResponse | DAGRunResponse; export type GetDagRunsData = { + bundleVersion?: string | null; confContains?: string; dagId: string; /** @@ -3530,6 +3554,12 @@ export type GetAuthMenusResponse = MenuItemCollectionResponse; export type GetCurrentUserInfoResponse = AuthenticatedMeResponse; +export type GenerateTokenData = { + requestBody: GenerateTokenBody; +}; + +export type GenerateTokenResponse2 = GenerateTokenResponse; + export type GetPartitionedDagRunsData = { dagId?: string | null; hasCreatedDagRunId?: boolean | null; @@ -6735,6 +6765,21 @@ export type $OpenApiTs = { }; }; }; + '/ui/auth/token': { + post: { + req: GenerateTokenData; + res: { + /** + * Successful Response + */ + 200: GenerateTokenResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; '/ui/partitioned_dag_runs': { get: { req: GetPartitionedDagRunsData; diff --git a/airflow-core/src/airflow/ui/package.json b/airflow-core/src/airflow/ui/package.json index 1c61c86b4352b..dcb556a664669 100644 --- a/airflow-core/src/airflow/ui/package.json +++ b/airflow-core/src/airflow/ui/package.json @@ -26,67 +26,67 @@ }, "dependencies": { "@chakra-ui/anatomy": "^2.3.4", - "@chakra-ui/react": "^3.20.0", + "@chakra-ui/react": "^3.34.0", "@emotion/react": "^11.14.0", "@lezer/highlight": "^1.2.3", "@guanmingchiu/sqlparser-ts": "^0.61.1", "@monaco-editor/react": "^4.7.0", - "@tanstack/react-query": "^5.90.11", + "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.12", + "@tanstack/react-virtual": "^3.13.21", "@visx/group": "^3.12.0", "@visx/shape": "^3.12.0", - "@xyflow/react": "^12.10.0", - "anser": "^2.3.3", - "axios": "^1.13.5", + "@xyflow/react": "^12.10.1", + "anser": "^2.3.5", + "axios": "^1.13.6", "chakra-react-select": "^6.1.1", "chart.js": "^4.5.1", "chartjs-adapter-dayjs-4": "^1.0.4", "chartjs-plugin-annotation": "^3.1.0", "dayjs": "^1.11.19", - "elkjs": "^0.11.0", + "elkjs": "^0.11.1", "html-to-image": "^1.11.13", - "i18next": "^25.6.3", - "i18next-browser-languagedetector": "^8.2.0", + "i18next": "^25.8.14", + "i18next-browser-languagedetector": "^8.2.1", "i18next-http-backend": "^3.0.2", "next-themes": "^0.4.6", "react": "^19.2.4", - "react-chartjs-2": "^5.3.0", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.4", - "react-hook-form": "^7.56.1", + "react-hook-form": "^7.71.2", "react-hotkeys-hook": "^4.6.1", "react-i18next": "^15.5.1", - "react-icons": "^5.5.0", + "react-icons": "^5.6.0", "react-innertext": "^1.1.5", "react-markdown": "^9.1.0", "react-resizable-panels": "^3.0.6", - "react-router-dom": "^7.12.0", + "react-router-dom": "^7.13.1", "react-syntax-highlighter": "^15.6.1", "remark-gfm": "^4.0.1", - "use-debounce": "^10.0.4", + "use-debounce": "^10.1.0", "usehooks-ts": "^3.1.1", - "yaml": "^2.6.1", - "zustand": "^5.0.4" + "yaml": "^2.8.2", + "zustand": "^5.0.11" }, "devDependencies": { "@7nohe/openapi-react-query-codegen": "^1.6.2", "@eslint/compat": "^1.2.9", "@eslint/js": "^9.39.1", - "@playwright/test": "^1.57.0", + "@playwright/test": "^1.58.2", "@stylistic/eslint-plugin": "^2.13.0", - "@tanstack/eslint-plugin-query": "^5.91.2", + "@tanstack/eslint-plugin-query": "^5.91.4", "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.0", + "@testing-library/react": "^16.3.2", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/node": "^24.10.1", - "@types/react": "^19.2.7", + "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.13", - "@typescript-eslint/eslint-plugin": "^8.49.0", - "@typescript-eslint/parser": "^8.49.0", - "@typescript-eslint/utils": "^8.49.0", - "@vitejs/plugin-react": "^5.1.2", - "@vitejs/plugin-react-swc": "^4.0.1", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", + "@typescript-eslint/utils": "^8.56.1", + "@vitejs/plugin-react": "^5.1.4", + "@vitejs/plugin-react-swc": "^4.2.3", "@vitest/coverage-v8": "^3.2.4", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^9.39.1", @@ -95,21 +95,21 @@ "eslint-plugin-jsonc": "^2.21.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-perfectionist": "^4.12.3", - "eslint-plugin-prettier": "^5.2.6", + "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.20", + "eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-unicorn": "^55.0.0", "globals": "^15.15.0", - "happy-dom": "^20.0.11", + "happy-dom": "^20.8.3", "jsonc-eslint-parser": "^2.4.0", - "msw": "^2.12.4", + "msw": "^2.12.10", "openapi-merge-cli": "^1.3.2", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "ts-morph": "^27.0.2", "typescript": "^5.9.3", - "typescript-eslint": "^8.48.1", - "vite": "^7.1.11", + "typescript-eslint": "^8.56.1", + "vite": "^7.3.1", "vite-plugin-css-injected-by-js": "^3.5.2", "vitest": "^3.2.4", "web-worker": "^1.5.0" @@ -121,7 +121,7 @@ "msw" ], "overrides": { - "tar@<7.5.10": ">=7.5.10", + "tar@<7.5.11": ">=7.5.11", "@isaacs/brace-expansion@<=5.0.0": ">=5.0.1", "prismjs@<1.30.0": ">=1.30.0", "rollup@>=4.0.0 <4.59.0": ">=4.59.0", diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml b/airflow-core/src/airflow/ui/pnpm-lock.yaml index ab7277e16e60e..620f60044a257 100644 --- a/airflow-core/src/airflow/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - tar@<7.5.10: '>=7.5.10' + tar@<7.5.11: '>=7.5.11' '@isaacs/brace-expansion@<=5.0.0': '>=5.0.1' prismjs@<1.30.0: '>=1.30.0' rollup@>=4.0.0 <4.59.0: '>=4.59.0' @@ -21,11 +21,11 @@ importers: specifier: ^2.3.4 version: 2.3.4 '@chakra-ui/react': - specifier: ^3.20.0 - version: 3.20.0(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^3.34.0 + version: 3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@emotion/react': specifier: ^11.14.0 - version: 11.14.0(@types/react@19.2.7)(react@19.2.4) + version: 11.14.0(@types/react@19.2.14)(react@19.2.4) '@guanmingchiu/sqlparser-ts': specifier: ^0.61.1 version: 0.61.1 @@ -36,14 +36,14 @@ importers: specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.53.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': - specifier: ^5.90.11 - version: 5.90.12(react@19.2.4) + specifier: ^5.90.21 + version: 5.90.21(react@19.2.4) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-virtual': - specifier: ^3.13.12 - version: 3.13.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^3.13.21 + version: 3.13.21(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@visx/group': specifier: ^3.12.0 version: 3.12.0(react@19.2.4) @@ -51,17 +51,17 @@ importers: specifier: ^3.12.0 version: 3.12.0(react@19.2.4) '@xyflow/react': - specifier: ^12.10.0 - version: 12.10.0(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^12.10.1 + version: 12.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) anser: - specifier: ^2.3.3 - version: 2.3.3 + specifier: ^2.3.5 + version: 2.3.5 axios: - specifier: ^1.13.5 - version: 1.13.5 + specifier: ^1.13.6 + version: 1.13.6 chakra-react-select: specifier: ^6.1.1 - version: 6.1.1(@chakra-ui/react@3.20.0(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.7)(next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 6.1.1(@chakra-ui/react@3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) chart.js: specifier: ^4.5.1 version: 4.5.1 @@ -75,17 +75,17 @@ importers: specifier: ^1.11.19 version: 1.11.19 elkjs: - specifier: ^0.11.0 - version: 0.11.0 + specifier: ^0.11.1 + version: 0.11.1 html-to-image: specifier: ^1.11.13 version: 1.11.13 i18next: - specifier: ^25.6.3 - version: 25.7.1(typescript@5.9.3) + specifier: ^25.8.14 + version: 25.8.14(typescript@5.9.3) i18next-browser-languagedetector: - specifier: ^8.2.0 - version: 8.2.0 + specifier: ^8.2.1 + version: 8.2.1 i18next-http-backend: specifier: ^3.0.2 version: 3.0.2 @@ -96,35 +96,35 @@ importers: specifier: ^19.2.4 version: 19.2.4 react-chartjs-2: - specifier: ^5.3.0 - version: 5.3.0(chart.js@4.5.1)(react@19.2.4) + specifier: ^5.3.1 + version: 5.3.1(chart.js@4.5.1)(react@19.2.4) react-dom: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) react-hook-form: - specifier: ^7.56.1 - version: 7.56.2(react@19.2.4) + specifier: ^7.71.2 + version: 7.71.2(react@19.2.4) react-hotkeys-hook: specifier: ^4.6.1 version: 4.6.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-i18next: specifier: ^15.5.1 - version: 15.5.1(i18next@25.7.1(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + version: 15.5.1(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) react-icons: - specifier: ^5.5.0 - version: 5.5.0(react@19.2.4) + specifier: ^5.6.0 + version: 5.6.0(react@19.2.4) react-innertext: specifier: ^1.1.5 - version: 1.1.5(@types/react@19.2.7)(react@19.2.4) + version: 1.1.5(@types/react@19.2.14)(react@19.2.4) react-markdown: specifier: ^9.1.0 - version: 9.1.0(@types/react@19.2.7)(react@19.2.4) + version: 9.1.0(@types/react@19.2.14)(react@19.2.4) react-resizable-panels: specifier: ^3.0.6 version: 3.0.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-router-dom: - specifier: ^7.12.0 - version: 7.12.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^7.13.1 + version: 7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-syntax-highlighter: specifier: ^15.6.1 version: 15.6.1(react@19.2.4) @@ -132,17 +132,17 @@ importers: specifier: ^4.0.1 version: 4.0.1 use-debounce: - specifier: ^10.0.4 - version: 10.0.4(react@19.2.4) + specifier: ^10.1.0 + version: 10.1.0(react@19.2.4) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.2.4) yaml: - specifier: ^2.6.1 - version: 2.8.0 + specifier: ^2.8.2 + version: 2.8.2 zustand: - specifier: ^5.0.4 - version: 5.0.4(@types/react@19.2.7)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) + specifier: ^5.0.11 + version: 5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: '@7nohe/openapi-react-query-codegen': specifier: ^1.6.2 @@ -154,53 +154,53 @@ importers: specifier: ^9.39.1 version: 9.39.1 '@playwright/test': - specifier: ^1.57.0 - version: 1.57.0 + specifier: ^1.58.2 + version: 1.58.2 '@stylistic/eslint-plugin': specifier: ^2.13.0 version: 2.13.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) '@tanstack/eslint-plugin-query': - specifier: ^5.91.2 - version: 5.91.2(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + specifier: ^5.91.4 + version: 5.91.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 '@testing-library/react': - specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@trivago/prettier-plugin-sort-imports': specifier: ^4.3.0 - version: 4.3.0(prettier@3.7.4) + version: 4.3.0(prettier@3.8.1) '@types/node': specifier: ^24.10.1 version: 24.10.3 '@types/react': - specifier: ^19.2.7 - version: 19.2.7 + specifier: ^19.2.14 + version: 19.2.14 '@types/react-dom': specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.7) + version: 19.2.3(@types/react@19.2.14) '@types/react-syntax-highlighter': specifier: ^15.5.13 version: 15.5.13 '@typescript-eslint/eslint-plugin': - specifier: ^8.49.0 - version: 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + specifier: ^8.56.1 + version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/parser': - specifier: ^8.49.0 - version: 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + specifier: ^8.56.1 + version: 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/utils': - specifier: ^8.49.0 - version: 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + specifier: ^8.56.1 + version: 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) '@vitejs/plugin-react': - specifier: ^5.1.2 - version: 5.1.2(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0)) + specifier: ^5.1.4 + version: 5.1.4(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2)) '@vitejs/plugin-react-swc': - specifier: ^4.0.1 - version: 4.2.2(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0)) + specifier: ^4.2.3 + version: 4.2.3(@swc/helpers@0.5.19)(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2)) '@vitest/coverage-v8': specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@1.21.7)(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.0)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.8.3)(jiti@1.21.7)(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.2)) babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 @@ -223,8 +223,8 @@ importers: specifier: ^4.12.3 version: 4.15.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-prettier: - specifier: ^5.2.6 - version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.7.4) + specifier: ^5.5.5 + version: 5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.8.1) eslint-plugin-react: specifier: ^7.37.5 version: 7.37.5(eslint@9.39.1(jiti@1.21.7)) @@ -232,8 +232,8 @@ importers: specifier: ^7.0.1 version: 7.0.1(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-react-refresh: - specifier: ^0.4.20 - version: 0.4.24(eslint@9.39.1(jiti@1.21.7)) + specifier: ^0.5.2 + version: 0.5.2(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-unicorn: specifier: ^55.0.0 version: 55.0.0(eslint@9.39.1(jiti@1.21.7)) @@ -241,20 +241,20 @@ importers: specifier: ^15.15.0 version: 15.15.0 happy-dom: - specifier: ^20.0.11 - version: 20.0.11 + specifier: ^20.8.3 + version: 20.8.3 jsonc-eslint-parser: specifier: ^2.4.0 version: 2.4.1 msw: - specifier: ^2.12.4 - version: 2.12.4(@types/node@24.10.3)(typescript@5.9.3) + specifier: ^2.12.10 + version: 2.12.10(@types/node@24.10.3)(typescript@5.9.3) openapi-merge-cli: specifier: ^1.3.2 version: 1.3.2 prettier: - specifier: ^3.7.4 - version: 3.7.4 + specifier: ^3.8.1 + version: 3.8.1 ts-morph: specifier: ^27.0.2 version: 27.0.2 @@ -262,17 +262,17 @@ importers: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.48.1 - version: 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + specifier: ^8.56.1 + version: 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) vite: - specifier: ^7.1.11 - version: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + specifier: ^7.3.1 + version: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0)) + version: 3.5.2(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@1.21.7)(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.8.3)(jiti@1.21.7)(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.2) web-worker: specifier: ^1.5.0 version: 1.5.0 @@ -300,8 +300,8 @@ packages: resolution: {integrity: sha512-9K6xOqeevacvweLGik6LnZCb1fBtCOSIWQs8d096XGeqoLKC33UVMGz9+77Gw44KvbH4pKcQPWo4ZpxkXYj05w==} engines: {node: '>= 16'} - '@ark-ui/react@5.12.0': - resolution: {integrity: sha512-UV89EqyESZoyr6rtvrbFJn/FejpswhvRVcfK44dZDU6h6UY8CxfR/6Ayvrq9UtFdD0dEawqwWrXS22l8Y05Nnw==} + '@ark-ui/react@5.34.1': + resolution: {integrity: sha512-RJlXCvsHzbK9LVxUVtaSD5pyF1PL8IUR1rHHkf0H0Sa397l6kOFE4EH7MCSj3pDumj2NsmKDVeVgfkfG0KCuEw==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' @@ -322,10 +322,18 @@ packages: resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + '@babel/core@7.28.5': resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} engines: {node: '>=6.9.0'} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.17.7': resolution: {integrity: sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==} engines: {node: '>=6.9.0'} @@ -338,10 +346,18 @@ packages: resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + '@babel/helper-environment-visitor@7.24.7': resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} engines: {node: '>=6.9.0'} @@ -366,14 +382,24 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.28.3': resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} '@babel/helper-split-export-declaration@7.24.7': @@ -408,6 +434,10 @@ packages: resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.26.10': resolution: {integrity: sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==} engines: {node: '>=6.0.0'} @@ -418,6 +448,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -434,30 +469,30 @@ packages: resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.4': - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.26.9': - resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.23.2': - resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.26.10': - resolution: {integrity: sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==} + '@babel/traverse@7.23.2': + resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} engines: {node: '>=6.9.0'} '@babel/traverse@7.28.5': resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.17.0': resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==} engines: {node: '>=6.9.0'} @@ -470,6 +505,10 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -477,8 +516,8 @@ packages: '@chakra-ui/anatomy@2.3.4': resolution: {integrity: sha512-fFIYN7L276gw0Q7/ikMMlZxP7mvnjRaWJ7f3Jsf9VtDOi6eAYIBRrhQe6+SZ0PGmoOkRaBc7gSE5oeIbgFFyrw==} - '@chakra-ui/react@3.20.0': - resolution: {integrity: sha512-zHYQAUqrT2pZZ/Xi+sskRC/An9q4ZelLPJkFHdobftTYkcFo1FtkMbBO0AEBZhb/6mZGyfw3JLflSawkuR++uQ==} + '@chakra-ui/react@3.34.0': + resolution: {integrity: sha512-VLhpVwv5IVxhwajO10KnS1VQT4hDqQMQP/A796Ya+uVu8AdoSX+5HHyTLTkYIeXIDMe0xLqJfov04OBKbBchJA==} peerDependencies: '@emotion/react': '>=11' react: '>=18' @@ -493,8 +532,8 @@ packages: '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} - '@emotion/is-prop-valid@1.3.1': - resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} '@emotion/memoize@0.9.0': resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} @@ -528,158 +567,158 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} - '@esbuild/aix-ppc64@0.25.11': - resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.11': - resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.11': - resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.11': - resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.11': - resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.11': - resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.11': - resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.11': - resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.11': - resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.11': - resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.11': - resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.11': - resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.11': - resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.11': - resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.11': - resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.11': - resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.11': - resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.11': - resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.11': - resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.11': - resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.11': - resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.11': - resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.11': - resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.11': - resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.11': - resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.11': - resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -696,10 +735,20 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/compat@1.2.9': resolution: {integrity: sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -740,9 +789,18 @@ packages: '@floating-ui/core@1.7.1': resolution: {integrity: sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + '@floating-ui/dom@1.7.1': resolution: {integrity: sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==} + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} @@ -777,8 +835,12 @@ packages: resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} - '@inquirer/confirm@5.1.8': - resolution: {integrity: sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg==} + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -786,8 +848,8 @@ packages: '@types/node': optional: true - '@inquirer/core@10.1.9': - resolution: {integrity: sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==} + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -795,12 +857,12 @@ packages: '@types/node': optional: true - '@inquirer/figures@1.0.11': - resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} - '@inquirer/type@3.0.5': - resolution: {integrity: sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==} + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -808,11 +870,11 @@ packages: '@types/node': optional: true - '@internationalized/date@3.8.1': - resolution: {integrity: sha512-PgVE6B6eIZtzf9Gu5HvJxRK3ufUFz9DhspELuhW/N0GuMGMTLvPQNRkHP2hTuP9lblOk+f+1xi96sPiPXANXAA==} + '@internationalized/date@3.11.0': + resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==} - '@internationalized/number@3.6.2': - resolution: {integrity: sha512-E5QTOlMg9wo5OrKdHD6edo1JJlIoOsylh0+mbf0evi1tHJwMZfJSaBpGtnJV9N7w3jeiioox9EG/EWRWPh82vg==} + '@internationalized/number@3.6.5': + resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -882,8 +944,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@mswjs/interceptors@0.40.0': - resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} + '@mswjs/interceptors@0.41.3': + resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} engines: {node: '>=18'} '@open-draft/deferred-promise@2.2.0': @@ -895,8 +957,8 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@pandacss/is-valid-prop@0.53.6': - resolution: {integrity: sha512-TgWBQmz/5j/oAMjavqJAjQh1o+yxhYspKvepXPn4lFhAN3yBhilrw9HliAkvpUr0sB2CkJ2BYMpFXbAJYEocsA==} + '@pandacss/is-valid-prop@1.9.0': + resolution: {integrity: sha512-AZvpXWGyjbHc8TC+YVloQ31Z2c4j2xMvYj6UfVxuZdB5w4c9+4N8wy5R7I/XswNh8e4cfUlkvsEGDXjhJRgypw==} '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -906,16 +968,20 @@ packages: resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.57.0': - resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} engines: {node: '>=18'} hasBin: true - '@rolldown/pluginutils@1.0.0-beta.47': - resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} + '@rolldown/pluginutils@1.0.0-rc.2': + resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} - '@rolldown/pluginutils@1.0.0-beta.53': - resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} @@ -951,66 +1017,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -1048,68 +1127,72 @@ packages: peerDependencies: eslint: '>=8.40.0' - '@swc/core-darwin-arm64@1.13.5': - resolution: {integrity: sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==} + '@swc/core-darwin-arm64@1.15.18': + resolution: {integrity: sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.13.5': - resolution: {integrity: sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==} + '@swc/core-darwin-x64@1.15.18': + resolution: {integrity: sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.13.5': - resolution: {integrity: sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==} + '@swc/core-linux-arm-gnueabihf@1.15.18': + resolution: {integrity: sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.13.5': - resolution: {integrity: sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==} + '@swc/core-linux-arm64-gnu@1.15.18': + resolution: {integrity: sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] - '@swc/core-linux-arm64-musl@1.13.5': - resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==} + '@swc/core-linux-arm64-musl@1.15.18': + resolution: {integrity: sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] - '@swc/core-linux-x64-gnu@1.13.5': - resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==} + '@swc/core-linux-x64-gnu@1.15.18': + resolution: {integrity: sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] - '@swc/core-linux-x64-musl@1.13.5': - resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==} + '@swc/core-linux-x64-musl@1.15.18': + resolution: {integrity: sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] - '@swc/core-win32-arm64-msvc@1.13.5': - resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==} + '@swc/core-win32-arm64-msvc@1.15.18': + resolution: {integrity: sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.13.5': - resolution: {integrity: sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==} + '@swc/core-win32-ia32-msvc@1.15.18': + resolution: {integrity: sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.13.5': - resolution: {integrity: sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==} + '@swc/core-win32-x64-msvc@1.15.18': + resolution: {integrity: sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.13.5': - resolution: {integrity: sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==} + '@swc/core@1.15.18': + resolution: {integrity: sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1120,22 +1203,26 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.19': + resolution: {integrity: sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==} '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} - '@tanstack/eslint-plugin-query@5.91.2': - resolution: {integrity: sha512-UPeWKl/Acu1IuuHJlsN+eITUHqAaa9/04geHHPedY8siVarSaWprY0SVMKrkpKfk5ehRT7+/MZ5QwWuEtkWrFw==} + '@tanstack/eslint-plugin-query@5.91.4': + resolution: {integrity: sha512-8a+GAeR7oxJ5laNyYBQ6miPK09Hi18o5Oie/jx8zioXODv/AUFLZQecKabPdpQSLmuDXEBPKFh+W5DKbWlahjQ==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@tanstack/query-core@5.90.12': - resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} + '@tanstack/query-core@5.90.20': + resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} - '@tanstack/react-query@5.90.12': - resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==} + '@tanstack/react-query@5.90.21': + resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==} peerDependencies: react: ^18 || ^19 @@ -1146,8 +1233,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.12': - resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + '@tanstack/react-virtual@3.13.21': + resolution: {integrity: sha512-SYXFrmrbPgXBvf+HsOsKhFgqSe4M6B29VHOsX9Jih9TlNkNkDWx0hWMiMLUghMEzyUz772ndzdEeCEBx+3GIZw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1156,8 +1243,8 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.12': - resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@tanstack/virtual-core@3.13.21': + resolution: {integrity: sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw==} '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -1167,8 +1254,8 @@ packages: resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@16.3.0': - resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 @@ -1299,9 +1386,6 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@20.19.23': - resolution: {integrity: sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==} - '@types/node@24.10.3': resolution: {integrity: sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==} @@ -1311,9 +1395,6 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - '@types/prop-types@15.7.14': - resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1327,11 +1408,8 @@ packages: peerDependencies: '@types/react': '*' - '@types/react@18.3.19': - resolution: {integrity: sha512-fcdJqaHOMDbiAwJnXv6XCzX0jDW77yI3tJqYh1Byn8EL5/S628WRx9b/y3DnNe55zTukUQKrfYxiZls2dHcUMw==} - - '@types/react@19.2.7': - resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -1348,122 +1426,70 @@ packages: '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} - '@typescript-eslint/eslint-plugin@8.48.1': - resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.48.1 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.49.0': - resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} + '@typescript-eslint/eslint-plugin@8.56.1': + resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.49.0 - eslint: ^8.57.0 || ^9.0.0 + '@typescript-eslint/parser': ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.48.1': - resolution: {integrity: sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==} + '@typescript-eslint/parser@8.56.1': + resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.49.0': - resolution: {integrity: sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.48.1': - resolution: {integrity: sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.49.0': - resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/scope-manager@8.48.1': - resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/scope-manager@8.49.0': - resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.48.1': - resolution: {integrity: sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==} + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.49.0': - resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.48.1': - resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==} + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.49.0': - resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} + '@typescript-eslint/type-utils@8.56.1': + resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/types@8.48.1': resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.49.0': - resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.48.1': - resolution: {integrity: sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==} + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.49.0': - resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.48.1': - resolution: {integrity: sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.49.0': - resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/visitor-keys@8.48.1': - resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.49.0': - resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -1488,14 +1514,14 @@ packages: '@visx/vendor@3.12.0': resolution: {integrity: sha512-SVO+G0xtnL9dsNpGDcjCgoiCnlB3iLSM9KLz1sLbSrV7RaVXwY3/BTm2X9OWN1jH2a9M+eHt6DJ6sE6CXm4cUg==} - '@vitejs/plugin-react-swc@4.2.2': - resolution: {integrity: sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA==} + '@vitejs/plugin-react-swc@4.2.3': + resolution: {integrity: sha512-QIluDil2prhY1gdA3GGwxZzTAmLdi8cQ2CcuMW4PB/Wu4e/1pzqrwhYWVd09LInCRlDUidQjd0B70QWbjWtLxA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4 || ^5 || ^6 || ^7 - '@vitejs/plugin-react@5.1.2': - resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} + '@vitejs/plugin-react@5.1.4': + resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -1538,224 +1564,243 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@xyflow/react@12.10.0': - resolution: {integrity: sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==} + '@xyflow/react@12.10.1': + resolution: {integrity: sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==} peerDependencies: react: '>=17' react-dom: '>=17' - '@xyflow/system@0.0.74': - resolution: {integrity: sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==} + '@xyflow/system@0.0.75': + resolution: {integrity: sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==} - '@zag-js/accordion@1.15.0': - resolution: {integrity: sha512-EKNeuKx+lOQ/deCe/ApCjVPxpxpDwT2NXvMPL+YvqXmSv7hAnTLs9fDKjbDUQUMmsyx32BsBd8t6d17DL3rPXg==} + '@zag-js/accordion@1.35.3': + resolution: {integrity: sha512-wmw6yo5Zr6ShiKGTc5ICEOJCurWAOSGubIpGISiHi3cZ4tlxKF/vpATIUT3eq8xzdB56YK57yKCujs/WmwqqoA==} - '@zag-js/anatomy@1.15.0': - resolution: {integrity: sha512-r0l5I7mSsF35HdwXm22TppNhfVftFuqvKfHvTUw+wQZhni4eUL93HypJD0Fl7mDhtP5zfVGfBwR048OzD0+tCw==} + '@zag-js/anatomy@1.35.3': + resolution: {integrity: sha512-oqU9iLNNylrtJMBX5Xu4DsxnPNvtZLiobryv2oNtsDI1mi1Fca/XHghQC9K5aYT0qNsmHj1M3W5WAWTaOtPLkQ==} - '@zag-js/angle-slider@1.15.0': - resolution: {integrity: sha512-xIZBa9V6d05uK7+XQVhfdsThqbZKimSYVxtMOWJfG0sKn63N9VGPxL1OtOMq7FA4IP3SyvlelsGt+3t82TUiyA==} + '@zag-js/angle-slider@1.35.3': + resolution: {integrity: sha512-HXRlmsbNEJSBT53fq9XQKL/vwZWwJC3nprskI7s4f/jy8a4uXPTlv7N7zuBYjew+ScTMzZah6fLWzUztBehmSg==} - '@zag-js/aria-hidden@1.15.0': - resolution: {integrity: sha512-3ogglAasycekTHI34ph16mqwM+VtHCOMtrFHWzPwB16itV5oDEeeMNdQXenHSSyQ/07nJ2QsRGFFjGhPm1kWNg==} + '@zag-js/aria-hidden@1.35.3': + resolution: {integrity: sha512-dk5POebn10WneQfLrEgbTzwolaXWpCSHL6F3jCTinW9IbOx7BXghzJD21iU5Iun+y9CorqJPW3p7LplYNUMO5Q==} - '@zag-js/auto-resize@1.15.0': - resolution: {integrity: sha512-EXgrsU7OWxc7obSOt8Okh0144H8DQi1S84OsOUY04Uni11Dnp5/X8+t6mvBbkw4/Qyz5UBjChjocwBcO+HHV8w==} + '@zag-js/async-list@1.35.3': + resolution: {integrity: sha512-SXX3wGzLK/maKS1PJ3XfLIGWbu0022f/OhcFsT1PbiHnoFZTH7h2fBhirrCBfy2TYFQ6r5uxgjkhPUNkuaeYnA==} - '@zag-js/avatar@1.15.0': - resolution: {integrity: sha512-EHGxzXb1mLf3n6x0z/rqFl1mghDB/gyfPAeaFUoA/cacmmMk8YB3aDUXkS9pTgN9stYJBM5f6T4xB1ZUhrP8tg==} + '@zag-js/auto-resize@1.35.3': + resolution: {integrity: sha512-ufG8HSqzLd9h5rnos8aumj8iORlRskeR/gbpJu1NHrnHBWIrpuXm6KJJR2oZhTFY1BUMMk8eYIBA2QkVuiJzWA==} - '@zag-js/carousel@1.15.0': - resolution: {integrity: sha512-ZI9H34f2utdJ2Ek6GZa+iuRH4eC99GHD/VEOKLdGani8uadpT2v8M5kUwPGrlAJq9SiPbQ2UuXBmCkmurPQqdA==} + '@zag-js/avatar@1.35.3': + resolution: {integrity: sha512-lbQ2Q4Va8AAScKULOHw2tCQez+0JRYGHSMFq6i+dJmeT3dlSgRanm69ra6K2po6hM9E4v6pRe+xOVE+9QMDnuA==} - '@zag-js/checkbox@1.15.0': - resolution: {integrity: sha512-6lQvPQNJXt7R0xxdpOuh2qtmAkzdBdqSvFIH7fE6GJzJ/AWiRZh0X+9deLQ76CN4EDUdxizEe7MlQfTI3a56aw==} + '@zag-js/carousel@1.35.3': + resolution: {integrity: sha512-F+b8HzUeZfB+xUkAkLG4r0Ubui8pj7pSgZhi26ZiWgsM7tsd7cD+xRMXkvPEITN5Fd5QCe3KlVBuE00w5byjmg==} - '@zag-js/clipboard@1.15.0': - resolution: {integrity: sha512-Q3kh0fHvOEAJUywQm3zAWyltrYyiI8OpeZQ18k5Mf3/M+bq3gSphZL0+AYsgGbKUg5O2+hJ1SfiErAjyhRtBQA==} + '@zag-js/cascade-select@1.35.3': + resolution: {integrity: sha512-Nifdx77hEuAdXqr1wpZSPjLXqygRhq/WvnPjGhCeSqFPpy62uT4JZ3avyjUZ4I0UhvIpkleUcXtFwQ3cSMh4ww==} - '@zag-js/collapsible@1.15.0': - resolution: {integrity: sha512-GX0kdMlKk4Yk5k/2wN0prudf21k+TfArGr4EHqimTDR0vQE3dSdb3pYyPjw20fLzceKHBBCLsoi2v+YnS75gHA==} + '@zag-js/checkbox@1.35.3': + resolution: {integrity: sha512-8XBt/Wg2zSQWqV2ZFqZBQUjYRkOYHA2O3IEi0VVYtds3S1n7Pu/HqkZT5qDw+E/SY2+X9Uyx4hO7h2XrlsiZQQ==} - '@zag-js/collection@1.15.0': - resolution: {integrity: sha512-oC3i6c/oP/FuNPsfgoC1reSXbAvDBGXl0HU3CcvXiNLHbjg2ek8J7kbow6MNuXK6chiksiOHbzKxHl2Oo0Ox7A==} + '@zag-js/clipboard@1.35.3': + resolution: {integrity: sha512-obTwynBpp6c17fLHe5tg//FQ497QsyCEry+K3bTdlrivWW200wvfHxZ6RKVbKwDAwhH+ye0bI1xkYAId8j7sdA==} - '@zag-js/color-picker@1.15.0': - resolution: {integrity: sha512-DGujS24h1OWkYL+TWyd+xukOO8NBgcSfFCINffa4ivkHtNx3nC28qkwLPRASbl7AK69pbrcuO6bx1Sy/JQJw0Q==} + '@zag-js/collapsible@1.35.3': + resolution: {integrity: sha512-IweG8JOBCerJwLO6QzTZGEMlsYUmQfQSeD0jniFguMM8vcunvGVSrM+AaL8pDbmXd+snXokaGyJpGO3vzMW6Fw==} - '@zag-js/color-utils@1.15.0': - resolution: {integrity: sha512-SKo+p5Fu0TBtdDua8UHVjptOkwLLBFoD499Z1FER/gr0R/97L03Kdir0YTxvKn5pXWXYY1EQn4hpTuTITN16lQ==} + '@zag-js/collection@1.35.3': + resolution: {integrity: sha512-BYoWJ4b7ma2PgiuQbRSnP603f2DlK6se5JtViUHTamZScLLLWnWHuQ6zFa1KS5kiIkbb7CFM6/bJ3WNYLch8Ig==} - '@zag-js/combobox@1.15.0': - resolution: {integrity: sha512-HBck3wcEeIOa7IQMsUkUKbm9cAU7bjoklIyq2zFGn90k7DcDa++oXK9Z2pmcd4TPoBYiyVuuXucaCcjmLX8V/Q==} + '@zag-js/color-picker@1.35.3': + resolution: {integrity: sha512-i9roSgtqeA1b4Q+jWqnxjXB//BQXMP5m1FQ4YcZVq/0yT14A53JIknchuqrh3wC3yPsJMXFqCoKg+NET2+OVig==} - '@zag-js/core@1.15.0': - resolution: {integrity: sha512-P/8F3IXabMhpFnc6hC7GDg3rvUnvY27cuZU04hxjUqTH6+SfORIA/Uvqd4ekhC+dIprL9jicnFrmGgcyelyxfQ==} + '@zag-js/color-utils@1.35.3': + resolution: {integrity: sha512-vxkEVgz4YdSbdaPvjiRI1VsJAdwzu/dUNvzqOaiVcPDrHr/FFgmUbv0SOFjnfSb2QWGI8EDEMn02RW9ym+BzGw==} - '@zag-js/date-picker@1.15.0': - resolution: {integrity: sha512-IZD0V9MAljp1QhxYbST80AonryuDnyx7hvEy/RrBY/VOx6I4STtKfcSJ5ZZgVIzJfH8Yyaed4+IwcenqG7W5YQ==} + '@zag-js/combobox@1.35.3': + resolution: {integrity: sha512-s1qmttTGJTMjlDakL+uvWSEggpafKr1vhOeZCh8j+N4eFt9bLAwaffjuh/1JzWBvzovw7WoMVkizdTXPlN8oYg==} + + '@zag-js/core@1.35.3': + resolution: {integrity: sha512-fGAHyqOYSEFmo52t7wI4dvbFfLyJmUlyf7wknsiUlzUHlrn3yv5PAZYZ2TibpOD1hwXIp4AoCjbiIPPZBxirZw==} + + '@zag-js/date-picker@1.35.3': + resolution: {integrity: sha512-4G10h6pzzLbd84SE2CKtqi6Z9wEBhSyx4GRSxxy3tsf5wAxnz4anRFat9CGwn2YVUYcUJpD+umYgBMPt6zGDnA==} peerDependencies: '@internationalized/date': '>=3.0.0' - '@zag-js/date-utils@1.15.0': - resolution: {integrity: sha512-FX9EesJRnUTYTpbXf5EVfCbsXW5vYtZfc635aQzojc9ekk1FGcHpqQs8ZKfCOTPuauZFOX9i6139A4KoPfQOiw==} + '@zag-js/date-utils@1.35.3': + resolution: {integrity: sha512-1co0FPpZ6nO5dN8sZtECkMYaf+3E5zu0KSIJZpZiXb4TgsZMDyHu7K7IsiKFHk9qmhuF6AdPpNxBju91pSXMFg==} peerDependencies: '@internationalized/date': '>=3.0.0' - '@zag-js/dialog@1.15.0': - resolution: {integrity: sha512-Vlt5vySs4u8c8xBEh2JMUvRfPc+aaVEIIUtFVxpc2ORWhBXs9glijyp1yf3rNHJhjj8gqqhF5sEvs3yUTTAk+Q==} + '@zag-js/dialog@1.35.3': + resolution: {integrity: sha512-byosV+aBHH5LoFKnjEgC7WdqJid7bP9UhgWLSC7+IXbxrif9Czg1YVp6ZlQM6Nx6uD1vnty4touI3P7D7CTKcw==} + + '@zag-js/dismissable@1.35.3': + resolution: {integrity: sha512-XPk+lqmsZp2Z1yMb5K1yj/e7Sobv4D7zK66B1GS97lk9Xzz8vuSgsimcLy0p7RXQl3KL6H5L69inSuQa2exybQ==} + + '@zag-js/dom-query@1.35.3': + resolution: {integrity: sha512-1RbFZoT4CjlHN9TUNse1++ZVOyKo45ktucTIT349o6HMsoWWKmTJDPvFkMBbmu/qY6XXn4dT+LJEp4bL3DR+Qw==} + + '@zag-js/drawer@1.35.3': + resolution: {integrity: sha512-DN5bwa7bDCDaUSbNzFxMc2U/WmbLcXvPSQjyOpKI6CC3VbW2kKaOnjJ5qQG+W5YBO0FpmJBtaxRV7lke4sZH2w==} - '@zag-js/dismissable@1.15.0': - resolution: {integrity: sha512-yv575KWy8gA1p4aajOiY5l/nBQ3Xw+Mrjpungp1+wiGd/98eNAIKJ6/adldfbE1Ygd/Q4Dx2VQ7D1AmiTdwUSw==} + '@zag-js/editable@1.35.3': + resolution: {integrity: sha512-HcjeacS61vQXfNT9IalZj/+oS45yW5bIDO2NjJWV7zNe5AG29NCceUnvBhy+hrUKPnKcjfDocdW5rCL+Lvs/CQ==} - '@zag-js/dom-query@1.15.0': - resolution: {integrity: sha512-z8H/j/Zs0eZEsGpbonScmlKSv0jEXKiAwUCrvQ9Mt6Gz9n0CQRM3MkFclSsM8aeiSv6qKLlhPfkzjl18OLkbgA==} + '@zag-js/file-upload@1.35.3': + resolution: {integrity: sha512-oIYwnDct4ERo2mfmcxsBIJnlmpzjrzYx82SQsXWD3NGKx3cgdh2lwBX+ebItaLH1jkgzBa3z0TWxc6rfvcUXbw==} - '@zag-js/editable@1.15.0': - resolution: {integrity: sha512-F14HKZuDsfkpfIkaF/ZDYPkz/pFf6VHrvoV0rdhj8wb8QJQ4nB+lgBv2APSwkEaFb/gGrnE19v3Ojlt5tqpPsw==} + '@zag-js/file-utils@1.35.3': + resolution: {integrity: sha512-Tb05RCzx4swc156hd4jLiO7z+Gxg/HQ+JCds03jgTbrFJAz2D56YaMeI7gSDc1m4Xre3nyqQpSo9AeX5nzbE/w==} - '@zag-js/file-upload@1.15.0': - resolution: {integrity: sha512-2hAlQr9qdT8EH4XnmkNkEIDCCsmp2SMoMAjq6nJKYO8UJNQGRanU2B5S8jV3quJBz0vIY43SwyvqiZ3+1VrJSg==} + '@zag-js/floating-panel@1.35.3': + resolution: {integrity: sha512-nTZypcS0X46Oo1kpCQTnP5UlzjhypOAj3B4dq2z/3bAOC0TntYTnFkj8PbEJtExk7364xfMyxfgZOiv7Aqq01w==} - '@zag-js/file-utils@1.15.0': - resolution: {integrity: sha512-tahJt3JmrXaOtGiknH5PxIiOyyNvroMfjiBqOqnNksIPzDoWmVNxHOEme/ts7dJlkRD8U2qm2NFC2VS0bKerzg==} + '@zag-js/focus-trap@1.35.3': + resolution: {integrity: sha512-evErLlGFdDVCI8xipNS5k0rAvO+KFRA9g273bbfWAL1+mT54mcB/XHa85nC3QpPgMNrSh+6LUNq9fapyOGoyYg==} - '@zag-js/floating-panel@1.15.0': - resolution: {integrity: sha512-AYYFseA1MeQUZl+zjNoKUu4j0kwz8EyJd4oJjs8uJIR6KG8u8QhpWYIBUny63M6AtZTCSYQAgBEcEh+mrbEyyQ==} + '@zag-js/focus-visible@1.35.3': + resolution: {integrity: sha512-g4F8PRGIoFoKBrHiQ1HQh5AjCS7brFRXHvpbDNb9+T11FGlF5Turb+6OVRoNV8MmiuqMltO2I28l36YsGc//uQ==} - '@zag-js/focus-trap@1.15.0': - resolution: {integrity: sha512-N8m/JpNe1gHUPJlr0hyGUdHg6pAuyJKkBaX0s38cyVntlo2CJhyAWZGuUdocpT2Q3HNPql666FNnH986rYPDKQ==} + '@zag-js/highlight-word@1.35.3': + resolution: {integrity: sha512-K+mvEBbf3SUFjQeMeJQYb3cjri3x6sPaPhcKWayalelSLB/StWEGqcpmz+a6uUYrCUAK5kEi3Hn0YLGfn0GOig==} - '@zag-js/focus-visible@1.15.0': - resolution: {integrity: sha512-TPXBf47tj6L0hhZNl9AWhuLoVzfPaNPM+/Gw8t9l9Whvy6v9rk/rqUCidY5LsrQuPiKTi7s5WI5J+Wod8ib3gw==} + '@zag-js/hover-card@1.35.3': + resolution: {integrity: sha512-xVoKOtvrnzhYzciZ1csgiV76IQ4DRtx1lsJeFSrfg5MH0kYWeC/pcmm3yCd2+Qh/45J7DbSXeZneqxpyiF5Vvw==} - '@zag-js/highlight-word@1.15.0': - resolution: {integrity: sha512-Rwr/rRm8BaF2xW9BAEJeA2wpFVx6HzoezfYQX7GFPPgw3N8nBMAYNjx+i1YIwIEcNyad2rbaBB+pSd2fZLIniA==} + '@zag-js/i18n-utils@1.35.3': + resolution: {integrity: sha512-k7UcNxbnC2jvGwCoHYAkFD3ZaRSMQNVHfuy8TujZQ+ci3IJovwgWLveZoRfFbXHkTLfhmbpE2tFXBdpwOVZutg==} - '@zag-js/hover-card@1.15.0': - resolution: {integrity: sha512-j6BsE+metdnv/C/Ls0TZzAMN78rtS2r8M1ccHY5FFTGyUvZnlE8BY/QPNyCSSSCUpynymzMYh3IMYlxbJgfpSQ==} + '@zag-js/image-cropper@1.35.3': + resolution: {integrity: sha512-1PH6bg8JAQESHzNqjka2TJ0QGNBGBAO6rb7AZ+9CaCCLw0pIzbUJhqPMkwd9GhdWGKGP+e7wFitnjcT4W5Js8g==} - '@zag-js/i18n-utils@1.15.0': - resolution: {integrity: sha512-anxSbT8kLbJaFJFSb0Ork2j/Lp+XVfMNCIgiBR2BuqUlfX72k23TIJvRxAfwNIkUfs0L8ikaSgLss9OwS4mAnw==} + '@zag-js/interact-outside@1.35.3': + resolution: {integrity: sha512-tOcuo/IztzpU7UKXtjVrLZtXzzcbhP4n2WynKwDRkTkq3mRCp61xXJp1csIBycI3JHm/CMeAEcPdRIioxIT/Zw==} - '@zag-js/interact-outside@1.15.0': - resolution: {integrity: sha512-OwBf/iesQGU9Oq3xe/tcK7gu7xipiGWsmwl2CcScr0fTp3BIMbQywHS928IgPk1DxA8KTHodY8wBjoY1dskfRA==} + '@zag-js/json-tree-utils@1.35.3': + resolution: {integrity: sha512-nOv2dPJf+1mxsobYiSlYt96hR1MK7iHKG1iDLoO5wLggS6GQA3ix1BerHJK0zdehoEZ71R45el5ghCG1HB9VzQ==} - '@zag-js/listbox@1.15.0': - resolution: {integrity: sha512-Gcg76uWZwUAyMFZzGWpHnFCU/aaquNbXmVnyzzBgE3Co2snkv02rK1yG9iBwemZe3e5+VBifMMAtLLPAQJdz+g==} + '@zag-js/listbox@1.35.3': + resolution: {integrity: sha512-FE6FOuBr6aWtOb8U8oDvAvcUzD6JKLXAe8WngiLFG+b2yyW4nlaz2AcKRG1bjjB066UMxMo9/+2p4D0Kf5Id1Q==} - '@zag-js/live-region@1.15.0': - resolution: {integrity: sha512-Xy1PqLZD9AKzKuTKCMo9miL1Xizk/N8qFvj64iybBKUYnKr89/af3w7hRFqd2BDX+q3zrNxPp9rZ6L7MlOc7kA==} + '@zag-js/live-region@1.35.3': + resolution: {integrity: sha512-64rWcfggYpyr2Fn4pdrB/lljMgm3quwn9is+vdDN85Vv3WShKWoz08T4njidm0hwcIbzas0bRqQYWDLLsAoSJQ==} - '@zag-js/menu@1.15.0': - resolution: {integrity: sha512-GbEBVYu0w7+88xrGX2GrjXfnwWuX5jLhoLiEcuxvxJQal/nahKrH4AGXJvHXNaRbj+53V3nWAh3u70C9210PWw==} + '@zag-js/marquee@1.35.3': + resolution: {integrity: sha512-bKZVpmAJWPDORP7WOWnS+65W5ZQBQmRs8zvV33ZfCpFbkXjhRiqKSzIj223/VOc2NEDjyWagz2vioAxrFYVzww==} - '@zag-js/number-input@1.15.0': - resolution: {integrity: sha512-+kK8kyXJhIAbEUnswoMDR+DSJUmvDNIOW0ffuZ9pbfukN3p6zaA3/dCp2Dtg3bQS7hGrFWgtrdejJ8l+mVvUAA==} + '@zag-js/menu@1.35.3': + resolution: {integrity: sha512-KyY0EZXkIU57Mjt+Lg+pupiePk3LcnQcB3Gl05Vva61bNjBjdKV71qwCQru/OxPZEwYgPo46L7TDIb56kfK/VQ==} - '@zag-js/pagination@1.15.0': - resolution: {integrity: sha512-Z62Q41fQPWqk59QyJk+9J0Ad3H9DCqZ0zZutI6iH8DdzT0A0xxmT6zhup6DM/8C8h0OLlaHFTWQnj0RdRNrnXg==} + '@zag-js/navigation-menu@1.35.3': + resolution: {integrity: sha512-8cCHx0X/KjEpr2BaMOxJS5LiA6fs/CNqVTF/sTTgZAv7Dm+MH0yNuKm4kpPvcLaVeBpVE09bnyCHrNKzZes+Fw==} - '@zag-js/password-input@1.15.0': - resolution: {integrity: sha512-oHuZKDRJIbycqWpTVznufy4L7K2g8kwcEaZ4runkwO2ocF00zP8HVmOZQzmhkUgTny0azErQydg8XE0VR5OfYg==} + '@zag-js/number-input@1.35.3': + resolution: {integrity: sha512-uqawVybAcLcefVEHMVONuAA5kDSDPP5TsROr5PnAyFlhM1iD85+r3KAfCueoDX5w2X4ibbu9o2tdV6zTFKD/nQ==} - '@zag-js/pin-input@1.15.0': - resolution: {integrity: sha512-IykjogZBG+BfbFXymSa+KGpOi5CrV9kl8HRm6G2V2Sr3NA5jEwMFaGSd/QrcHS9vh23D1Smx/io4pvF7c3q0kg==} + '@zag-js/pagination@1.35.3': + resolution: {integrity: sha512-fKm4s5KAd12RiCI/EDmmGKjPQ+i2qS/UsJPdMe65yb/4mY5OibwV2zyHcVeFsOD4gBZpnU6kYlDAGSttmLWLlQ==} - '@zag-js/popover@1.15.0': - resolution: {integrity: sha512-cdzEed3zcGbjSgPQnQnrsuXo2hVVslmSNwQbU5dHcNzG1uxxmtPCIMVeBUmGyJbAFF5XQpKCq/7mIr26dT73vw==} + '@zag-js/password-input@1.35.3': + resolution: {integrity: sha512-etd0gm6ELAm3y+cFhPU+TYm8khm9cL5Mg5m2DcZxu1Mqpj7JY0LsXZ8SFOdCZgTIHuMEhKBiYfnuyMAd4CJztA==} - '@zag-js/popper@1.15.0': - resolution: {integrity: sha512-Ra/0Ko423KN+8D4+mIFFkeTn9uaHfpxn6UUNIWwZKoiJQvED8DH4dPbLbmvGEoKp6qmisnRHAzi71NLgEhk0Mw==} + '@zag-js/pin-input@1.35.3': + resolution: {integrity: sha512-ZFt+WIHMdVlSg29BrQLFq5ijabiUO3tXMhoKhjjzTSe/tLqfNeu3UxFB6y/FYpn8+Cvn6xwvhu3lgnORYmI0zQ==} - '@zag-js/presence@1.15.0': - resolution: {integrity: sha512-hoxXis50pm79PpkY2kA1wdhh4AEo7t7pBv0VsQYZYjmzuFh4V5IMw9oa1EOfBlC6f/A+EMZ9E+xg+EVsB68a8w==} + '@zag-js/popover@1.35.3': + resolution: {integrity: sha512-+MIEENPsbKPxzoNuDI/C5d5ZN9uxnfZ+MBDc5C5XSgjjg9FcvMXClNq7IFM1aZi24peRXg9cMNf//lApVRT37w==} - '@zag-js/progress@1.15.0': - resolution: {integrity: sha512-/Mz26GR2rOAuoErNOiSGRpvwckTmbCD5nWGDE/aYlVRID13HcsmN15Zk2Jfa4LadqK88aIN8Iy0Sk4elG0+Efw==} + '@zag-js/popper@1.35.3': + resolution: {integrity: sha512-gpB7Xn9WtlfrUsIVbSgNQGDwgNOL/cSGt0Id3wEQKArmqVC704EWtPvXzOMMybBEdm8YW2hQrXuo+o66abI1Sg==} - '@zag-js/qr-code@1.15.0': - resolution: {integrity: sha512-GkGy5k5tk6DIui9lGjDO8+e8TsSVOxEGp1lblPiaRm1ggIh10GhIfCQWGe/x78ezdie8WzxlSrma89suTpaiAQ==} + '@zag-js/presence@1.35.3': + resolution: {integrity: sha512-ev5E7+U9IZAGvEaflpdVLHaZl8ZaQMhGB3ypd0yKhPwXeM51obV8w3+5HjzTqHPl8TKuoHWL31YaiUBd5EuS6w==} - '@zag-js/radio-group@1.15.0': - resolution: {integrity: sha512-+KTebHUtMsE/YDyGE8wF5VnWfZQp+f2WoAwwzBjfhPpRxXbOUMDo0pZEEr3yxkSvQ9hgCcBhMKH8pEk0SPxvjQ==} + '@zag-js/progress@1.35.3': + resolution: {integrity: sha512-u0GxQN1AfXMAgzYOUMxKQA12DyuAP0svh2S//KvOorTSv7d5hAa8nZXi2cEv5abYsyfKJ6/bc1Z56byzW1jVZw==} - '@zag-js/rating-group@1.15.0': - resolution: {integrity: sha512-omGKN97FhplFwBX9J/Mj7BCZuwFXSXssSVTKU7Yp2d1Cmxhez4+Ju7KdSRNnIoWB4OxFCxwZyaAPTcg3E0Pjrg==} + '@zag-js/qr-code@1.35.3': + resolution: {integrity: sha512-t0Ehwogr49vTNtWyNdQU2tYex7uJyfAn7N/5LgD7FXw8aa+RBMWZWlqjCUvHqJ929tVMrn+LIrQnZCcwNunalA==} - '@zag-js/react@1.15.0': - resolution: {integrity: sha512-YSp9QBkdeBfZt4nVhJW+CUd5sNEEVAuwkmoZWDFUoDoWSAXwzSKuHCmTm5/8DaXg1IZD2bMrXgMNDqZv2x0hZw==} + '@zag-js/radio-group@1.35.3': + resolution: {integrity: sha512-kOzocjqWk3dXuRfyfsHwfw63Z99NHbc7rvVUutSsfXANXi+DFYZHuqdPUwMt+29LfaL15XTOfuGV+yUXDCgQHQ==} + + '@zag-js/rating-group@1.35.3': + resolution: {integrity: sha512-BmhJZdbaTnd3nFWMY+nR+HF952UhWXfaXXxiBWptSLMBfAYImQTWBMrLgTHCSnVfmFATj4Gb7xQe79FQU8T5fA==} + + '@zag-js/react@1.35.3': + resolution: {integrity: sha512-x2PxYUCQ6OgOpUdmSkG5tbL9JWVqYRh42r4V2UeAdMh0MRwjAJtxjvAy50DZ8Sfia5o4UGdZMXJyDY2O7Pdhyw==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' - '@zag-js/rect-utils@1.15.0': - resolution: {integrity: sha512-sjAn78x1t3XiDG3NT8SoFfyO0u7/SEJU5RKRhMgjTPoOLXTzZj+lu2d5N4cUw0uZTfeGb/ormObSchMQVhFgYQ==} + '@zag-js/rect-utils@1.35.3': + resolution: {integrity: sha512-mt/oD3RXdyaX6ZPSd8BO13vvPBJ7QpVWieubE3O0WM3OPhU7ykDMRp/tR7cYMQrzUm04GlY9pbkmSSw2uABxlA==} - '@zag-js/remove-scroll@1.15.0': - resolution: {integrity: sha512-vdWSAdgY8wJ7s4YeaKwTMwmZiRMBxCehmdktSxBWvwtAjU1cM3UWvjmZ9E6INJrQXxH9vDpe/rpFSyv1guIQIw==} + '@zag-js/remove-scroll@1.35.3': + resolution: {integrity: sha512-e59z9SbEpPiw0qwNQa2cB5/h30ZCLREaHsCw1TKTANFhwg7v85k9Lq1H/G/49li1CAjmiaOU9BNGlDvbzpNETQ==} - '@zag-js/scroll-snap@1.15.0': - resolution: {integrity: sha512-/LfBlsjoR4tVL3Djus3k9jKLhwC2ApdHTACxEc72TAewoPe4M8icnSDLXmKHvwwOhzK0HlFz8wGm6ZncAbQbuA==} + '@zag-js/scroll-area@1.35.3': + resolution: {integrity: sha512-IQwdUws/AckRIHK1z/wHdHurnOeGd8h8Dmspfh3VT7NkwTnxeJ4SW9di9smuD+d25eXkJRuX5zGEDHAyx2IaPQ==} - '@zag-js/select@1.15.0': - resolution: {integrity: sha512-4urUBADzhrsGEO/UsqHdjsgmDdF15Zzeid3ejEbIMTrkt2/mMMcQ1CShuxtsWqm2EUBz/N1kOcZlE6Tq69n7Xg==} + '@zag-js/scroll-snap@1.35.3': + resolution: {integrity: sha512-NVa2yRm2DQnF6hTV9k7Xz7l8YCZBagZTiqSwNvWKUulKD1csjt2fpBxvUt2cK+1iQnLOey2ydhs7MMsAnXPbJA==} - '@zag-js/signature-pad@1.15.0': - resolution: {integrity: sha512-5Tj8vkrRxEkSV417oR2qdy+TRgDmS3W8dY7xsIjpbBf/kqkt/8Uo4JpaVH2vwQAFw9AwEFogBh9i6dHcXMy0rA==} + '@zag-js/select@1.35.3': + resolution: {integrity: sha512-ztszGHWvlbBDE0YT5LYPH+sMd6VH1ct5pH/M9VSzIUO6C5PARkW0NwSVQ1rCQJMj4sfvSE1gC1/r7urRzqEcUQ==} - '@zag-js/slider@1.15.0': - resolution: {integrity: sha512-NYIsn3GKXIoPmvkDXsQmw9wdYg3QHbYHXnZ8Ewl2fVubN7S5mDlHSZs2iDVsBvX+a4RChWFRO6JHX8E1+BncOg==} + '@zag-js/signature-pad@1.35.3': + resolution: {integrity: sha512-jvtxxzAQ8fre11zWUh6HflG4Ycr5z83Wba4pONRJbUE/vNgkJQ7yJgfyUl1QTlkn8Arfg2Zwoxu9GIq80HLZWg==} - '@zag-js/splitter@1.15.0': - resolution: {integrity: sha512-Xnedl+cpnD/hv9m+GOYCK5K2xRxbs4xuP/EajYtgVcDw8E1X5cBmxHa1hCrp7BMgb2xYCvZ5et4hnmZfb+1X9g==} + '@zag-js/slider@1.35.3': + resolution: {integrity: sha512-Th142JO4Fqla5AWhGrTW6CQicwvTw87PdVpur/WotQ7brlZIww5HipzEMh5eQJSWfwpKD4PI2bYK9V/ZE/mpXA==} - '@zag-js/steps@1.15.0': - resolution: {integrity: sha512-VoIDcDIEErZawmW2m0yTGlffqjfRuSwR37K9LdSRy8Q4Qzz3wV7jASaTjMhTya1hlreJ7tJg+Qbjqowvw9GndA==} + '@zag-js/splitter@1.35.3': + resolution: {integrity: sha512-IsIbRwzjr5amGANEDsZDSToaSn8wHUWvS2l0XHmf3BiiguVApaZgQTlfqthVQC9hBHMOaGIXIW1CFUOrQYkvUQ==} - '@zag-js/store@1.15.0': - resolution: {integrity: sha512-ecqjcy3b1GsULpsT8RVJV9KDaikajRN0XRg48HMvaGkaPIvxI6esyrE6RKnShuqr2eVXIPghgBnCnrJUev4UlA==} + '@zag-js/steps@1.35.3': + resolution: {integrity: sha512-TYIrqV+v9/ULhvrTRBtQFFvJQPPTWOmjFXxlIxDwozek5R4dCIyeUYt1/ChJEc2mNETocbfDVSTxRO1dwCFpwQ==} - '@zag-js/switch@1.15.0': - resolution: {integrity: sha512-2CaAUTi7jM4lJjCYoSE1HWlFPCifI5GR+hufWOCYKpanf8VA/LM+t/a2Aq5QoBsWdcQv3B9mHxF/aVTDbnCKPQ==} + '@zag-js/store@1.35.3': + resolution: {integrity: sha512-7kEV4T/20DU36UIfVMzuDlLhWSSEy/vabmpiB700tcdD9BBBODTiSg3ZeljW17dQbvE545vZOFEjVf/cQ5LVGA==} - '@zag-js/tabs@1.15.0': - resolution: {integrity: sha512-voHWpibC1TKLmbAJfixOesxrCio7wK+gdLRvh7Xh5u+3VSsT2fP2wEw3ySkJbpw3MpEE7R2OWkInbCV/SwPcsA==} + '@zag-js/switch@1.35.3': + resolution: {integrity: sha512-EP/2cJ46sd+6C5x5+89jn/9NOpM05CRESYB4RMhOnTe/WFtcS4IpiYtVHFhikdXkvJoibm67O2EHep2Pm/Xj4w==} - '@zag-js/tags-input@1.15.0': - resolution: {integrity: sha512-CB60z+/I/Nso1gwatTO1qrk4XITxDd4qtRD+l6fuuKyOkZGgKm0AP0W+/6qUuOvtWIuY6fas3yZHFmF2eEZ9vQ==} + '@zag-js/tabs@1.35.3': + resolution: {integrity: sha512-lZKlDmxE25miCikj9QZCCnL02SVV2K14KZy5bn7+XDgrWlfSNTpNTj8r5E3zGlSgio5pkTGou57ASqS7WaPDWg==} - '@zag-js/time-picker@1.15.0': - resolution: {integrity: sha512-4S02433X88X3MW/BxaFJiWna4BIRXsAdrmDcBb0PZ8dln29DUmpD8YHcFtONsKvmCAmrbO7Gr65n86nQwK8zeg==} - peerDependencies: - '@internationalized/date': '>=3.0.0' + '@zag-js/tags-input@1.35.3': + resolution: {integrity: sha512-HqyoQ3DZFhByOGnDShFfxi6u0bIf7aSVTlwmAvcL+b2ZhyU6/wIMGc4WJE7BMx1NYWM/jNLHedvGExAI8R0kXQ==} - '@zag-js/timer@1.15.0': - resolution: {integrity: sha512-gDsYm4C9yju7g/r5u7n7mRQ2UY7diXXVbbLFr5Ja+0iUXgbD+uoSZEt9HypVc5TL9NWEEwn5/tut36owEeW4rw==} + '@zag-js/timer@1.35.3': + resolution: {integrity: sha512-edmgitbRgsq+msxvVB4wc17Q5d5k63zMWaLJnWjUdDGAgEtM6/HNxwGb3riv46S2U3RgYxaaHTNZ/M7EE5mvYw==} - '@zag-js/toast@1.15.0': - resolution: {integrity: sha512-0RupMCXyGr7/La4Zlei7VqBF0VPNJelGd7zimLboe+IKZyy4Ypi/N2IX14rl8JZQDsDEgkLUl33xrSk/9RW2nQ==} + '@zag-js/toast@1.35.3': + resolution: {integrity: sha512-whlR791GHdnMD21nNPsl2Dbql8+qu1wBZl75QzwYrjR8FlKjp8bhr3gXKzQEddcBXe9GPEFGvUs4iCyXsuTbpg==} - '@zag-js/toggle-group@1.15.0': - resolution: {integrity: sha512-992vMz/2sriLrUKI3LpT/01kCGTbPGLgGLibiHRt562i0v9+2tV+GiY2jBctHZjJaKPrzBY3H0l8CCCvDj8gng==} + '@zag-js/toggle-group@1.35.3': + resolution: {integrity: sha512-Gn6JHzkQ4tlttjZcE0ZjIdxYkFeVp9VHrcMVizjJTkGZRmQ+kPZ5G/wOsZhIrvLX3Dw6Y0NkuBcP+jDHz/o3TA==} - '@zag-js/toggle@1.15.0': - resolution: {integrity: sha512-mMSQ1+f1hOMp/7gLA7rTeiSNyeZxsCjRxP4XnTBY4BxJ5LswLuhem9CplBwaVthkhY1Y/5f3HHu80LBcfF+BVQ==} + '@zag-js/toggle@1.35.3': + resolution: {integrity: sha512-aFfHKuR4sKzglhkmWLA+0RTNPs9dfeqwtc96qljawGYfAYWJXkEPYK9dFfVa+arZ7L84xBi24QSLiTg7LGSFLw==} - '@zag-js/tooltip@1.15.0': - resolution: {integrity: sha512-sOpVECyfdS4RZBx46mSV+RPc9C5k9JvYQYUfoOVWh0E5RLSEz5bQm5xxctKOHfCOv+vJNTfG5gP596B1r2+Fkw==} + '@zag-js/tooltip@1.35.3': + resolution: {integrity: sha512-/pImDGYl79MfLdvEphj3rSvNdj2tLW4GwGEncgdLM/GKwQiEUjfi/9EJOfLYP23M4lOOnoW7orehJ9xeaXOAkA==} - '@zag-js/tour@1.15.0': - resolution: {integrity: sha512-EplcxoiE0z9vI0z6675+ABclQ9Mi1YUWhDZOHx7wfjRzpfawmJoBAlNDKzK3wc801d6OxgJx69SPj7ac0BwwwA==} + '@zag-js/tour@1.35.3': + resolution: {integrity: sha512-DI2aCXmZaE9KcPZDs9itc2BO7ixLApJ/yVRfM69pXwVOrucdSeDDNPFkfbhj5XwB+9VjjZEkqWFHKntRIyPl5g==} - '@zag-js/tree-view@1.15.0': - resolution: {integrity: sha512-wqdd+hu1bDOCWtnZ8MarRFHqbZF2t8qKBM3kO42IBq7jTI/93LCkHSlceEPft9dgZ6Ea9km0YJMHhoTqCPZ/fw==} + '@zag-js/tree-view@1.35.3': + resolution: {integrity: sha512-DbHaLxSNa1goE3o3IsXxEdzp8P5dvmkk1rVWgNUUIhpA+44idEjSSNXJkHPl18Mk5blqSMVjK1EX91oqai01Vw==} - '@zag-js/types@1.15.0': - resolution: {integrity: sha512-lV2ov2M07BlmjDUCSwBeHxPApHI3oAiLytG94AqcYvQ0BtsCRo5T60yRQ0syFc6fHf0e9+kwt89uoIgfGFYfmw==} + '@zag-js/types@1.35.3': + resolution: {integrity: sha512-Fnm3AMs1lfb55hlkip/eJeWHOjFB3gSi1JkZlkkdltG2l7y/zsHkumPSe6jIKy+DRRIFKRCyXVTatbPN27bO3w==} - '@zag-js/utils@1.15.0': - resolution: {integrity: sha512-XctFny5H8C00BsougV40Yp0qVEj9M2d/NRme7B33mon9wG+3hscZwP6miJmF6BYI5Pgu6e2P0Sv45FddQU1Tkg==} + '@zag-js/utils@1.35.3': + resolution: {integrity: sha512-LHcC+9y6TFhDsIz9I3koYxONl2JFfx5yQDzc6ZEQO2cqzXedRcN0R9IPqNGCX7JuhGt14ctDkVCm1JWGP2J6Wg==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1775,19 +1820,15 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - anser@2.3.3: - resolution: {integrity: sha512-QGY1oxYE7/kkeNmbtY/2ZjQ07BCG3zYdz+k/+sf69kMzEIxb93guHkPnIXITQ+BYi61oQwG74twMOX1tD4aesg==} - - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} + anser@2.3.5: + resolution: {integrity: sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==} ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} ansi-styles@4.3.0: @@ -1798,8 +1839,8 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} anymatch@3.1.3: @@ -1876,8 +1917,8 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} - axios@1.13.5: - resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -1896,9 +1937,14 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.3: - resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} - engines: {node: 20 || >=22} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} @@ -1910,9 +1956,9 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brace-expansion@5.0.2: - resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} - engines: {node: 20 || >=22} + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -1923,6 +1969,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} @@ -1962,6 +2013,9 @@ packages: caniuse-lite@1.0.30001707: resolution: {integrity: sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==} + caniuse-lite@1.0.30001777: + resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2119,9 +2173,6 @@ packages: css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -2233,6 +2284,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decode-named-character-reference@1.1.0: resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} @@ -2302,8 +2362,11 @@ packages: electron-to-chromium@1.5.123: resolution: {integrity: sha512-refir3NlutEZqlKaBLK0tzlVLe5P2wDKS7UQt/3SpibizgsRAPOsqQC3ffw1nlv3ze5gjRQZYHoPymgVZkplFA==} - elkjs@0.11.0: - resolution: {integrity: sha512-u4J8h9mwEDaYMqo0RYJpqNMFDoMK7f+pu4GjcV+N8jIC7TRdORgzkfSjTJemhqONFfH6fBI3wpysgWbhgVWIXw==} + electron-to-chromium@1.5.307: + resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + + elkjs@0.11.1: + resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2311,6 +2374,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -2352,8 +2419,8 @@ packages: es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} - esbuild@0.25.11: - resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} hasBin: true @@ -2418,8 +2485,8 @@ packages: peerDependencies: eslint: '>=8.45.0' - eslint-plugin-prettier@5.5.4: - resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + eslint-plugin-prettier@5.5.5: + resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -2438,10 +2505,10 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-refresh@0.4.24: - resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} peerDependencies: - eslint: '>=8.40' + eslint: ^9 || ^10 eslint-plugin-react@7.37.5: resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} @@ -2471,6 +2538,10 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.39.1: resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2539,9 +2610,6 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fault@1.0.4: resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} @@ -2691,8 +2759,8 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - graphql@16.12.0: - resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + graphql@16.13.1: + resolution: {integrity: sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} handlebars@4.7.8: @@ -2700,8 +2768,8 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - happy-dom@20.0.11: - resolution: {integrity: sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==} + happy-dom@20.8.3: + resolution: {integrity: sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ==} engines: {node: '>=20.0.0'} has-bigints@1.1.0: @@ -2776,14 +2844,14 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} - i18next-browser-languagedetector@8.2.0: - resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==} + i18next-browser-languagedetector@8.2.1: + resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==} i18next-http-backend@3.0.2: resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==} - i18next@25.7.1: - resolution: {integrity: sha512-XbTnkh1yCZWSAZGnA9xcQfHcYNgZs2cNxm+c6v1Ma9UAUGCeJPplRe1ILia6xnDvXBjk0uXU+Z8FYWhA19SKFw==} + i18next@25.8.14: + resolution: {integrity: sha512-paMUYkfWJMsWPeE/Hejcw+XLhHrQPehem+4wMo+uELnvIwvCG019L9sAIljwjCmEMtFQQO3YeitJY8Kctei3iA==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -3113,8 +3181,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.5: - resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -3303,6 +3371,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} @@ -3316,8 +3388,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msw@2.12.4: - resolution: {integrity: sha512-rHNiVfTyKhzc0EjoXUBVGteNKBevdjOlVC6GlIRXpy+/3LHEIGRovnB5WPjcvmNODVQ1TNFnoa7wsGbd0V3epg==} + msw@2.12.10: + resolution: {integrity: sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -3366,6 +3438,9 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -3482,9 +3557,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -3506,8 +3581,8 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - perfect-freehand@1.2.2: - resolution: {integrity: sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==} + perfect-freehand@1.2.3: + resolution: {integrity: sha512-bHZSfqDHGNlPpgH2yxXgPHlQSPpEbo+qg7li0M78J9vNAi2yjwLeA4x79BEQhX44lEWpCLSFCeRZwpw0niiXPA==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3527,13 +3602,13 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - playwright-core@1.57.0: - resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} engines: {node: '>=18'} hasBin: true - playwright@1.57.0: - resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} engines: {node: '>=18'} hasBin: true @@ -3545,20 +3620,20 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier@3.7.4: - resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} hasBin: true @@ -3595,8 +3670,8 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} - react-chartjs-2@5.3.0: - resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==} + react-chartjs-2@5.3.1: + resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==} peerDependencies: chart.js: ^4.1.1 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3606,8 +3681,8 @@ packages: peerDependencies: react: ^19.2.4 - react-hook-form@7.56.2: - resolution: {integrity: sha512-vpfuHuQMF/L6GpuQ4c3ZDo+pRYxIi40gQqsCmmfUBwm+oqvBhKhwghCuj2o00YCgSfU6bR9KC/xnQGWm3Gr08A==} + react-hook-form@7.71.2: + resolution: {integrity: sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -3634,8 +3709,8 @@ packages: typescript: optional: true - react-icons@5.5.0: - resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + react-icons@5.6.0: + resolution: {integrity: sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==} peerDependencies: react: '*' @@ -3667,15 +3742,15 @@ packages: react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - react-router-dom@7.12.0: - resolution: {integrity: sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==} + react-router-dom@7.13.1: + resolution: {integrity: sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' react-dom: '>=18' - react-router@7.12.0: - resolution: {integrity: sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==} + react-router@7.13.1: + resolution: {integrity: sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -3776,8 +3851,8 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true - rettime@0.7.0: - resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} + rettime@0.10.1: + resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} @@ -3815,6 +3890,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -3948,8 +4028,8 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} strip-indent@3.0.0: @@ -3980,6 +4060,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + synckit@0.11.8: resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==} engines: {node: ^14.18.0 || >=16.0.0} @@ -3988,8 +4072,8 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} - tar@7.5.10: - resolution: {integrity: sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==} + tar@7.5.11: + resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==} engines: {node: '>=18'} test-exclude@7.0.1: @@ -4018,11 +4102,11 @@ packages: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} - tldts-core@7.0.19: - resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + tldts-core@7.0.25: + resolution: {integrity: sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==} - tldts@7.0.19: - resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + tldts@7.0.25: + resolution: {integrity: sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==} hasBin: true to-fast-properties@2.0.0: @@ -4046,8 +4130,8 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -4065,10 +4149,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -4077,8 +4157,8 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} - type-fest@5.3.0: - resolution: {integrity: sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g==} + type-fest@5.4.4: + resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} engines: {node: '>=20'} typed-array-buffer@1.0.3: @@ -4097,11 +4177,11 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.48.1: - resolution: {integrity: sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==} + typescript-eslint@8.56.1: + resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' typescript@5.9.3: @@ -4121,9 +4201,6 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -4154,6 +4231,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uqr@0.1.2: resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} @@ -4163,8 +4246,8 @@ packages: urijs@1.19.11: resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} - use-debounce@10.0.4: - resolution: {integrity: sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==} + use-debounce@10.1.0: + resolution: {integrity: sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg==} engines: {node: '>= 16.0.0'} peerDependencies: react: '*' @@ -4178,8 +4261,8 @@ packages: '@types/react': optional: true - use-sync-external-store@1.4.0: - resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4208,8 +4291,8 @@ packages: peerDependencies: vite: '>2.0.0-0' - vite@7.2.6: - resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==} + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4341,6 +4424,18 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -4360,8 +4455,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.0: - resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true @@ -4377,8 +4472,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yoctocolors-cjs@2.1.2: - resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} zod-validation-error@4.0.2: @@ -4390,8 +4485,8 @@ packages: zod@4.2.1: resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} - zustand@4.5.6: - resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} peerDependencies: '@types/react': '>=16.8' @@ -4405,8 +4500,8 @@ packages: react: optional: true - zustand@5.0.4: - resolution: {integrity: sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==} + zustand@5.0.11: + resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -4451,66 +4546,73 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.1.1 - '@ark-ui/react@5.12.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@internationalized/date': 3.8.1 - '@zag-js/accordion': 1.15.0 - '@zag-js/anatomy': 1.15.0 - '@zag-js/angle-slider': 1.15.0 - '@zag-js/auto-resize': 1.15.0 - '@zag-js/avatar': 1.15.0 - '@zag-js/carousel': 1.15.0 - '@zag-js/checkbox': 1.15.0 - '@zag-js/clipboard': 1.15.0 - '@zag-js/collapsible': 1.15.0 - '@zag-js/collection': 1.15.0 - '@zag-js/color-picker': 1.15.0 - '@zag-js/color-utils': 1.15.0 - '@zag-js/combobox': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/date-picker': 1.15.0(@internationalized/date@3.8.1) - '@zag-js/date-utils': 1.15.0(@internationalized/date@3.8.1) - '@zag-js/dialog': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/editable': 1.15.0 - '@zag-js/file-upload': 1.15.0 - '@zag-js/file-utils': 1.15.0 - '@zag-js/floating-panel': 1.15.0 - '@zag-js/focus-trap': 1.15.0 - '@zag-js/highlight-word': 1.15.0 - '@zag-js/hover-card': 1.15.0 - '@zag-js/i18n-utils': 1.15.0 - '@zag-js/listbox': 1.15.0 - '@zag-js/menu': 1.15.0 - '@zag-js/number-input': 1.15.0 - '@zag-js/pagination': 1.15.0 - '@zag-js/password-input': 1.15.0 - '@zag-js/pin-input': 1.15.0 - '@zag-js/popover': 1.15.0 - '@zag-js/presence': 1.15.0 - '@zag-js/progress': 1.15.0 - '@zag-js/qr-code': 1.15.0 - '@zag-js/radio-group': 1.15.0 - '@zag-js/rating-group': 1.15.0 - '@zag-js/react': 1.15.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@zag-js/select': 1.15.0 - '@zag-js/signature-pad': 1.15.0 - '@zag-js/slider': 1.15.0 - '@zag-js/splitter': 1.15.0 - '@zag-js/steps': 1.15.0 - '@zag-js/switch': 1.15.0 - '@zag-js/tabs': 1.15.0 - '@zag-js/tags-input': 1.15.0 - '@zag-js/time-picker': 1.15.0(@internationalized/date@3.8.1) - '@zag-js/timer': 1.15.0 - '@zag-js/toast': 1.15.0 - '@zag-js/toggle': 1.15.0 - '@zag-js/toggle-group': 1.15.0 - '@zag-js/tooltip': 1.15.0 - '@zag-js/tour': 1.15.0 - '@zag-js/tree-view': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@ark-ui/react@5.34.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@internationalized/date': 3.11.0 + '@zag-js/accordion': 1.35.3 + '@zag-js/anatomy': 1.35.3 + '@zag-js/angle-slider': 1.35.3 + '@zag-js/async-list': 1.35.3 + '@zag-js/auto-resize': 1.35.3 + '@zag-js/avatar': 1.35.3 + '@zag-js/carousel': 1.35.3 + '@zag-js/cascade-select': 1.35.3 + '@zag-js/checkbox': 1.35.3 + '@zag-js/clipboard': 1.35.3 + '@zag-js/collapsible': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/color-picker': 1.35.3 + '@zag-js/color-utils': 1.35.3 + '@zag-js/combobox': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/date-picker': 1.35.3(@internationalized/date@3.11.0) + '@zag-js/date-utils': 1.35.3(@internationalized/date@3.11.0) + '@zag-js/dialog': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/drawer': 1.35.3 + '@zag-js/editable': 1.35.3 + '@zag-js/file-upload': 1.35.3 + '@zag-js/file-utils': 1.35.3 + '@zag-js/floating-panel': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/highlight-word': 1.35.3 + '@zag-js/hover-card': 1.35.3 + '@zag-js/i18n-utils': 1.35.3 + '@zag-js/image-cropper': 1.35.3 + '@zag-js/json-tree-utils': 1.35.3 + '@zag-js/listbox': 1.35.3 + '@zag-js/marquee': 1.35.3 + '@zag-js/menu': 1.35.3 + '@zag-js/navigation-menu': 1.35.3 + '@zag-js/number-input': 1.35.3 + '@zag-js/pagination': 1.35.3 + '@zag-js/password-input': 1.35.3 + '@zag-js/pin-input': 1.35.3 + '@zag-js/popover': 1.35.3 + '@zag-js/presence': 1.35.3 + '@zag-js/progress': 1.35.3 + '@zag-js/qr-code': 1.35.3 + '@zag-js/radio-group': 1.35.3 + '@zag-js/rating-group': 1.35.3 + '@zag-js/react': 1.35.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@zag-js/scroll-area': 1.35.3 + '@zag-js/select': 1.35.3 + '@zag-js/signature-pad': 1.35.3 + '@zag-js/slider': 1.35.3 + '@zag-js/splitter': 1.35.3 + '@zag-js/steps': 1.35.3 + '@zag-js/switch': 1.35.3 + '@zag-js/tabs': 1.35.3 + '@zag-js/tags-input': 1.35.3 + '@zag-js/timer': 1.35.3 + '@zag-js/toast': 1.35.3 + '@zag-js/toggle': 1.35.3 + '@zag-js/toggle-group': 1.35.3 + '@zag-js/tooltip': 1.35.3 + '@zag-js/tour': 1.35.3 + '@zag-js/tree-view': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -4534,6 +4636,8 @@ snapshots: '@babel/compat-data@7.28.5': {} + '@babel/compat-data@7.29.0': {} + '@babel/core@7.28.5': dependencies: '@babel/code-frame': 7.27.1 @@ -4554,6 +4658,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.17.7': dependencies: '@babel/types': 7.17.0 @@ -4562,7 +4686,7 @@ snapshots: '@babel/generator@7.26.10': dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.26.10 '@babel/types': 7.26.10 '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 @@ -4576,6 +4700,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.27.2': dependencies: '@babel/compat-data': 7.28.5 @@ -4584,13 +4716,21 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + '@babel/helper-environment-visitor@7.24.7': dependencies: '@babel/types': 7.26.10 '@babel/helper-function-name@7.24.7': dependencies: - '@babel/template': 7.26.9 + '@babel/template': 7.28.6 '@babel/types': 7.26.10 '@babel/helper-globals@7.28.0': {} @@ -4601,8 +4741,8 @@ snapshots: '@babel/helper-module-imports@7.25.9': dependencies: - '@babel/traverse': 7.26.10 - '@babel/types': 7.28.5 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -4613,6 +4753,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -4622,7 +4769,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} '@babel/helper-split-export-declaration@7.24.7': dependencies: @@ -4645,6 +4801,11 @@ snapshots: '@babel/template': 7.27.2 '@babel/types': 7.28.5 + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@babel/parser@7.26.10': dependencies: '@babel/types': 7.26.10 @@ -4653,27 +4814,25 @@ snapshots: dependencies: '@babel/types': 7.28.5 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': + '@babel/parser@7.29.0': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.29.0 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 '@babel/runtime@7.26.10': dependencies: regenerator-runtime: 0.14.1 - '@babel/runtime@7.28.4': {} - - '@babel/template@7.26.9': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/runtime@7.28.6': {} '@babel/template@7.27.2': dependencies: @@ -4681,6 +4840,12 @@ snapshots: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@babel/traverse@7.23.2': dependencies: '@babel/code-frame': 7.26.2 @@ -4696,18 +4861,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.26.10': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.10 - '@babel/parser': 7.28.5 - '@babel/template': 7.26.9 - '@babel/types': 7.28.5 - debug: 4.4.1 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 @@ -4720,6 +4873,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.17.0': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -4735,21 +4900,25 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@1.0.2': {} '@chakra-ui/anatomy@2.3.4': {} - '@chakra-ui/react@3.20.0(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@chakra-ui/react@3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@ark-ui/react': 5.12.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@emotion/is-prop-valid': 1.3.1 - '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.4) + '@ark-ui/react': 5.34.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@emotion/is-prop-valid': 1.4.0 + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.4) '@emotion/utils': 1.4.2 - '@pandacss/is-valid-prop': 0.53.6 - csstype: 3.1.3 - fast-safe-stringify: 2.1.1 + '@pandacss/is-valid-prop': 1.9.0 + csstype: 3.2.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -4779,13 +4948,13 @@ snapshots: '@emotion/hash@0.9.2': {} - '@emotion/is-prop-valid@1.3.1': + '@emotion/is-prop-valid@1.4.0': dependencies: '@emotion/memoize': 0.9.0 '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.4)': + '@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4)': dependencies: '@babel/runtime': 7.26.10 '@emotion/babel-plugin': 11.13.5 @@ -4797,7 +4966,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.2.4 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 transitivePeerDependencies: - supports-color @@ -4807,7 +4976,7 @@ snapshots: '@emotion/memoize': 0.9.0 '@emotion/unitless': 0.10.0 '@emotion/utils': 1.4.2 - csstype: 3.1.3 + csstype: 3.2.3 '@emotion/sheet@1.4.0': {} @@ -4821,82 +4990,82 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} - '@esbuild/aix-ppc64@0.25.11': + '@esbuild/aix-ppc64@0.27.3': optional: true - '@esbuild/android-arm64@0.25.11': + '@esbuild/android-arm64@0.27.3': optional: true - '@esbuild/android-arm@0.25.11': + '@esbuild/android-arm@0.27.3': optional: true - '@esbuild/android-x64@0.25.11': + '@esbuild/android-x64@0.27.3': optional: true - '@esbuild/darwin-arm64@0.25.11': + '@esbuild/darwin-arm64@0.27.3': optional: true - '@esbuild/darwin-x64@0.25.11': + '@esbuild/darwin-x64@0.27.3': optional: true - '@esbuild/freebsd-arm64@0.25.11': + '@esbuild/freebsd-arm64@0.27.3': optional: true - '@esbuild/freebsd-x64@0.25.11': + '@esbuild/freebsd-x64@0.27.3': optional: true - '@esbuild/linux-arm64@0.25.11': + '@esbuild/linux-arm64@0.27.3': optional: true - '@esbuild/linux-arm@0.25.11': + '@esbuild/linux-arm@0.27.3': optional: true - '@esbuild/linux-ia32@0.25.11': + '@esbuild/linux-ia32@0.27.3': optional: true - '@esbuild/linux-loong64@0.25.11': + '@esbuild/linux-loong64@0.27.3': optional: true - '@esbuild/linux-mips64el@0.25.11': + '@esbuild/linux-mips64el@0.27.3': optional: true - '@esbuild/linux-ppc64@0.25.11': + '@esbuild/linux-ppc64@0.27.3': optional: true - '@esbuild/linux-riscv64@0.25.11': + '@esbuild/linux-riscv64@0.27.3': optional: true - '@esbuild/linux-s390x@0.25.11': + '@esbuild/linux-s390x@0.27.3': optional: true - '@esbuild/linux-x64@0.25.11': + '@esbuild/linux-x64@0.27.3': optional: true - '@esbuild/netbsd-arm64@0.25.11': + '@esbuild/netbsd-arm64@0.27.3': optional: true - '@esbuild/netbsd-x64@0.25.11': + '@esbuild/netbsd-x64@0.27.3': optional: true - '@esbuild/openbsd-arm64@0.25.11': + '@esbuild/openbsd-arm64@0.27.3': optional: true - '@esbuild/openbsd-x64@0.25.11': + '@esbuild/openbsd-x64@0.27.3': optional: true - '@esbuild/openharmony-arm64@0.25.11': + '@esbuild/openharmony-arm64@0.27.3': optional: true - '@esbuild/sunos-x64@0.25.11': + '@esbuild/sunos-x64@0.27.3': optional: true - '@esbuild/win32-arm64@0.25.11': + '@esbuild/win32-arm64@0.27.3': optional: true - '@esbuild/win32-ia32@0.25.11': + '@esbuild/win32-ia32@0.27.3': optional: true - '@esbuild/win32-x64@0.25.11': + '@esbuild/win32-x64@0.27.3': optional: true '@eslint-community/eslint-utils@4.5.1(eslint@9.39.1(jiti@1.21.7))': @@ -4909,8 +5078,15 @@ snapshots: eslint: 9.39.1(jiti@1.21.7) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.1(jiti@1.21.7))': + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} + '@eslint/compat@1.2.9(eslint@9.39.1(jiti@1.21.7))': optionalDependencies: eslint: 9.39.1(jiti@1.21.7) @@ -4958,11 +5134,22 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.9 + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + '@floating-ui/dom@1.7.1': dependencies: '@floating-ui/core': 1.7.1 '@floating-ui/utils': 0.2.9 + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + '@floating-ui/utils@0.2.9': {} '@guanmingchiu/sqlparser-ts@0.61.1': {} @@ -4991,45 +5178,47 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} - '@inquirer/confirm@5.1.8(@types/node@24.10.3)': + '@inquirer/ansi@1.0.2': {} + + '@inquirer/confirm@5.1.21(@types/node@24.10.3)': dependencies: - '@inquirer/core': 10.1.9(@types/node@24.10.3) - '@inquirer/type': 3.0.5(@types/node@24.10.3) + '@inquirer/core': 10.3.2(@types/node@24.10.3) + '@inquirer/type': 3.0.10(@types/node@24.10.3) optionalDependencies: '@types/node': 24.10.3 - '@inquirer/core@10.1.9(@types/node@24.10.3)': + '@inquirer/core@10.3.2(@types/node@24.10.3)': dependencies: - '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.5(@types/node@24.10.3) - ansi-escapes: 4.3.2 + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.3) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.2 + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.10.3 - '@inquirer/figures@1.0.11': {} + '@inquirer/figures@1.0.15': {} - '@inquirer/type@3.0.5(@types/node@24.10.3)': + '@inquirer/type@3.0.10(@types/node@24.10.3)': optionalDependencies: '@types/node': 24.10.3 - '@internationalized/date@3.8.1': + '@internationalized/date@3.11.0': dependencies: - '@swc/helpers': 0.5.15 + '@swc/helpers': 0.5.19 - '@internationalized/number@3.6.2': + '@internationalized/number@3.6.5': dependencies: - '@swc/helpers': 0.5.15 + '@swc/helpers': 0.5.19 '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -5038,7 +5227,7 @@ snapshots: '@isaacs/fs-minipass@4.0.1': dependencies: - minipass: 7.1.2 + minipass: 7.1.3 '@istanbuljs/schema@0.1.3': {} @@ -5055,7 +5244,7 @@ snapshots: '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} @@ -5097,7 +5286,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@mswjs/interceptors@0.40.0': + '@mswjs/interceptors@0.41.3': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -5115,20 +5304,22 @@ snapshots: '@open-draft/until@2.1.0': {} - '@pandacss/is-valid-prop@0.53.6': {} + '@pandacss/is-valid-prop@1.9.0': {} '@pkgjs/parseargs@0.11.0': optional: true '@pkgr/core@0.2.4': {} - '@playwright/test@1.57.0': + '@pkgr/core@0.2.9': {} + + '@playwright/test@1.58.2': dependencies: - playwright: 1.57.0 + playwright: 1.58.2 - '@rolldown/pluginutils@1.0.0-beta.47': {} + '@rolldown/pluginutils@1.0.0-rc.2': {} - '@rolldown/pluginutils@1.0.0-beta.53': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -5207,7 +5398,7 @@ snapshots: '@stylistic/eslint-plugin@2.13.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.1(jiti@1.21.7) eslint-visitor-keys: 4.2.0 espree: 10.3.0 @@ -5217,55 +5408,56 @@ snapshots: - supports-color - typescript - '@swc/core-darwin-arm64@1.13.5': + '@swc/core-darwin-arm64@1.15.18': optional: true - '@swc/core-darwin-x64@1.13.5': + '@swc/core-darwin-x64@1.15.18': optional: true - '@swc/core-linux-arm-gnueabihf@1.13.5': + '@swc/core-linux-arm-gnueabihf@1.15.18': optional: true - '@swc/core-linux-arm64-gnu@1.13.5': + '@swc/core-linux-arm64-gnu@1.15.18': optional: true - '@swc/core-linux-arm64-musl@1.13.5': + '@swc/core-linux-arm64-musl@1.15.18': optional: true - '@swc/core-linux-x64-gnu@1.13.5': + '@swc/core-linux-x64-gnu@1.15.18': optional: true - '@swc/core-linux-x64-musl@1.13.5': + '@swc/core-linux-x64-musl@1.15.18': optional: true - '@swc/core-win32-arm64-msvc@1.13.5': + '@swc/core-win32-arm64-msvc@1.15.18': optional: true - '@swc/core-win32-ia32-msvc@1.13.5': + '@swc/core-win32-ia32-msvc@1.15.18': optional: true - '@swc/core-win32-x64-msvc@1.13.5': + '@swc/core-win32-x64-msvc@1.15.18': optional: true - '@swc/core@1.13.5': + '@swc/core@1.15.18(@swc/helpers@0.5.19)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.13.5 - '@swc/core-darwin-x64': 1.13.5 - '@swc/core-linux-arm-gnueabihf': 1.13.5 - '@swc/core-linux-arm64-gnu': 1.13.5 - '@swc/core-linux-arm64-musl': 1.13.5 - '@swc/core-linux-x64-gnu': 1.13.5 - '@swc/core-linux-x64-musl': 1.13.5 - '@swc/core-win32-arm64-msvc': 1.13.5 - '@swc/core-win32-ia32-msvc': 1.13.5 - '@swc/core-win32-x64-msvc': 1.13.5 + '@swc/core-darwin-arm64': 1.15.18 + '@swc/core-darwin-x64': 1.15.18 + '@swc/core-linux-arm-gnueabihf': 1.15.18 + '@swc/core-linux-arm64-gnu': 1.15.18 + '@swc/core-linux-arm64-musl': 1.15.18 + '@swc/core-linux-x64-gnu': 1.15.18 + '@swc/core-linux-x64-musl': 1.15.18 + '@swc/core-win32-arm64-msvc': 1.15.18 + '@swc/core-win32-ia32-msvc': 1.15.18 + '@swc/core-win32-x64-msvc': 1.15.18 + '@swc/helpers': 0.5.19 '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.15': + '@swc/helpers@0.5.19': dependencies: tslib: 2.8.1 @@ -5273,19 +5465,20 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/eslint-plugin-query@5.91.2(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.91.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.1(jiti@1.21.7) + optionalDependencies: + typescript: 5.9.3 transitivePeerDependencies: - supports-color - - typescript - '@tanstack/query-core@5.90.12': {} + '@tanstack/query-core@5.90.20': {} - '@tanstack/react-query@5.90.12(react@19.2.4)': + '@tanstack/react-query@5.90.21(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.90.12 + '@tanstack/query-core': 5.90.20 react: 19.2.4 '@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -5294,20 +5487,20 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@tanstack/react-virtual@3.13.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-virtual@3.13.21(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/virtual-core': 3.13.12 + '@tanstack/virtual-core': 3.13.21 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-core@3.13.12': {} + '@tanstack/virtual-core@3.13.21': {} '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -5324,17 +5517,17 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.28.6 '@testing-library/dom': 10.4.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.7.4)': + '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.1)': dependencies: '@babel/generator': 7.17.7 '@babel/parser': 7.26.10 @@ -5342,7 +5535,7 @@ snapshots: '@babel/types': 7.17.0 javascript-natural-sort: 0.7.1 lodash: 4.17.23 - prettier: 3.7.4 + prettier: 3.8.1 transitivePeerDependencies: - supports-color @@ -5356,24 +5549,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.26.10 - '@babel/types': 7.26.10 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.26.10 + '@babel/types': 7.29.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.26.10 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/chai@5.2.2': dependencies: @@ -5399,7 +5592,7 @@ snapshots: '@types/d3-interpolate@3.0.1': dependencies: - '@types/d3-color': 3.1.3 + '@types/d3-color': 3.1.0 '@types/d3-interpolate@3.0.4': dependencies: @@ -5438,7 +5631,7 @@ snapshots: '@types/estree-jsx@1.0.5': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.6 '@types/estree@1.0.6': {} @@ -5464,10 +5657,6 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@20.19.23': - dependencies: - undici-types: 6.21.0 - '@types/node@24.10.3': dependencies: undici-types: 7.16.0 @@ -5476,26 +5665,19 @@ snapshots: '@types/parse-json@4.0.2': {} - '@types/prop-types@15.7.14': {} - - '@types/react-dom@19.2.3(@types/react@19.2.7)': + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 '@types/react-syntax-highlighter@15.5.13': dependencies: - '@types/react': 18.3.19 - - '@types/react-transition-group@4.4.12(@types/react@19.2.7)': - dependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 - '@types/react@18.3.19': + '@types/react-transition-group@4.4.12(@types/react@19.2.14)': dependencies: - '@types/prop-types': 15.7.14 - csstype: 3.1.3 + '@types/react': 19.2.14 - '@types/react@19.2.7': + '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -5509,188 +5691,102 @@ snapshots: '@types/whatwg-mimetype@3.0.2': {} - '@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@types/ws@8.18.1': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.48.1 - eslint: 9.39.1(jiti@1.21.7) - graphemer: 1.4.0 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + '@types/node': 24.10.3 - '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.49.0 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 eslint: 9.39.1(jiti@1.21.7) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.48.1 - debug: 4.4.1 + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 eslint: 9.39.1(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.49.0 - debug: 4.4.1 - eslint: 9.39.1(jiti@1.21.7) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.48.1(typescript@5.9.3)': + '@typescript-eslint/scope-manager@8.56.1': dependencies: - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.49.0 - debug: 4.4.1 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 - '@typescript-eslint/project-service@8.49.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.49.0 - debug: 4.4.1 typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.48.1': - dependencies: - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/visitor-keys': 8.48.1 - - '@typescript-eslint/scope-manager@8.49.0': - dependencies: - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/visitor-keys': 8.49.0 - '@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': dependencies: - typescript: 5.9.3 - - '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/type-utils@8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - debug: 4.4.1 - eslint: 9.39.1(jiti@1.21.7) - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/type-utils@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - debug: 4.4.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + debug: 4.4.3 eslint: 9.39.1(jiti@1.21.7) - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.48.1': {} - '@typescript-eslint/types@8.49.0': {} + '@typescript-eslint/types@8.56.1': {} - '@typescript-eslint/typescript-estree@8.48.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.48.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3) - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/visitor-keys': 8.48.1 - debug: 4.4.1 - minimatch: 9.0.9 - semver: 7.7.1 - tinyglobby: 0.2.15 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.49.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/visitor-keys': 8.49.0 - debug: 4.4.1 - minimatch: 9.0.9 - semver: 7.7.1 + '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 tinyglobby: 0.2.15 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) - eslint: 9.39.1(jiti@1.21.7) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.1(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) eslint: 9.39.1(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.48.1': + '@typescript-eslint/visitor-keys@8.56.1': dependencies: - '@typescript-eslint/types': 8.48.1 - eslint-visitor-keys: 4.2.1 - - '@typescript-eslint/visitor-keys@8.49.0': - dependencies: - '@typescript-eslint/types': 8.49.0 - eslint-visitor-keys: 4.2.1 + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.0': {} @@ -5701,7 +5797,7 @@ snapshots: '@visx/group@3.12.0(react@19.2.4)': dependencies: - '@types/react': 18.3.19 + '@types/react': 19.2.14 classnames: 2.5.1 prop-types: 15.8.1 react: 19.2.4 @@ -5715,7 +5811,7 @@ snapshots: '@types/d3-path': 1.0.11 '@types/d3-shape': 1.3.12 '@types/lodash': 4.17.20 - '@types/react': 18.3.19 + '@types/react': 19.2.14 '@visx/curve': 3.12.0 '@visx/group': 3.12.0(react@19.2.4) '@visx/scale': 3.12.0 @@ -5748,27 +5844,27 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@4.2.2(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0))': + '@vitejs/plugin-react-swc@4.2.3(@swc/helpers@0.5.19)(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.47 - '@swc/core': 1.13.5 - vite: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + '@rolldown/pluginutils': 1.0.0-rc.2 + '@swc/core': 1.15.18(@swc/helpers@0.5.19) + vite: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@5.1.2(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0))': + '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2))': dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0-beta.53 + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + vite: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@1.21.7)(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.0))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.8.3)(jiti@1.21.7)(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -5783,7 +5879,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@1.21.7)(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.8.3)(jiti@1.21.7)(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -5795,14 +5891,14 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - msw: 2.12.4(@types/node@24.10.3)(typescript@5.9.3) - vite: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + msw: 2.12.10(@types/node@24.10.3)(typescript@5.9.3) + vite: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -5830,18 +5926,18 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@xyflow/react@12.10.0(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@xyflow/react@12.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@xyflow/system': 0.0.74 + '@xyflow/system': 0.0.75 classcat: 5.0.5 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.6(@types/react@19.2.7)(react@19.2.4) + zustand: 4.5.7(@types/react@19.2.14)(react@19.2.4) transitivePeerDependencies: - '@types/react' - immer - '@xyflow/system@0.0.74': + '@xyflow/system@0.0.75': dependencies: '@types/d3-drag': 3.0.7 '@types/d3-interpolate': 3.0.4 @@ -5853,504 +5949,561 @@ snapshots: d3-selection: 3.0.0 d3-zoom: 3.0.0 - '@zag-js/accordion@1.15.0': + '@zag-js/accordion@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/anatomy@1.15.0': {} + '@zag-js/anatomy@1.35.3': {} - '@zag-js/angle-slider@1.15.0': + '@zag-js/angle-slider@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/rect-utils': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/aria-hidden@1.15.0': {} + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/auto-resize@1.15.0': + '@zag-js/aria-hidden@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 + '@zag-js/dom-query': 1.35.3 - '@zag-js/avatar@1.15.0': + '@zag-js/async-list@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/core': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/carousel@1.15.0': + '@zag-js/auto-resize@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/scroll-snap': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/dom-query': 1.35.3 - '@zag-js/checkbox@1.15.0': + '@zag-js/avatar@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-visible': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/clipboard@1.15.0': + '@zag-js/carousel@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/scroll-snap': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/collapsible@1.15.0': + '@zag-js/cascade-select@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/collection@1.15.0': + '@zag-js/checkbox@1.35.3': dependencies: - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/color-picker@1.15.0': + '@zag-js/clipboard@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/color-utils': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/color-utils@1.15.0': + '@zag-js/collapsible@1.35.3': dependencies: - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/combobox@1.15.0': + '@zag-js/collection@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/aria-hidden': 1.15.0 - '@zag-js/collection': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/utils': 1.35.3 - '@zag-js/core@1.15.0': + '@zag-js/color-picker@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/color-utils': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/date-picker@1.15.0(@internationalized/date@3.8.1)': + '@zag-js/color-utils@1.35.3': dependencies: - '@internationalized/date': 3.8.1 - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/date-utils': 1.15.0(@internationalized/date@3.8.1) - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/live-region': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/utils': 1.35.3 - '@zag-js/date-utils@1.15.0(@internationalized/date@3.8.1)': + '@zag-js/combobox@1.35.3': dependencies: - '@internationalized/date': 3.8.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/dialog@1.15.0': + '@zag-js/core@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/aria-hidden': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-trap': 1.15.0 - '@zag-js/remove-scroll': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/dismissable@1.15.0': + '@zag-js/date-picker@1.35.3(@internationalized/date@3.11.0)': dependencies: - '@zag-js/dom-query': 1.15.0 - '@zag-js/interact-outside': 1.15.0 - '@zag-js/utils': 1.15.0 + '@internationalized/date': 3.11.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/date-utils': 1.35.3(@internationalized/date@3.11.0) + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/live-region': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/dom-query@1.15.0': + '@zag-js/date-utils@1.35.3(@internationalized/date@3.11.0)': dependencies: - '@zag-js/types': 1.15.0 + '@internationalized/date': 3.11.0 - '@zag-js/editable@1.15.0': + '@zag-js/dialog@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/interact-outside': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/remove-scroll': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/file-upload@1.15.0': + '@zag-js/dismissable@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/file-utils': 1.15.0 - '@zag-js/i18n-utils': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/dom-query': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/file-utils@1.15.0': + '@zag-js/dom-query@1.35.3': dependencies: - '@zag-js/i18n-utils': 1.15.0 + '@zag-js/types': 1.35.3 - '@zag-js/floating-panel@1.15.0': + '@zag-js/drawer@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/rect-utils': 1.15.0 - '@zag-js/store': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/remove-scroll': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/focus-trap@1.15.0': + '@zag-js/editable@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/focus-visible@1.15.0': + '@zag-js/file-upload@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/file-utils': 1.35.3 + '@zag-js/i18n-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/highlight-word@1.15.0': {} + '@zag-js/file-utils@1.35.3': + dependencies: + '@zag-js/i18n-utils': 1.35.3 - '@zag-js/hover-card@1.15.0': + '@zag-js/floating-panel@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/store': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/i18n-utils@1.15.0': + '@zag-js/focus-trap@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 + '@zag-js/dom-query': 1.35.3 - '@zag-js/interact-outside@1.15.0': + '@zag-js/focus-visible@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/dom-query': 1.35.3 - '@zag-js/listbox@1.15.0': + '@zag-js/highlight-word@1.35.3': {} + + '@zag-js/hover-card@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/collection': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-visible': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/live-region@1.15.0': {} + '@zag-js/i18n-utils@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 - '@zag-js/menu@1.15.0': + '@zag-js/image-cropper@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/rect-utils': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/number-input@1.15.0': + '@zag-js/interact-outside@1.35.3': dependencies: - '@internationalized/number': 3.6.2 - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/json-tree-utils@1.35.3': {} - '@zag-js/pagination@1.15.0': + '@zag-js/listbox@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/password-input@1.15.0': + '@zag-js/live-region@1.35.3': {} + + '@zag-js/marquee@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/pin-input@1.15.0': + '@zag-js/menu@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/popover@1.15.0': + '@zag-js/navigation-menu@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/aria-hidden': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-trap': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/remove-scroll': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/popper@1.15.0': + '@zag-js/number-input@1.35.3': dependencies: - '@floating-ui/dom': 1.7.1 - '@zag-js/dom-query': 1.15.0 - '@zag-js/utils': 1.15.0 + '@internationalized/number': 3.6.5 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/presence@1.15.0': + '@zag-js/pagination@1.35.3': dependencies: - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/progress@1.15.0': + '@zag-js/password-input@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/qr-code@1.15.0': + '@zag-js/pin-input@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/popover@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/remove-scroll': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/popper@1.35.3': + dependencies: + '@floating-ui/dom': 1.7.6 + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/presence@1.35.3': + dependencies: + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + + '@zag-js/progress@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/qr-code@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 proxy-memoize: 3.0.1 uqr: 0.1.2 - '@zag-js/radio-group@1.15.0': + '@zag-js/radio-group@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-visible': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/rating-group@1.15.0': + '@zag-js/rating-group@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/react@1.15.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@zag-js/react@1.35.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@zag-js/core': 1.15.0 - '@zag-js/store': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/core': 1.35.3 + '@zag-js/store': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@zag-js/rect-utils@1.15.0': {} + '@zag-js/rect-utils@1.35.3': {} + + '@zag-js/remove-scroll@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 - '@zag-js/remove-scroll@1.15.0': + '@zag-js/scroll-area@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/scroll-snap@1.15.0': + '@zag-js/scroll-snap@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 + '@zag-js/dom-query': 1.35.3 - '@zag-js/select@1.15.0': + '@zag-js/select@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/collection': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/signature-pad@1.15.0': + '@zag-js/signature-pad@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - perfect-freehand: 1.2.2 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + perfect-freehand: 1.2.3 - '@zag-js/slider@1.15.0': + '@zag-js/slider@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/splitter@1.15.0': + '@zag-js/splitter@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/steps@1.15.0': + '@zag-js/steps@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/store@1.15.0': + '@zag-js/store@1.35.3': dependencies: proxy-compare: 3.0.1 - '@zag-js/switch@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-visible': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/tabs@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/tags-input@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/auto-resize': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/interact-outside': 1.15.0 - '@zag-js/live-region': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/time-picker@1.15.0(@internationalized/date@3.8.1)': - dependencies: - '@internationalized/date': 3.8.1 - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/timer@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/toast@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/toggle-group@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/toggle@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/tooltip@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-visible': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/store': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/tour@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-trap': 1.15.0 - '@zag-js/interact-outside': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/tree-view@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/collection': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/types@1.15.0': - dependencies: - csstype: 3.1.3 - - '@zag-js/utils@1.15.0': {} + '@zag-js/switch@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tabs@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tags-input@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/auto-resize': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/live-region': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/timer@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/toast@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/toggle-group@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/toggle@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tooltip@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tour@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tree-view@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/types@1.35.3': + dependencies: + csstype: 3.2.3 + + '@zag-js/utils@1.35.3': {} acorn-jsx@5.3.2(acorn@8.14.1): dependencies: @@ -6371,15 +6524,11 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - anser@2.3.3: {} - - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 + anser@2.3.5: {} ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} + ansi-regex@6.2.2: {} ansi-styles@4.3.0: dependencies: @@ -6387,7 +6536,7 @@ snapshots: ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} anymatch@3.1.3: dependencies: @@ -6486,7 +6635,7 @@ snapshots: axe-core@4.10.3: {} - axios@1.13.5: + axios@1.13.6: dependencies: follow-redirects: 1.15.11 form-data: 4.0.5 @@ -6498,7 +6647,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.26.10 cosmiconfig: 7.1.0 resolve: 1.22.10 @@ -6510,7 +6659,9 @@ snapshots: balanced-match@1.0.2: {} - balanced-match@4.0.3: {} + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.0: {} binary-extensions@2.3.0: {} @@ -6523,9 +6674,9 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.2: + brace-expansion@5.0.4: dependencies: - balanced-match: 4.0.3 + balanced-match: 4.0.4 braces@3.0.3: dependencies: @@ -6538,6 +6689,14 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.4) + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001777 + electron-to-chromium: 1.5.307 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + builtin-modules@3.3.0: {} c12@1.11.1(magicast@0.3.5): @@ -6582,6 +6741,8 @@ snapshots: caniuse-lite@1.0.30001707: {} + caniuse-lite@1.0.30001777: {} + ccount@2.0.1: {} chai@5.3.3: @@ -6592,12 +6753,12 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 - chakra-react-select@6.1.1(@chakra-ui/react@3.20.0(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.7)(next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + chakra-react-select@6.1.1(@chakra-ui/react@3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@chakra-ui/react': 3.20.0(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@chakra-ui/react': 3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-themes: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 - react-select: 5.10.1(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-select: 5.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - '@types/react' - react-dom @@ -6731,8 +6892,6 @@ snapshots: css.escape@1.5.1: {} - csstype@3.1.3: {} - csstype@3.2.3: {} d3-array@3.2.1: @@ -6837,6 +6996,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decode-named-character-reference@1.1.0: dependencies: character-entities: 2.0.2 @@ -6885,8 +7048,8 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.28.4 - csstype: 3.1.3 + '@babel/runtime': 7.28.6 + csstype: 3.2.3 dotenv@16.6.1: {} @@ -6900,12 +7063,16 @@ snapshots: electron-to-chromium@1.5.123: {} - elkjs@0.11.0: {} + electron-to-chromium@1.5.307: {} + + elkjs@0.11.1: {} emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + entities@7.0.1: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -7012,34 +7179,34 @@ snapshots: es6-promise@4.2.8: {} - esbuild@0.25.11: + esbuild@0.27.3: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.11 - '@esbuild/android-arm': 0.25.11 - '@esbuild/android-arm64': 0.25.11 - '@esbuild/android-x64': 0.25.11 - '@esbuild/darwin-arm64': 0.25.11 - '@esbuild/darwin-x64': 0.25.11 - '@esbuild/freebsd-arm64': 0.25.11 - '@esbuild/freebsd-x64': 0.25.11 - '@esbuild/linux-arm': 0.25.11 - '@esbuild/linux-arm64': 0.25.11 - '@esbuild/linux-ia32': 0.25.11 - '@esbuild/linux-loong64': 0.25.11 - '@esbuild/linux-mips64el': 0.25.11 - '@esbuild/linux-ppc64': 0.25.11 - '@esbuild/linux-riscv64': 0.25.11 - '@esbuild/linux-s390x': 0.25.11 - '@esbuild/linux-x64': 0.25.11 - '@esbuild/netbsd-arm64': 0.25.11 - '@esbuild/netbsd-x64': 0.25.11 - '@esbuild/openbsd-arm64': 0.25.11 - '@esbuild/openbsd-x64': 0.25.11 - '@esbuild/openharmony-arm64': 0.25.11 - '@esbuild/sunos-x64': 0.25.11 - '@esbuild/win32-arm64': 0.25.11 - '@esbuild/win32-ia32': 0.25.11 - '@esbuild/win32-x64': 0.25.11 + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 escalade@3.2.0: {} @@ -7106,19 +7273,19 @@ snapshots: eslint-plugin-perfectionist@4.15.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3): dependencies: '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.1(jiti@1.21.7) natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.7.4): + eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.8.1): dependencies: eslint: 9.39.1(jiti@1.21.7) - prettier: 3.7.4 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.8 + prettier: 3.8.1 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.12 optionalDependencies: eslint-config-prettier: 10.1.8(eslint@9.39.1(jiti@1.21.7)) @@ -7133,7 +7300,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.4.24(eslint@9.39.1(jiti@1.21.7)): + eslint-plugin-react-refresh@0.5.2(eslint@9.39.1(jiti@1.21.7)): dependencies: eslint: 9.39.1(jiti@1.21.7) @@ -7190,6 +7357,8 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint-visitor-keys@5.0.1: {} + eslint@9.39.1(jiti@1.21.7): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) @@ -7281,8 +7450,6 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-safe-stringify@2.1.1: {} - fault@1.0.4: dependencies: format: 0.2.2 @@ -7394,7 +7561,7 @@ snapshots: node-fetch-native: 1.6.7 nypm: 0.5.4 pathe: 2.0.3 - tar: 7.5.10 + tar: 7.5.11 glob-parent@5.1.2: dependencies: @@ -7418,9 +7585,9 @@ snapshots: foreground-child: 3.3.1 jackspeak: 4.2.3 minimatch: 10.2.4 - minipass: 7.1.2 + minipass: 7.1.3 package-json-from-dist: 1.0.1 - path-scurry: 2.0.1 + path-scurry: 2.0.2 globals@11.12.0: {} @@ -7437,7 +7604,7 @@ snapshots: graphemer@1.4.0: {} - graphql@16.12.0: {} + graphql@16.13.1: {} handlebars@4.7.8: dependencies: @@ -7448,11 +7615,17 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - happy-dom@20.0.11: + happy-dom@20.8.3: dependencies: - '@types/node': 20.19.23 + '@types/node': 24.10.3 '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + entities: 7.0.1 whatwg-mimetype: 3.0.0 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate has-bigints@1.1.0: {} @@ -7538,9 +7711,9 @@ snapshots: html-url-attributes@3.0.1: {} - i18next-browser-languagedetector@8.2.0: + i18next-browser-languagedetector@8.2.1: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 i18next-http-backend@3.0.2: dependencies: @@ -7548,9 +7721,9 @@ snapshots: transitivePeerDependencies: - encoding - i18next@25.7.1(typescript@5.9.3): + i18next@25.8.14(typescript@5.9.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 optionalDependencies: typescript: 5.9.3 @@ -7866,7 +8039,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.5: {} + lru-cache@11.2.6: {} lru-cache@5.1.1: dependencies: @@ -7886,7 +8059,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.1 + semver: 7.7.4 markdown-table@3.0.4: {} @@ -8219,7 +8392,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3 decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -8248,7 +8421,7 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.2 + brace-expansion: 5.0.4 minimatch@3.1.5: dependencies: @@ -8262,9 +8435,11 @@ snapshots: minipass@7.1.2: {} + minipass@7.1.3: {} + minizlib@3.1.0: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 mlly@1.8.0: dependencies: @@ -8279,24 +8454,24 @@ snapshots: ms@2.1.3: {} - msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3): + msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3): dependencies: - '@inquirer/confirm': 5.1.8(@types/node@24.10.3) - '@mswjs/interceptors': 0.40.0 + '@inquirer/confirm': 5.1.21(@types/node@24.10.3) + '@mswjs/interceptors': 0.41.3 '@open-draft/deferred-promise': 2.2.0 '@types/statuses': 2.0.6 cookie: 1.1.1 - graphql: 16.12.0 + graphql: 16.13.1 headers-polyfill: 4.0.3 is-node-process: 1.2.0 outvariant: 1.4.3 path-to-regexp: 6.3.0 picocolors: 1.1.1 - rettime: 0.7.0 + rettime: 0.10.1 statuses: 2.0.2 strict-event-emitter: 0.5.1 tough-cookie: 6.0.0 - type-fest: 5.3.0 + type-fest: 5.4.4 until-async: 3.0.2 yargs: 17.7.2 optionalDependencies: @@ -8327,6 +8502,8 @@ snapshots: node-releases@2.0.19: {} + node-releases@2.0.36: {} + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -8462,7 +8639,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.29.0 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -8480,10 +8657,10 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-scurry@2.0.1: + path-scurry@2.0.2: dependencies: - lru-cache: 11.2.5 - minipass: 7.1.2 + lru-cache: 11.2.6 + minipass: 7.1.3 path-to-regexp@6.3.0: {} @@ -8497,7 +8674,7 @@ snapshots: perfect-debounce@1.0.0: {} - perfect-freehand@1.2.2: {} + perfect-freehand@1.2.3: {} picocolors@1.1.1: {} @@ -8513,11 +8690,11 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 - playwright-core@1.57.0: {} + playwright-core@1.58.2: {} - playwright@1.57.0: + playwright@1.58.2: dependencies: - playwright-core: 1.57.0 + playwright-core: 1.58.2 optionalDependencies: fsevents: 2.3.2 @@ -8525,7 +8702,7 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.5.6: + postcss@8.5.8: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -8533,11 +8710,11 @@ snapshots: prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.0: + prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 - prettier@3.7.4: {} + prettier@3.8.1: {} pretty-format@27.5.1: dependencies: @@ -8574,7 +8751,7 @@ snapshots: defu: 6.1.4 destr: 2.0.5 - react-chartjs-2@5.3.0(chart.js@4.5.1)(react@19.2.4): + react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.4): dependencies: chart.js: 4.5.1 react: 19.2.4 @@ -8584,7 +8761,7 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 - react-hook-form@7.56.2(react@19.2.4): + react-hook-form@7.71.2(react@19.2.4): dependencies: react: 19.2.4 @@ -8593,34 +8770,34 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-i18next@15.5.1(i18next@25.7.1(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + react-i18next@15.5.1(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): dependencies: '@babel/runtime': 7.26.10 html-parse-stringify: 3.0.1 - i18next: 25.7.1(typescript@5.9.3) + i18next: 25.8.14(typescript@5.9.3) react: 19.2.4 optionalDependencies: react-dom: 19.2.4(react@19.2.4) typescript: 5.9.3 - react-icons@5.5.0(react@19.2.4): + react-icons@5.6.0(react@19.2.4): dependencies: react: 19.2.4 - react-innertext@1.1.5(@types/react@19.2.7)(react@19.2.4): + react-innertext@1.1.5(@types/react@19.2.14)(react@19.2.4): dependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 react: 19.2.4 react-is@16.13.1: {} react-is@17.0.2: {} - react-markdown@9.1.0(@types/react@19.2.7)(react@19.2.4): + react-markdown@9.1.0(@types/react@19.2.14)(react@19.2.4): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.2.7 + '@types/react': 19.2.14 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 @@ -8641,13 +8818,13 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-router-dom@7.12.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-router-dom@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-router: 7.12.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-router: 7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react-router@7.12.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: cookie: 1.1.1 react: 19.2.4 @@ -8655,19 +8832,19 @@ snapshots: optionalDependencies: react-dom: 19.2.4(react@19.2.4) - react-select@5.10.1(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-select@5.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 '@emotion/cache': 11.14.0 - '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.4) + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4) '@floating-ui/dom': 1.7.1 - '@types/react-transition-group': 4.4.12(@types/react@19.2.7) + '@types/react-transition-group': 4.4.12(@types/react@19.2.14) memoize-one: 6.0.0 prop-types: 15.8.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) react-transition-group: 4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.7)(react@19.2.4) + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.4) transitivePeerDependencies: - '@types/react' - supports-color @@ -8684,7 +8861,7 @@ snapshots: react-transition-group@4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -8801,7 +8978,7 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rettime@0.7.0: {} + rettime@0.10.1: {} robust-predicates@3.0.2: {} @@ -8863,6 +9040,8 @@ snapshots: semver@7.7.1: {} + semver@7.7.4: {} + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: @@ -8971,7 +9150,7 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 string.prototype.includes@2.0.1: dependencies: @@ -9032,9 +9211,9 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: + strip-ansi@7.2.0: dependencies: - ansi-regex: 6.1.0 + ansi-regex: 6.2.2 strip-indent@3.0.0: dependencies: @@ -9062,17 +9241,21 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + synckit@0.11.12: + dependencies: + '@pkgr/core': 0.2.9 + synckit@0.11.8: dependencies: '@pkgr/core': 0.2.4 tagged-tag@1.0.0: {} - tar@7.5.10: + tar@7.5.11: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 - minipass: 7.1.2 + minipass: 7.1.3 minizlib: 3.1.0 yallist: 5.0.0 @@ -9097,11 +9280,11 @@ snapshots: tinyspy@4.0.3: {} - tldts-core@7.0.19: {} + tldts-core@7.0.25: {} - tldts@7.0.19: + tldts@7.0.25: dependencies: - tldts-core: 7.0.19 + tldts-core: 7.0.25 to-fast-properties@2.0.0: {} @@ -9111,7 +9294,7 @@ snapshots: tough-cookie@6.0.0: dependencies: - tldts: 7.0.19 + tldts: 7.0.25 tr46@0.0.3: {} @@ -9119,7 +9302,7 @@ snapshots: trough@2.2.0: {} - ts-api-utils@2.1.0(typescript@5.9.3): + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -9136,13 +9319,11 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.21.3: {} - type-fest@0.6.0: {} type-fest@0.8.1: {} - type-fest@5.3.0: + type-fest@5.4.4: dependencies: tagged-tag: 1.0.0 @@ -9179,12 +9360,12 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3): + typescript-eslint@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.1(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: @@ -9204,8 +9385,6 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@6.21.0: {} - undici-types@7.16.0: {} unified@11.0.5: @@ -9249,6 +9428,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + uqr@0.1.2: {} uri-js@4.4.1: @@ -9257,17 +9442,17 @@ snapshots: urijs@1.19.11: {} - use-debounce@10.0.4(react@19.2.4): + use-debounce@10.1.0(react@19.2.4): dependencies: react: 19.2.4 - use-isomorphic-layout-effect@1.2.1(@types/react@19.2.7)(react@19.2.4): + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.4): dependencies: react: 19.2.4 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 - use-sync-external-store@1.4.0(react@19.2.4): + use-sync-external-store@1.6.0(react@19.2.4): dependencies: react: 19.2.4 @@ -9291,13 +9476,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.2.4(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0): + vite-node@3.2.4(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + vite: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -9312,29 +9497,29 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@3.5.2(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0)): + vite-plugin-css-injected-by-js@3.5.2(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2)): dependencies: - vite: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + vite: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) - vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0): + vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2): dependencies: - esbuild: 0.25.11 + esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - postcss: 8.5.6 + postcss: 8.5.8 rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.10.3 fsevents: 2.3.3 jiti: 1.21.7 - yaml: 2.8.0 + yaml: 2.8.2 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@1.21.7)(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.8.3)(jiti@1.21.7)(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.2): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -9352,13 +9537,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + vite: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.3 - happy-dom: 20.0.11 + happy-dom: 20.8.3 transitivePeerDependencies: - jiti - less @@ -9456,9 +9641,11 @@ snapshots: wrap-ansi@8.1.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 + + ws@8.19.0: {} xtend@4.0.2: {} @@ -9470,7 +9657,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.0: {} + yaml@2.8.2: {} yargs-parser@21.1.1: {} @@ -9486,7 +9673,7 @@ snapshots: yocto-queue@0.1.0: {} - yoctocolors-cjs@2.1.2: {} + yoctocolors-cjs@2.1.3: {} zod-validation-error@4.0.2(zod@4.2.1): dependencies: @@ -9494,17 +9681,17 @@ snapshots: zod@4.2.1: {} - zustand@4.5.6(@types/react@19.2.7)(react@19.2.4): + zustand@4.5.7(@types/react@19.2.14)(react@19.2.4): dependencies: - use-sync-external-store: 1.4.0(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 react: 19.2.4 - zustand@5.0.4(@types/react@19.2.7)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)): + zustand@5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 react: 19.2.4 - use-sync-external-store: 1.4.0(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) zwitch@2.0.4: {} diff --git a/airflow-core/src/airflow/ui/public/i18n/README.md b/airflow-core/src/airflow/ui/public/i18n/README.md index 994c499481f48..c4a269bf8ed7c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/README.md +++ b/airflow-core/src/airflow/ui/public/i18n/README.md @@ -332,16 +332,16 @@ Adding missing translations (with `TODO: translate` prefix): breeze ui check-translation-completeness --language --add-missing ``` -You can also remove extra translations from the language of your choice: +You can also remove unused translations from the language of your choice: ```bash -breeze ui check-translation-completeness --language --remove-extra +breeze ui check-translation-completeness --language --remove-unused ``` Or from all languages: ```bash -breeze ui check-translation-completeness --remove-extra +breeze ui check-translation-completeness --remove-unused ``` diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json index 1f0136b906d25..1b02ee5b9b448 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json @@ -120,7 +120,8 @@ "includeDeferred": "Include Deferred", "nameMaxLength": "Name can contain a maximum of 256 characters", "nameRequired": "Name is required", - "slots": "Slots" + "slots": "Slots", + "slotsHelperText": "Use -1 for unlimited slots." }, "noPoolsFound": "No pools found", "pool_one": "Pool", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json index ba586990e6051..89c5d48e81684 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json @@ -128,6 +128,7 @@ "selectDateRange": "Select Date Range", "startTime": "Start Time" }, + "generateToken": "Generate Token", "logicalDate": "Logical Date", "logout": "Logout", "logoutConfirmation": "You are about to logout from the application.", @@ -312,6 +313,10 @@ "title": "Delete {{resourceName}} Request Submitted" } }, + "forbidden": { + "description": "You do not have permission to perform this action.", + "title": "Access Denied" + }, "import": { "error": "Import {{resourceName}} Request Failed", "success": { @@ -327,6 +332,18 @@ } } }, + "tokenGeneration": { + "apiToken": "API Token", + "cliToken": "CLI Token", + "errorDescription": "An error occurred while generating the token. Please try again.", + "errorTitle": "Token Generation Failed", + "generate": "Generate", + "selectType": "Select the type of token to generate.", + "title": "Generate Token", + "tokenExpiresIn": "This token expires in {{duration}}.", + "tokenGenerated": "Your token has been generated.", + "tokenShownOnce": "This token will only be shown once. Copy it now." + }, "total": "Total {{state}}", "triggered": "Triggered", "tryNumber": "Try Number", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/admin.json index c80f8a8bcb6eb..221a30d6c5c1a 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/admin.json @@ -3,6 +3,7 @@ "description": "描述", "key": "键", "name": "名称", + "team": "团队", "value": "值" }, "config": { @@ -49,6 +50,12 @@ "searchPlaceholder": "搜索连接", "test": "测试连接", "testDisabled": "测试连接功能已停用。请联系管理员以启用。", + "testError": { + "title": "测试连接失败" + }, + "testSuccess": { + "title": "测试连接成功" + }, "typeMeta": { "error": "获取连接类型元数据失败", "standardFields": { @@ -74,6 +81,23 @@ "formActions": { "save": "保存" }, + "jobs": { + "columns": { + "executorClass": "执行器类别", + "hostname": "主机名称", + "id": "ID", + "jobType": "作业类型", + "latestHeartbeat": "最新心跳", + "unixname": "Unix 名称" + }, + "filters": { + "allStates": "全部状态", + "allTypes": "全部类型", + "dagProcessorJob": "Dag 处理器作业", + "schedulerJob": "调度器作业", + "triggererJob": "触发器作业" + } + }, "plugins": { "columns": { "source": "来源" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/assets.json index 059726de65f7a..2e33ab4361404 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/assets.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/assets.json @@ -1,5 +1,9 @@ { + "additional_data": "附加数据", + "asset_many": "资源", + "asset_one": "资源", "consumingDags": "消费者 Dags", + "consumingTasks": "消费者任务", "createEvent": { "button": "创建事件", "manual": { @@ -21,10 +25,13 @@ }, "title": "为 {{name}} 创建资源事件" }, + "extra": "额外信息", "group": "分组", "lastAssetEvent": "最后资源事件", "name": "名称", "producingTasks": "生产任务", "scheduledDags": "已调度的 Dags", - "searchPlaceholder": "搜索资源" + "scheduling": "调度", + "searchPlaceholder": "搜索资源", + "taskDependencies": "任务依赖" } diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/browse.json index 8888c37783993..91625ddb17366 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/browse.json @@ -12,11 +12,35 @@ "title": "审计日志" }, "xcom": { + "add": { + "error": "添加 XCom 失败", + "errorTitle": "错误", + "success": "XCom 添加成功", + "successTitle": "已添加 XCom", + "title": "添加 XCom" + }, "columns": { "dag": "Dag", "key": "键", "value": "值" }, - "title": "XCom" + "delete": { + "error": "删除 XCom 失败", + "errorTitle": "错误", + "success": "XCom 删除成功", + "successTitle": "已删除 XCom", + "title": "删除 XCom", + "warning": "确定要删除此 XCom 吗?此操作无法还原。" + }, + "edit": { + "error": "更新 XCom 失败", + "errorTitle": "错误", + "success": "XCom 更新成功", + "successTitle": "已更新 XCom", + "title": "编辑 XCom" + }, + "key": "键", + "title": "XCom", + "value": "值" } } diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/common.json index 0c22b45055ce2..c5a0d51df0c8a 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/common.json @@ -22,6 +22,7 @@ "backfill_other": "回填", "browse": { "auditLog": "审计日志", + "jobs": "作业", "requiredActions": "待响应的任务实例", "xcoms": "XComs" }, @@ -60,6 +61,8 @@ "dataIntervalEnd": "数据区间结束", "dataIntervalStart": "数据区间起始", "lastSchedulingDecision": "最后调度决策", + "mappedPartitionKey": "映射分区键", + "partitionKey": "分区键", "queuedAt": "开始排队时间", "runAfter": "最早可执行时间", "runType": "执行类型", @@ -73,18 +76,30 @@ "dagWarnings": "Dag 警告 / 错误", "defaultToGraphView": "默认使用图形视图", "defaultToGridView": "默认使用网格视图", + "delete": "删除", + "diff": "差异对比", + "diffCompareWith": "比较对象", + "diffExit": "退出差异对比", + "diffSelectVersionToCompare": "选择要比较的版本", "direction": "书写方向", "docs": { "documentation": "文档", "githubRepo": "GitHub 仓库", "restApiReference": "REST API 参考" }, + "download": { + "download": "下载", + "hotkey": "d", + "tooltip": "按 {{hotkey}} 下载日志" + }, "duration": "执行时间", + "edit": "编辑", "endDate": "结束日期", "error": { "back": "返回", "defaultMessage": "发生未预期的错误", "home": "首页", + "invalidUrl": "找不到页面。请检查 URL 后重试。", "notFound": "找不到页面", "title": "错误" }, @@ -103,22 +118,29 @@ }, "filter": "筛选", "filters": { + "durationFrom": "持续时间起始", + "durationTo": "持续时间结束", + "endTime": "结束时间", "logicalDateFrom": "逻辑日期起始", "logicalDateTo": "逻辑结束日期", "runAfterFrom": "执行时间起始", - "runAfterTo": "执行时间结束" + "runAfterTo": "执行时间结束", + "selectDateRange": "选择日期范围", + "startTime": "开始时间" }, "logicalDate": "逻辑日期", "logout": "退出登录", "logoutConfirmation": "确定要退出登录吗?", "mapIndex": "映射索引", "modal": { + "add": "添加", "cancel": "取消", "confirm": "确认", "delete": { "button": "删除", "confirmation": "确定要删除 {{resourceName}} 吗?此操作无法还原。" - } + }, + "save": "保存" }, "nav": { "admin": "管理", @@ -139,15 +161,13 @@ "placeholder": "添加笔记...", "taskInstance": "任务实例笔记" }, - "pools": { - "deferred": "已延后", - "open": "开放", - "pools_one": "资源池", - "pools_other": "资源池", - "queued": "排队中", - "running": "执行中", - "scheduled": "已调度" + "partitionedDagRun_one": "分区 Dag 执行", + "partitionedDagRun_other": "分区 Dag 执行", + "partitionedDagRunDetail": { + "receivedAssetEvents": "接收到的资源事件" }, + "pendingDagRun_one": "{{count}} 个待执行 Dag 执行", + "pendingDagRun_other": "{{count}} 个待执行 Dag 执行", "reset": "重置", "runId": "执行 ID", "runTypes": { @@ -172,6 +192,7 @@ }, "selectLanguage": "选择语言", "showDetailsPanel": "显示详细信息", + "signedInAs": "当前登录身份", "source": { "hide": "隐藏来源", "hotkey": "s", @@ -186,6 +207,7 @@ "failed": "失败", "no_status": "无状态", "none": "无状态", + "open": "开放", "planned": "已计划", "queued": "排队中", "removed": "已移除", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json index 13d6c72767ca5..a4a06d2ea000f 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json @@ -10,6 +10,7 @@ "maxRuns": "活跃执行数上限", "missingAndErroredRuns": "遗漏和错误的执行", "missingRuns": "遗漏的执行", + "permissionDenied": "预演失败:用户没有权限创建回填作业。", "reprocessBehavior": "重新处理行为", "run": "执行回填", "selectDescription": "为指定的日期范围补上 Dag 执行", @@ -49,6 +50,13 @@ "warning_one": "1 个警告", "warning_other": "{{count}} 个警告" }, + "dateRangeFilter": { + "validation": { + "invalidDateFormat": "日期格式无效。", + "invalidTimeFormat": "时间格式无效。", + "startBeforeEnd": "开始日期/时间必须早于结束日期/时间" + } + }, "durationChart": { "duration": "持续时间 (秒)", "lastDagRun_one": "最近 1 次 Dag 执行", @@ -87,9 +95,13 @@ }, "limitedList": "+ 其他 {{count}} 项", "limitedList.allItems": "所有 {{count}} 项:", + "limitedList.allTags_one": "所有标签 (1)", + "limitedList.allTags_other": "所有标签 ({{count}})", "limitedList.clickToInteract": "点击标签以筛选 Dags", "limitedList.clickToOpenFull": "点击 \"+{{count}} 更多\" 打开完整视图", "limitedList.copyPasteText": "你可以复制并粘贴上方文本", + "limitedList.showingItems_one": "显示 1 项", + "limitedList.showingItems_other": "显示 {{count}} 项", "logs": { "file": "文件", "location": "第 {{line}} 行,位于 {{name}}" @@ -99,17 +111,34 @@ "sortedDescending": "递减排序", "sortedUnsorted": "未排序", "taskTries": "任务尝试次数", + "taskTryPlaceholder": "任务尝试", + "team": { + "selector": { + "helperText": "选填。仅限指定团队使用。", + "label": "团队", + "placeHolder": "选择团队" + } + }, "toggleCardView": "显示卡片视图", "toggleTableView": "显示表格视图", "triggerDag": { "button": "触发", + "dataInterval": "数据区间", + "dataIntervalAuto": "根据逻辑日期和时间表自动推断", + "dataIntervalManual": "手动指定", + "intervalEnd": "结束", + "intervalStart": "开始", "loading": "正在加载 Dag 信息...", "loadingFailed": "加载 Dag 信息失败,请重试。", + "manualRunDenied": "此 Dag 不允许手动执行", "runIdHelp": "选填 - 若未提供将会自动生成", "selectDescription": "触发此 Dag 单次执行", "selectLabel": "单次执行", "title": "触发 Dag", "toaster": { + "error": { + "title": "触发 Dag 失败" + }, "success": { "description": "已成功触发 Dag 执行。", "title": "已触发 Dag 执行" @@ -131,6 +160,7 @@ "versionId": "版本 ID" }, "versionSelect": { + "allVersions": "所有版本", "dagVersion": "Dag 版本", "versionCode": "v{{versionCode}}" } diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dag.json index 6f9a498f7a2c2..4307c3603b2b6 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dag.json @@ -10,6 +10,7 @@ "hourly": "每小时", "legend": { "less": "更少", + "mixed": "混合", "more": "更多" }, "navigation": { @@ -19,6 +20,7 @@ "previousYear": "上一年" }, "noData": "无数据", + "noFailedRuns": "无失败的执行", "noRuns": "未执行", "totalRuns": "总执行次数", "week": "第 {{weekNumber}} 周", @@ -43,7 +45,8 @@ "buttons": { "resetToLatest": "重置为最新", "toggleGroup": "切换分组状态" - } + }, + "runTypeLegend": "执行类型图例" }, "header": { "buttons": { @@ -71,6 +74,12 @@ "navigation": "导航: {{arrow}}", "toggleGroup": "展开/收起分组: 空格键" }, + "notFound": { + "back": "返回", + "backToDags": "返回 Dags", + "message": "Dag \"{{dagId}}\" 不存在。", + "title": "找不到 Dag" + }, "overview": { "buttons": { "failedRun_one": "失败的执行", @@ -112,6 +121,35 @@ }, "graphDirection": { "label": "图表方向" + }, + "showVersionIndicator": { + "label": "显示版本指示器", + "options": { + "hideAll": "全部隐藏", + "showAll": "全部显示", + "showBundleVersion": "显示套件包版本", + "showDagVersion": "显示 Dag 版本" + } + }, + "taskStreamFilter": { + "activeFilter": "已启用的筛选器", + "clearFilter": "清除筛选器", + "clickTask": "点击任务以选取为筛选根节点", + "depth": "深度", + "direction": "方向", + "label": "筛选", + "mode": "模式", + "modeTooltip": "静态模式下切换任务时保持当前视图不变,遍历模式下点击任务时自动更新筛选条件,方便浏览 Dag 结构。", + "modes": { + "static": "静态", + "traverse": "遍历" + }, + "options": { + "both": "同时显示上游和下游", + "downstream": "下游", + "upstream": "上游" + }, + "selectedTask": "已选择的任务" } }, "paramsFailed": "加载参数失败", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dags.json index 90ba4ff9588cd..2f74392d809c5 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dags.json @@ -34,6 +34,10 @@ "error": "清除 {{type}} 时发生错误", "title": "清除 {{type}}" }, + "confirmationDialog": { + "description": "任务当前处于 {{state}} 状态,由用户 {{user}} 于 {{time}} 启动。\n在任务完成执行前,或用户在清除任务对话框中取消勾选「防止重复执行执行中的任务」选项前,无法清除此任务。", + "title": "无法清除任务实例" + }, "delete": { "button": "删除 {{type}}", "dialog": { @@ -61,6 +65,7 @@ "future": "未来", "onlyFailed": "只清除失败任务", "past": "过去", + "preventRunningTasks": "防止重复执行执行中的任务", "queueNew": "加入新任务到队列", "runOnLatestVersion": "执行最新套件包版本", "upstream": "上游" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dashboard.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dashboard.json index 89022de3eb740..6338ec2497815 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dashboard.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dashboard.json @@ -29,7 +29,8 @@ "poolSlots": "资源池配额", "sortBy": { "newestFirst": "由新到旧", - "oldestFirst": "由旧到新" + "oldestFirst": "由旧到新", + "placeholder": "排序方式" }, "source": "来源", "stats": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/hitl.json index 0c4cc910c60a4..4548c5770675b 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/hitl.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/hitl.json @@ -1,5 +1,7 @@ { "filters": { + "body": "内容", + "createdAt": "创建时间", "response": { "all": "全部", "pending": "待响应", @@ -12,11 +14,13 @@ "requiredActionCount_other": "待操作的任务实例 ({{count}})", "requiredActionState": "待操作的任务实例状态", "response": { + "created": "响应创建时间:", "error": "响应失败", "optionsDescription": "请为此任务实例选择一个选项", "optionsLabel": "选项", "received": "收到响应的时间:", "respond": "发送响应", + "responded_by_user_name": "响应者(用户名称)", "success": "任务 {{taskId}} 响应成功", "title": "人类参与流程任务实例 - {{taskId}}" }, diff --git a/airflow-core/src/airflow/ui/src/components/AnsiRenderer.tsx b/airflow-core/src/airflow/ui/src/components/AnsiRenderer.tsx index aea00ca4afa1b..716160d260095 100644 --- a/airflow-core/src/airflow/ui/src/components/AnsiRenderer.tsx +++ b/airflow-core/src/airflow/ui/src/components/AnsiRenderer.tsx @@ -18,6 +18,7 @@ */ import { chakra } from "@chakra-ui/react"; import Anser, { type AnserJsonEntry } from "anser"; +import type { JSX } from "react"; import * as React from "react"; const fixBackspace = (inputText: string): string => { diff --git a/airflow-core/src/airflow/ui/src/components/DataTable/types.ts b/airflow-core/src/airflow/ui/src/components/DataTable/types.ts index 38a8b0e36b892..5e2c047db6f23 100644 --- a/airflow-core/src/airflow/ui/src/components/DataTable/types.ts +++ b/airflow-core/src/airflow/ui/src/components/DataTable/types.ts @@ -18,7 +18,7 @@ */ import type { SimpleGridProps } from "@chakra-ui/react"; import type { ColumnDef, PaginationState, SortingState, VisibilityState } from "@tanstack/react-table"; -import type { ReactNode } from "react"; +import type { JSX, ReactNode } from "react"; export type TableState = { columnVisibility?: VisibilityState; diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx index 186ebb5f6f19e..8008307fabf40 100644 --- a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx +++ b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx @@ -35,8 +35,9 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import type { TaskInstanceResponse, GridRunsResponse } from "openapi/requests/types.gen"; +import { useTimezone } from "src/context/timezone"; import { getComputedCSSVariableValue } from "src/theme"; -import { DEFAULT_DATETIME_FORMAT, renderDuration } from "src/utils/datetimeUtils"; +import { DEFAULT_DATETIME_FORMAT, formatDate, renderDuration } from "src/utils/datetimeUtils"; import { buildTaskInstanceUrl } from "src/utils/links"; ChartJS.register( @@ -69,15 +70,35 @@ const getDuration = (start: string, end: string | null) => { return dayjs.duration(endDate.diff(startDate)).asSeconds(); }; +const getTickLabelFormat = (entries: Array): string => { + if (entries.length < 2) { + return "HH:mm:ss"; + } + + const first = dayjs(entries[0]?.run_after); + const last = dayjs(entries[entries.length - 1]?.run_after); + + if (!first.isValid() || !last.isValid()) { + return "MMM DD"; + } + + const diffInDays = Math.abs(last.diff(first, "day")); + + return diffInDays < 1 ? "HH:mm:ss" : "MMM DD HH:mm"; +}; + export const DurationChart = ({ entries, + isAutoRefreshing = false, kind, }: { readonly entries: Array | undefined; + readonly isAutoRefreshing?: boolean; readonly kind: "Dag Run" | "Task Instance"; }) => { const { t: translate } = useTranslation(["components", "common"]); const navigate = useNavigate(); + const { selectedTimezone } = useTimezone(); const [queuedColorToken] = useToken("colors", ["queued.solid"]); // Get states and create color tokens for them @@ -175,6 +196,7 @@ export const DurationChart = ({ }} datasetIdKey="id" options={{ + animation: isAutoRefreshing ? false : undefined, onClick: (_event, elements) => { const [element] = elements; @@ -239,6 +261,8 @@ export const DurationChart = ({ x: { stacked: true, ticks: { + callback: (_value, index) => + formatDate(entries[index]?.run_after, selectedTimezone, getTickLabelFormat(entries)), maxTicksLimit: 3, }, title: { align: "end", display: true, text: translate("common:dagRun.runAfter") }, diff --git a/airflow-core/src/airflow/ui/src/components/Logo.tsx b/airflow-core/src/airflow/ui/src/components/Logo.tsx new file mode 100644 index 0000000000000..3047abbc698fd --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/Logo.tsx @@ -0,0 +1,54 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import type { ComponentProps } from "react"; +import { useState } from "react"; + +import { AirflowPin } from "src/assets/AirflowPin"; +import { useColorMode } from "src/context/colorMode"; +import { useConfig } from "src/queries/useConfig"; + +type LogoProps = ComponentProps; + +export const Logo = ({ height = "1.5em", width = "1.5em", ...rest }: LogoProps) => { + const theme = useConfig("theme") as unknown as { icon?: string; icon_dark_mode?: string } | undefined; + const { colorMode } = useColorMode(); + const darkIcon = theme?.icon_dark_mode ?? undefined; + const lightIcon = theme?.icon ?? undefined; + const iconSrc = colorMode === "dark" && darkIcon !== undefined ? darkIcon : lightIcon; + const hasIconSrc = Boolean(iconSrc); + const [failedLoadingCustomIcon, setFailedLoadingCustomIcon] = useState({ dark: false, light: false }); + + if (hasIconSrc && colorMode && !failedLoadingCustomIcon[colorMode]) { + return ( + // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions + Logo setFailedLoadingCustomIcon((prev) => ({ ...prev, [colorMode]: true }))} + src={iconSrc} + // Chakra allows object as 'height' and 'width' but 'img' tag only allows string or number, so we need to check the type before passing it to 'img' + style={{ + height: typeof height === "string" || typeof height === "number" ? height : "1.5em", + width: typeof width === "string" || typeof width === "number" ? width : "1.5em", + }} + /> + ); + } + + return ; +}; diff --git a/airflow-core/src/airflow/ui/src/components/PoolBar.tsx b/airflow-core/src/airflow/ui/src/components/PoolBar.tsx index e19cce8ecdd00..30f43f73e11c3 100644 --- a/airflow-core/src/airflow/ui/src/components/PoolBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/PoolBar.tsx @@ -26,6 +26,8 @@ import { Tooltip } from "src/components/ui"; import { SearchParamsKeys } from "src/constants/searchParams"; import { type Slots, slotConfigs } from "src/utils/slots"; +export const UNLIMITED_SLOTS = -1; + export const PoolBar = ({ pool, poolsWithSlotType, @@ -37,6 +39,7 @@ export const PoolBar = ({ }) => { const { t: translate } = useTranslation("common"); + const isUnlimited = totalSlots === UNLIMITED_SLOTS; const isDashboard = Boolean(poolsWithSlotType); const includeDeferredInBar = "include_deferred" in pool && pool.include_deferred; const barSlots = ["running", "queued", "open"]; @@ -51,59 +54,69 @@ export const PoolBar = ({ } const preparedSlots = slotConfigs.map((config) => { - const slotType = config.key.replace("_slots", "") as TaskInstanceState; + const slotType = config.key.replace("_slots", "") as TaskInstanceState | "open"; + const rawValue = (pool[config.key] as number | undefined) ?? 0; return { ...config, label: translate(`common:states.${slotType}`), slotType, - slotValue: (pool[config.key] as number | undefined) ?? 0, + slotValue: slotType === "open" && rawValue === UNLIMITED_SLOTS ? Infinity : rawValue, }; }); + const displayedSlots = preparedSlots.filter( + (slot) => barSlots.includes(slot.slotType) && slot.slotValue > 0, + ); + const usedSlots = displayedSlots + .filter((slot) => slot.slotType !== "open") + .reduce((sum, slot) => sum + slot.slotValue, 0); + return ( - {preparedSlots - .filter((slot) => barSlots.includes(slot.slotType) && slot.slotValue > 0) - .map((slot) => { - const flexValue = slot.slotValue / totalSlots || 0; + {displayedSlots.map((slot) => { + const flexValue = isUnlimited + ? slot.slotType === "open" + ? Math.max(1, usedSlots) // open takes at least as much space as all used slots combined + : slot.slotValue + : slot.slotValue / totalSlots || 0; - const poolContent = ( - - - {slot.icon} - - {slot.slotValue} - - - - ); + const poolContent = ( + + + {slot.icon} + + {slot.slotValue === Infinity ? "∞" : slot.slotValue} + + + + ); - return slot.color !== "success" && "name" in pool ? ( - - - {poolContent} - - - ) : ( - + return slot.color !== "success" && "name" in pool ? ( + + {poolContent} - - ); - })} + + + ) : ( + + {poolContent} + + ); + })} @@ -111,7 +124,7 @@ export const PoolBar = ({ .filter((slot) => infoSlots.includes(slot.slotType) && slot.slotValue > 0) .map((slot) => ( - + {slot.label}: {slot.slotValue} diff --git a/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx b/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx index 634fecd600725..5a9095172efeb 100644 --- a/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx +++ b/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Flex, type FlexProps } from "@chakra-ui/react"; +import { Box, Flex, type FlexProps } from "@chakra-ui/react"; import Editor, { type OnMount } from "@monaco-editor/react"; import { useCallback } from "react"; @@ -46,30 +46,34 @@ const RenderedJsonField = ({ collapsed = false, content, enableClipboard = true, ); return ( - - + + + + + + {enableClipboard ? ( diff --git a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx index 33d7b4e4c54f9..f3febe79687be 100644 --- a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx +++ b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx @@ -18,6 +18,7 @@ */ import { chakra, Code, Link } from "@chakra-ui/react"; import type { TFunction } from "i18next"; +import type { JSX } from "react"; import * as React from "react"; import { Link as RouterLink } from "react-router-dom"; diff --git a/airflow-core/src/airflow/ui/src/components/ui/createErrorToaster.ts b/airflow-core/src/airflow/ui/src/components/ui/createErrorToaster.ts new file mode 100644 index 0000000000000..c59cabbf3b675 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/ui/createErrorToaster.ts @@ -0,0 +1,40 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import type { TFunction } from "i18next"; + +import type { ExpandedApiError } from "src/components/ErrorAlert"; +import { toaster } from "src/components/ui"; + +type ErrorToastMessage = { + readonly description: string; + readonly title: string; +}; + +export const createErrorToaster = + (translate: TFunction, fallbackMessage: ErrorToastMessage) => (error: unknown) => { + const isForbidden = (error as ExpandedApiError).status === 403; + + toaster.create({ + description: isForbidden + ? translate("toaster.forbidden.description", { ns: "common" }) + : fallbackMessage.description, + title: isForbidden ? translate("toaster.forbidden.title", { ns: "common" }) : fallbackMessage.title, + type: "error", + }); + }; diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx index 698d485f93c0f..691ed35c04272 100644 --- a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx +++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx @@ -77,6 +77,12 @@ export const useFilterConfigs = () => { label: translate("hitl:filters.body"), type: FilterTypes.TEXT, }, + [SearchParamsKeys.BUNDLE_VERSION]: { + hotkeyDisabled: true, + icon: , + label: translate("common:bundleVersion"), + type: FilterTypes.TEXT, + }, [SearchParamsKeys.CONF_CONTAINS]: { hotkeyDisabled: true, icon: , diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts b/airflow-core/src/airflow/ui/src/constants/searchParams.ts index 0215a55b2da51..d37001e768802 100644 --- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts +++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts @@ -21,6 +21,7 @@ export enum SearchParamsKeys { ASSET_EVENT_DATE_RANGE = "asset_event_date_range", BEFORE = "before", BODY_SEARCH = "body_search", + BUNDLE_VERSION = "bundle_version", CONF_CONTAINS = "conf_contains", CREATED_AT_GTE = "created_at_gte", CREATED_AT_LTE = "created_at_lte", diff --git a/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx index cb645babc82ad..30023505e6273 100644 --- a/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx @@ -32,6 +32,7 @@ export const BaseLayout = ({ children }: PropsWithChildren) => { const instanceName = useConfig("instance_name"); const { i18n } = useTranslation(); const { data: pluginData } = usePluginServiceGetPlugins(); + const theme = useConfig("theme") as unknown as { icon?: string; icon_dark_mode?: string } | undefined; const baseReactPlugins = pluginData?.plugins @@ -59,6 +60,47 @@ export const BaseLayout = ({ children }: PropsWithChildren) => { }; }, [i18n]); + useEffect(() => { + const link = document.querySelector("link[rel='icon']"); + + if (!link) { + return undefined; + } + + const defaultFavicon = link.href; + const darkIcon = theme?.icon_dark_mode; + const lightIcon = theme?.icon; + // favicon color theme should follow system color scheme, not the one set in Airflow UI. + // (tab colors in browsers are based on system color scheme, not the one set in the website) + const darkModeQuery = globalThis.matchMedia("(prefers-color-scheme: dark)"); + + const updateFavicon = () => { + const customIcon = + darkModeQuery.matches && typeof darkIcon === "string" && darkIcon.length > 0 ? darkIcon : lightIcon; + + if (typeof customIcon === "string" && customIcon.length > 0) { + link.href = customIcon; + + const img = new Image(); + + img.addEventListener("error", () => { + link.href = defaultFavicon; + }); + img.src = customIcon; + } else { + link.href = defaultFavicon; + } + }; + + updateFavicon(); + darkModeQuery.addEventListener("change", updateFavicon); + + return () => { + darkModeQuery.removeEventListener("change", updateFavicon); + link.href = defaultFavicon; + }; + }, [theme?.icon, theme?.icon_dark_mode]); + return ( diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx index 7053e6fc5cbe9..4e4e9e39b1728 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Flex, VStack, useDisclosure } from "@chakra-ui/react"; +import { Box, Flex, Text, VStack, useDisclosure } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { FiDatabase, FiHome, FiClock } from "react-icons/fi"; import { Link } from "react-router-dom"; @@ -27,9 +27,10 @@ import { usePluginServiceGetPlugins, } from "openapi/queries"; import type { ExternalViewResponse } from "openapi/requests/types.gen"; -import { AirflowPin } from "src/assets/AirflowPin"; import { DagIcon } from "src/assets/DagIcon"; +import { Logo } from "src/components/Logo"; import { useTimezone } from "src/context/timezone"; +import { useConfig } from "src/queries/useConfig"; import { getTimezoneOffsetString, getTimezoneTooltipLabel } from "src/utils/datetimeUtils"; import type { NavItemResponse } from "src/utils/types"; @@ -100,6 +101,8 @@ export const Nav = () => { const { selectedTimezone } = useTimezone(); const offset = getTimezoneOffsetString(selectedTimezone); const tooltipLabel = getTimezoneTooltipLabel(selectedTimezone); + const theme = useConfig("theme") as unknown as { icon?: string; icon_dark_mode?: string } | undefined; + const hasCustomLogo = Boolean(theme?.icon) || Boolean(theme?.icon_dark_mode); // Get both external views and react apps with nav destination const navItems: Array = @@ -157,7 +160,7 @@ export const Nav = () => { - { - + - - - + + {hasCustomLogo ? ( + + {/* eslint-disable-next-line i18next/no-literal-string -- Trademark must not be translated */} + + Apache Airflow® + + + ) : undefined} diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.tsx index 50801577333e5..f943efc32aa1d 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.tsx @@ -72,6 +72,7 @@ export const NavButton = ({ icon, isExternal = false, pluginIcon, title, to, ... color: "fg", }, alignItems: "center", + "aria-label": title, bg: isActive ? "brand.solid" : undefined, borderRadius: "md", borderWidth: 0, @@ -84,7 +85,7 @@ export const NavButton = ({ icon, isExternal = false, pluginIcon, title, to, ... overflow: "hidden", padding: 0, textDecoration: "none", - title, + transition: "background-color 0.2s ease, color 0.2s ease", variant: "plain", whiteSpace: "wrap", diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/TokenGenerationModal.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/TokenGenerationModal.tsx new file mode 100644 index 0000000000000..796c6e2d0e2f8 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/TokenGenerationModal.tsx @@ -0,0 +1,137 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Box, Button, Flex, HStack, Text } from "@chakra-ui/react"; +import React, { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { FiAlertTriangle } from "react-icons/fi"; + +import { useAuthLinksServiceGenerateToken } from "openapi/queries"; +import type { GenerateTokenResponse } from "openapi/requests/types.gen"; +import { Dialog, toaster } from "src/components/ui"; +import { ClipboardIconButton, ClipboardInput, ClipboardRoot } from "src/components/ui/Clipboard"; + +type TokenGenerationModalProps = { + readonly isOpen: boolean; + readonly onClose: () => void; +}; + +type TokenType = "api" | "cli"; + +const formatExpiration = (seconds: number): string => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + if (hours > 0 && minutes > 0) { + return `${String(hours)}h ${String(minutes)}m`; + } else if (hours > 0) { + return `${String(hours)}h`; + } + + return `${String(minutes)}m`; +}; + +const TokenGenerationModal: React.FC = ({ isOpen, onClose }) => { + const { t: translate } = useTranslation(); + const [tokenType, setTokenType] = useState("api"); + const [generatedToken, setGeneratedToken] = useState(null); + const [expiresIn, setExpiresIn] = useState(null); + + const { isPending, mutate: generateToken } = useAuthLinksServiceGenerateToken({ + onError: (error: unknown) => { + toaster.create({ + description: error instanceof Error ? error.message : translate("tokenGeneration.errorDescription"), + title: translate("tokenGeneration.errorTitle"), + type: "error", + }); + }, + onSuccess: (data: GenerateTokenResponse) => { + setGeneratedToken(data.access_token); + setExpiresIn(data.expires_in_seconds); + }, + }); + + const handleClose = useCallback(() => { + setGeneratedToken(null); + setExpiresIn(null); + setTokenType("api"); + onClose(); + }, [onClose]); + + const handleGenerate = useCallback(() => { + generateToken({ requestBody: { token_type: tokenType } }); + }, [generateToken, tokenType]); + + return ( + + + {translate("tokenGeneration.title")} + + + {generatedToken !== null && generatedToken !== "" ? ( + + + {translate("tokenGeneration.tokenGenerated")} + + + + + + + + + + {translate("tokenGeneration.tokenShownOnce")} + + {expiresIn !== null && expiresIn > 0 ? ( + + {translate("tokenGeneration.tokenExpiresIn", { + duration: formatExpiration(expiresIn), + })} + + ) : undefined} + + ) : ( + + {translate("tokenGeneration.selectType")} + + + + + + + )} + + + + ); +}; + +export default TokenGenerationModal; diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx index 2ad032457e7ef..ace6165ff4533 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx @@ -20,6 +20,7 @@ import { Box, Icon, useDisclosure } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { FiGrid, + FiKey, FiLogOut, FiMoon, FiSun, @@ -43,6 +44,7 @@ import LanguageModal from "./LanguageModal"; import LogoutModal from "./LogoutModal"; import { NavButton } from "./NavButton"; import { PluginMenuItem } from "./PluginMenuItem"; +import TokenGenerationModal from "./TokenGenerationModal"; const COLOR_MODES = { DARK: "dark", @@ -77,6 +79,7 @@ export const UserSettingsButton = ({ externalViews }: { readonly externalViews: const { onClose: onCloseLogout, onOpen: onOpenLogout, open: isOpenLogout } = useDisclosure(); const { onClose: onCloseLanguage, onOpen: onOpenLanguage, open: isOpenLanguage } = useDisclosure(); + const { onClose: onCloseToken, onOpen: onOpenToken, open: isOpenToken } = useDisclosure(); const [dagView, setDagView] = useLocalStorage<"graph" | "grid">(DEFAULT_DAG_VIEW_KEY, "grid"); @@ -138,6 +141,10 @@ export const UserSettingsButton = ({ externalViews }: { readonly externalViews: {dagView === "grid" ? translate("defaultToGraphView") : translate("defaultToGridView")} + + + {translate("generateToken")} + {externalViews.map((view) => ( ))} @@ -150,6 +157,7 @@ export const UserSettingsButton = ({ externalViews }: { readonly externalViews: + ); }; diff --git a/airflow-core/src/airflow/ui/src/main.tsx b/airflow-core/src/airflow/ui/src/main.tsx index abd40e064d478..44d9934f75e9f 100644 --- a/airflow-core/src/airflow/ui/src/main.tsx +++ b/airflow-core/src/airflow/ui/src/main.tsx @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import * as ChakraUI from "@chakra-ui/react"; +import * as EmotionReact from "@emotion/react"; import { QueryClientProvider } from "@tanstack/react-query"; import axios, { type AxiosError } from "axios"; import { StrictMode } from "react"; @@ -37,13 +39,14 @@ import { getRedirectPath } from "src/utils/links.ts"; import i18n from "./i18n/config"; import { client } from "./queryClient"; -// Set React, ReactDOM, and ReactJSXRuntime on globalThis to share them with the dynamically imported React plugins. -// Only one instance of React should be used. -// Reflect will avoid type checking. +// Set React, ReactDOM, Chakra UI, and Emotion on globalThis so dynamically imported React +// plugins (e.g. HITL Review) use the host's copies instead of bundling their own. Reflect.set(globalThis, "React", React); Reflect.set(globalThis, "ReactDOM", ReactDOM); Reflect.set(globalThis, "ReactJSXRuntime", ReactJSXRuntime); Reflect.set(globalThis, "ReactRouterDOM", ReactRouterDOM); +Reflect.set(globalThis, "ChakraUI", ChakraUI); +Reflect.set(globalThis, "EmotionReact", EmotionReact); // redirect to login page if the API responds with unauthorized or forbidden errors axios.interceptors.response.use( diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/DagNotFound.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/DagNotFound.tsx index 0a667e8ef63a8..55ff3310ca172 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/DagNotFound.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/DagNotFound.tsx @@ -20,7 +20,7 @@ import { Box, Button, Container, Heading, HStack, Text, VStack } from "@chakra-u import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { AirflowPin } from "src/assets/AirflowPin"; +import { Logo } from "src/components/Logo"; type DagNotFoundProps = { readonly dagId: string; @@ -34,7 +34,7 @@ export const DagNotFound = ({ dagId }: DagNotFoundProps) => { - + 404 diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx index 043ae4d1b1509..7ea155fe84761 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx @@ -36,6 +36,7 @@ import { TrendCountButton } from "src/components/TrendCountButton"; import { dagRunsLimitKey } from "src/constants/localStorage"; import { SearchParamsKeys } from "src/constants/searchParams"; import { useGridRuns } from "src/queries/useGridRuns.ts"; +import { isStatePending, useAutoRefresh } from "src/utils"; const FailedLogs = lazy(() => import("./FailedLogs")); @@ -68,6 +69,9 @@ export const Overview = () => { state: ["failed"], }); const { data: gridRuns, isLoading: isLoadingRuns } = useGridRuns({ limit }); + const refetchInterval = useAutoRefresh({ dagId }); + const isAutoRefreshing = + Boolean(refetchInterval) && (gridRuns ?? []).some((run) => isStatePending(run.state)); const { data: assetEventsData, isLoading: isLoadingAssetEvents } = useAssetServiceGetAssetEvents({ limit, orderBy: [assetSortBy], @@ -125,7 +129,11 @@ export const Overview = () => { {isLoadingRuns ? ( ) : ( - + )} {assetEventsData && assetEventsData.total_entries > 0 ? ( diff --git a/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx b/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx index a05a756c0a47c..94b33640312e1 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx @@ -45,6 +45,7 @@ import { renderDuration, useAutoRefresh, isStatePending } from "src/utils"; type DagRunRow = { row: { original: DAGRunResponse } }; const { + BUNDLE_VERSION: BUNDLE_VERSION_PARAM, CONF_CONTAINS: CONF_CONTAINS_PARAM, DAG_ID_PATTERN: DAG_ID_PATTERN_PARAM, DAG_VERSION: DAG_VERSION_PARAM, @@ -213,6 +214,7 @@ export const DagRuns = () => { const filteredTriggeringUserNamePattern = searchParams.get(TRIGGERING_USER_NAME_PATTERN_PARAM); const filteredDagIdPattern = searchParams.get(DAG_ID_PATTERN_PARAM); const filteredDagVersion = searchParams.get(DAG_VERSION_PARAM); + const bundleVersion = searchParams.get(BUNDLE_VERSION_PARAM); const startDateGte = searchParams.get(START_DATE_GTE_PARAM); const startDateLte = searchParams.get(START_DATE_LTE_PARAM); const endDateGte = searchParams.get(END_DATE_GTE_PARAM); @@ -230,6 +232,7 @@ export const DagRuns = () => { const { data, error, isLoading } = useDagRunServiceGetDagRuns( { + bundleVersion: bundleVersion ?? undefined, confContains: confContains !== null && confContains !== "" ? confContains : undefined, dagId: dagId ?? "~", dagIdPattern: filteredDagIdPattern ?? undefined, diff --git a/airflow-core/src/airflow/ui/src/pages/DagRunsFilters.tsx b/airflow-core/src/airflow/ui/src/pages/DagRunsFilters.tsx index 201b77d0814cb..b0f90d66f9876 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagRunsFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagRunsFilters.tsx @@ -41,6 +41,7 @@ export const DagRunsFilters = ({ dagId }: DagRunsFiltersProps) => { SearchParamsKeys.TRIGGERING_USER_NAME_PATTERN, SearchParamsKeys.DAG_VERSION, SearchParamsKeys.PARTITION_KEY_PATTERN, + SearchParamsKeys.BUNDLE_VERSION, ]; if (dagId === undefined) { diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.tsx index 65f4ba790958c..9955ad82a8d10 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.tsx @@ -24,7 +24,7 @@ import { Link as RouterLink } from "react-router-dom"; import { type PoolServiceGetPoolsDefaultResponse, useAuthLinksServiceGetAuthMenus } from "openapi/queries"; import { usePoolServiceGetPools } from "openapi/queries/queries"; import type { ApiError } from "openapi/requests"; -import { PoolBar } from "src/components/PoolBar"; +import { PoolBar, UNLIMITED_SLOTS } from "src/components/PoolBar"; import { useAutoRefresh } from "src/utils"; import { type Slots, slotKeys } from "src/utils/slots"; @@ -52,7 +52,10 @@ export const PoolSummary = () => { } const pools = data?.pools; - const totalSlots = pools?.reduce((sum, pool) => sum + pool.slots, 0) ?? 0; + const hasUnlimitedPool = pools?.some((pool) => pool.slots === UNLIMITED_SLOTS) ?? false; + const totalSlots = hasUnlimitedPool + ? UNLIMITED_SLOTS + : (pools?.reduce((sum, pool) => sum + pool.slots, 0) ?? 0); const aggregatePool: Slots = { deferred_slots: 0, open_slots: 0, @@ -73,8 +76,13 @@ export const PoolSummary = () => { slotKeys.forEach((slotKey) => { const slotValue = pool[slotKey]; - if (slotValue > 0) { - aggregatePool[slotKey] += slotValue; + if (slotValue === UNLIMITED_SLOTS) { + aggregatePool[slotKey] = UNLIMITED_SLOTS; + poolsWithSlotType[slotKey] += 1; + } else if (slotValue > 0) { + if (aggregatePool[slotKey] !== UNLIMITED_SLOTS) { + aggregatePool[slotKey] += slotValue; + } poolsWithSlotType[slotKey] += 1; } }); diff --git a/airflow-core/src/airflow/ui/src/pages/Error.tsx b/airflow-core/src/airflow/ui/src/pages/Error.tsx index fb10bdb212a36..a6a1fdbe1d67c 100644 --- a/airflow-core/src/airflow/ui/src/pages/Error.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Error.tsx @@ -20,7 +20,7 @@ import { Box, VStack, Heading, Text, Button, Container, HStack, Code } from "@ch import { useTranslation } from "react-i18next"; import { useNavigate, useRouteError, isRouteErrorResponse } from "react-router-dom"; -import { AirflowPin } from "src/assets/AirflowPin"; +import { Logo } from "src/components/Logo"; export const ErrorPage = () => { const navigate = useNavigate(); @@ -51,7 +51,7 @@ export const ErrorPage = () => { - + {statusCode || translate("error.title")} diff --git a/airflow-core/src/airflow/ui/src/pages/Iframe.tsx b/airflow-core/src/airflow/ui/src/pages/Iframe.tsx index eb02d17bd86c2..6d3f5152c2048 100644 --- a/airflow-core/src/airflow/ui/src/pages/Iframe.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Iframe.tsx @@ -35,13 +35,13 @@ export const Iframe = ({ if (externalView.destination !== undefined && externalView.destination !== "nav") { // Check if the href contains placeholders that need to be replaced if (dagId !== undefined) { - src = src.replaceAll("{DAG_ID}", dagId); + src = src.replaceAll("{DAG_ID}", encodeURIComponent(dagId)); } if (runId !== undefined) { - src = src.replaceAll("{RUN_ID}", runId); + src = src.replaceAll("{RUN_ID}", encodeURIComponent(runId)); } if (taskId !== undefined) { - src = src.replaceAll("{TASK_ID}", taskId); + src = src.replaceAll("{TASK_ID}", encodeURIComponent(taskId)); } if (mapIndex !== undefined) { src = src.replaceAll("{MAP_INDEX}", mapIndex); diff --git a/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx b/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx index ded363b4fc34a..8be4f5d1d4732 100644 --- a/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx @@ -20,7 +20,7 @@ import { Box, Flex, HStack, Text, VStack } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import type { PoolResponse } from "openapi/requests/types.gen"; -import { PoolBar } from "src/components/PoolBar"; +import { PoolBar, UNLIMITED_SLOTS } from "src/components/PoolBar"; import { StateIcon } from "src/components/StateIcon"; import { Tooltip } from "src/components/ui"; @@ -40,8 +40,8 @@ const PoolBarCard = ({ pool }: PoolBarCardProps) => { - {pool.name} ({pool.slots} {translate("pools.form.slots")}) - {pool.team_name !== null && ` (${pool.team_name})`} + {pool.name} ({pool.slots === UNLIMITED_SLOTS ? "∞" : pool.slots} {translate("pools.form.slots")} + ){pool.team_name !== null && ` (${pool.team_name})`} {pool.include_deferred ? ( diff --git a/airflow-core/src/airflow/ui/src/pages/Pools/PoolForm.tsx b/airflow-core/src/airflow/ui/src/pages/Pools/PoolForm.tsx index 9025ddc330a90..6fe5784d99551 100644 --- a/airflow-core/src/airflow/ui/src/pages/Pools/PoolForm.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Pools/PoolForm.tsx @@ -91,7 +91,7 @@ const PoolForm = ({ error, initialPool, isPending, manageMutate, setError }: Poo {translate("pools.form.slots")} { const value = event.target.valueAsNumber; @@ -101,6 +101,7 @@ const PoolForm = ({ error, initialPool, isPending, manageMutate, setError }: Poo type="number" value={field.value} /> + {translate("pools.form.slotsHelperText")} )} /> diff --git a/airflow-core/src/airflow/ui/src/pages/Security.tsx b/airflow-core/src/airflow/ui/src/pages/Security.tsx index 011c101868697..c3b0fb89309d4 100644 --- a/airflow-core/src/airflow/ui/src/pages/Security.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Security.tsx @@ -17,8 +17,7 @@ * under the License. */ import { Box } from "@chakra-ui/react"; -import { useParams } from "react-router-dom"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { useAuthLinksServiceGetAuthMenus } from "openapi/queries"; import { ProgressBar } from "src/components/ui"; @@ -43,8 +42,12 @@ export const Security = () => { const onLoad = () => { const iframe: HTMLIFrameElement | null = document.querySelector("#security-iframe"); - if (iframe?.contentWindow && !iframe.contentWindow.location.pathname.startsWith("/auth/")) { - void Promise.resolve(navigate("/")); + if (iframe?.contentWindow) { + const base = new URL(document.baseURI).pathname.replace(/\/$/u, ""); // Remove trailing slash if exists + + if (!iframe.contentWindow.location.pathname.startsWith(`${base}/auth/`)) { + void navigate("/"); + } } }; diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx index 6393bc94d719c..253bdd0bc3de8 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx @@ -131,6 +131,6 @@ describe("Task log grouping", () => { fireEvent.click(collapseItem); - await waitFor(() => expect(screen.queryByText(/Marking task as SUCCESS/iu)).toBeVisible()); + await waitFor(() => expect(screen.queryByText(/Marking task as SUCCESS/iu)).not.toBeVisible()); }, 10_000); }); diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx index 694ba0ee87189..f6df9e1c420d0 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx @@ -18,7 +18,7 @@ */ import { Box, Code, VStack, IconButton } from "@chakra-ui/react"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { useLayoutEffect, useRef } from "react"; +import { type JSX, useLayoutEffect, useRef } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { FiChevronDown, FiChevronUp } from "react-icons/fi"; diff --git a/airflow-core/src/airflow/ui/src/queries/useDagParsing.ts b/airflow-core/src/airflow/ui/src/queries/useDagParsing.ts index 914799dfc5499..2fc8c43c0e394 100644 --- a/airflow-core/src/airflow/ui/src/queries/useDagParsing.ts +++ b/airflow-core/src/airflow/ui/src/queries/useDagParsing.ts @@ -25,18 +25,15 @@ import { UseDagSourceServiceGetDagSourceKeyFn, } from "openapi/queries"; import { toaster } from "src/components/ui"; +import { createErrorToaster } from "src/components/ui/createErrorToaster"; export const useDagParsing = ({ dagId }: { readonly dagId: string }) => { const queryClient = useQueryClient(); - const { t: translate } = useTranslation("dag"); - - const onError = () => { - toaster.create({ - description: translate("parse.toaster.error.description"), - title: translate("parse.toaster.error.title"), - type: "error", - }); - }; + const { t: translate } = useTranslation(["dag", "common"]); + const onError = createErrorToaster(translate, { + description: translate("parse.toaster.error.description"), + title: translate("parse.toaster.error.title"), + }); const onSuccess = async () => { await queryClient.invalidateQueries({ diff --git a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx index b6278dcdb0bb5..1a82896741468 100644 --- a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx +++ b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx @@ -20,6 +20,7 @@ import { chakra, Box } from "@chakra-ui/react"; import type { UseQueryOptions } from "@tanstack/react-query"; import dayjs from "dayjs"; import type { TFunction } from "i18next"; +import type { JSX } from "react"; import { useTranslation } from "react-i18next"; import innerText from "react-innertext"; diff --git a/airflow-core/src/airflow/ui/src/utils/links.test.ts b/airflow-core/src/airflow/ui/src/utils/links.test.ts index 47dd032ebc519..75d175e22a599 100644 --- a/airflow-core/src/airflow/ui/src/utils/links.test.ts +++ b/airflow-core/src/airflow/ui/src/utils/links.test.ts @@ -243,7 +243,7 @@ describe("buildTaskInstanceUrl", () => { }), ).toBe("/dags/new_dag/runs/new_run/tasks/group/new_group"); - // Groups should never preserve tabs even for mapped groups + // Groups should never get /mapped appended — no such route exists for task groups expect( buildTaskInstanceUrl({ currentPathname: "/dags/old/runs/old/tasks/group/old_group/events", @@ -254,6 +254,21 @@ describe("buildTaskInstanceUrl", () => { runId: "new_run", taskId: "new_group", }), - ).toBe("/dags/new_dag/runs/new_run/tasks/group/new_group/mapped/3"); + ).toBe("/dags/new_dag/runs/new_run/tasks/group/new_group"); + }); + + it("should not append /mapped for dynamic task groups from grid view", () => { + // Regression test for https://github.com/apache/airflow/issues/63197 + // Dynamic task groups have isMapped=true but no route exists for group/:groupId/mapped + expect( + buildTaskInstanceUrl({ + currentPathname: "/dags/my_dag/runs/run_1/tasks/group/my_group", + dagId: "my_dag", + isGroup: true, + isMapped: true, + runId: "run_1", + taskId: "my_group", + }), + ).toBe("/dags/my_dag/runs/run_1/tasks/group/my_group"); }); }); diff --git a/airflow-core/src/airflow/ui/src/utils/links.ts b/airflow-core/src/airflow/ui/src/utils/links.ts index 3beafb06afea1..23c438721a5b8 100644 --- a/airflow-core/src/airflow/ui/src/utils/links.ts +++ b/airflow-core/src/airflow/ui/src/utils/links.ts @@ -89,7 +89,7 @@ export const buildTaskInstanceUrl = (params: { let basePath = `/dags/${dagId}/runs/${runId}/tasks/${groupPath}${taskId}`; - if (isMapped) { + if (isMapped && !isGroup) { basePath += `/mapped`; if (mapIndex !== undefined && mapIndex !== "-1") { basePath += `/${mapIndex}`; diff --git a/airflow-core/src/airflow/ui/src/utils/slots.tsx b/airflow-core/src/airflow/ui/src/utils/slots.tsx index 8211e990c7cef..0d1fc385ce659 100644 --- a/airflow-core/src/airflow/ui/src/utils/slots.tsx +++ b/airflow-core/src/airflow/ui/src/utils/slots.tsx @@ -18,6 +18,8 @@ */ /* eslint-disable perfectionist/sort-objects */ +import type { JSX } from "react"; + import type { PoolResponse } from "openapi/requests/types.gen"; import { StateIcon } from "src/components/StateIcon"; diff --git a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts index 2e879ee0c8e4d..17ab2fc35061f 100644 --- a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts +++ b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts @@ -58,6 +58,7 @@ const handleDateRangeChange = ( export type FilterableSearchParamsKeys = | SearchParamsKeys.ASSET_EVENT_DATE_RANGE | SearchParamsKeys.BODY_SEARCH + | SearchParamsKeys.BUNDLE_VERSION | SearchParamsKeys.CONF_CONTAINS | SearchParamsKeys.CREATED_AT_RANGE | SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/AssetDetailPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/AssetDetailPage.ts index a649d067e14ed..964774e6a803f 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/AssetDetailPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/AssetDetailPage.ts @@ -41,19 +41,19 @@ export class AssetDetailPage extends BasePage { await expect(this.page.getByRole("heading", { name })).toBeVisible(); } - public async verifyProducingTasks(minCount: number): Promise { - await this.verifyStatSection("Producing Tasks", minCount); + public async verifyProducingTasks(): Promise { + await this.verifyStatSection("Producing Tasks"); } - public async verifyScheduledDags(minCount: number): Promise { - await this.verifyStatSection("Scheduled Dags", minCount); + public async verifyScheduledDags(): Promise { + await this.verifyStatSection("Scheduled Dags"); } /** * Common helper to verify stat sections (Producing Tasks, Scheduled Dags) * Uses stable selectors based on text content and ARIA roles */ - private async verifyStatSection(labelText: string, minCount: number): Promise { + private async verifyStatSection(labelText: string): Promise { const label = this.page.getByText(labelText, { exact: true }); await expect(label).toBeVisible(); @@ -64,19 +64,17 @@ export class AssetDetailPage extends BasePage { const button = statContainer.getByRole("button").first(); await expect(button).toBeVisible(); + await expect(button).toHaveText(/^[1-9]/); + const text = await button.textContent(); const count = parseInt(text?.split(" ")[0] ?? "0", 10); - expect(count).toBeGreaterThanOrEqual(minCount); - - if (count > 0) { - await button.click(); - await expect(button).toHaveAttribute("aria-expanded", "true", { timeout: 5000 }); - const popoverLinks = this.page.getByRole("dialog").last().getByRole("link"); + await button.click(); + await expect(button).toHaveAttribute("aria-expanded", "true", { timeout: 5000 }); + const popoverLinks = this.page.getByRole("dialog").last().getByRole("link"); - await expect(popoverLinks).toHaveCount(count); - await button.click(); - await expect(button).toHaveAttribute("aria-expanded", "false", { timeout: 5000 }); - } + await expect(popoverLinks).toHaveCount(count); + await button.click(); + await expect(button).toHaveAttribute("aria-expanded", "false", { timeout: 5000 }); } } diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/AssetListPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/AssetListPage.ts index dd6c30c753b2c..2b9ab8ee6e054 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/AssetListPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/AssetListPage.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import type { Locator, Page } from "@playwright/test"; +import { expect, type Locator, type Page } from "@playwright/test"; import { BasePage } from "./BasePage"; @@ -42,14 +42,6 @@ export class AssetListPage extends BasePage { this.emptyState = page.getByText(/no items/i); } - public async assetCount(): Promise { - return this.rows.count(); - } - - public async assetNames(): Promise> { - return this.rows.locator("td a").allTextContents(); - } - public async navigate(): Promise { await this.navigateTo("/assets"); } @@ -80,22 +72,6 @@ export class AssetListPage extends BasePage { } private async waitForTableData(): Promise { - // Wait for actual data links to appear (not skeleton loaders) - await this.page.waitForFunction( - () => { - const table = document.querySelector('[data-testid="table-list"]'); - - if (!table) { - return false; - } - - // Check for actual links in tbody (real data, not skeleton) - const links = table.querySelectorAll("tbody tr td a"); - - return links.length > 0; - }, - undefined, - { timeout: 30_000 }, - ); + await expect(this.rows.locator("td a").first()).toBeVisible({ timeout: 30_000 }); } } diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts index 288c3d40b58e9..56fdbd9f90dbe 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts @@ -94,7 +94,7 @@ export class BackfillPage extends BasePage { super(page); this.triggerButton = page.getByTestId("trigger-dag-button"); // Chakra UI radio cards: target the label directly since is hidden. - this.backfillModeRadio = page.locator('label:has-text("Backfill")'); + this.backfillModeRadio = page.locator("label").getByText("Backfill", { exact: true }); this.backfillFromDateInput = page.getByTestId("datetime-input").first(); this.backfillToDateInput = page.getByTestId("datetime-input").nth(1); this.backfillRunButton = page.getByRole("button", { name: "Run Backfill" }); @@ -374,10 +374,6 @@ export class BackfillPage extends BasePage { return this.page.getByRole("button", { name: /filter table columns/i }); } - public async getTableColumnCount(): Promise { - return this.backfillsTable.locator("thead th").count(); - } - public async navigateToBackfillsTab(dagName: string): Promise { await this.navigateTo(BackfillPage.getBackfillsUrl(dagName)); await expect(this.backfillsTable).toBeVisible({ timeout: 15_000 }); @@ -400,31 +396,50 @@ export class BackfillPage extends BasePage { } public async pauseBackfillViaApi(backfillId: number): Promise { - // Retry: the server may not have fully initialized the backfill yet. - for (let attempt = 0; attempt < 5; attempt++) { - const response = await this.page.request.put(`${baseUrl}/api/v2/backfills/${backfillId}/pause`, { - timeout: 30_000, - }); + let isPaused = false; + + try { + // Retry: the server may not have fully initialized the backfill yet. + await expect + .poll( + async () => { + const response = await this.page.request.put(`${baseUrl}/api/v2/backfills/${backfillId}/pause`, { + timeout: 30_000, + }); - if (response.ok()) { - return true; - } + if (response.ok()) { + isPaused = true; - // 409 means the backfill already completed — not retriable. - if (response.status() === 409) { - return false; - } + return true; + } + + // 409 means the backfill already completed — not retriable. + if (response.status() === 409) { + isPaused = false; - await this.page.waitForTimeout(2000); + return true; + } + + return false; + }, + { + intervals: [2000], + message: `Failed to pause backfill ${backfillId}`, + timeout: 10_000, + }, + ) + .toBeTruthy(); + } catch { + return false; } - return false; + return isPaused; } public async selectReprocessBehavior(behavior: ReprocessBehaviorApi): Promise { const label = REPROCESS_API_TO_UI[behavior]; - await this.page.locator(`label:has-text("${label}")`).first().click({ timeout: 5000 }); + await this.page.locator("label").getByText(label, { exact: true }).click({ timeout: 5000 }); } public async toggleColumn(columnName: string): Promise { diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts index 31a1dfe7f9481..b07b2ac69a111 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts @@ -42,6 +42,8 @@ export class ConnectionsPage extends BasePage { public readonly connectionForm: Locator; public readonly connectionIdHeader: Locator; public readonly connectionIdInput: Locator; + // All table body rows (for web-first assertions in specs) + public readonly connectionRows: Locator; // Core page elements public readonly connectionsTable: Locator; public readonly connectionTypeHeader: Locator; @@ -51,12 +53,12 @@ export class ConnectionsPage extends BasePage { public readonly hostHeader: Locator; public readonly hostInput: Locator; public readonly loginInput: Locator; - public readonly passwordInput: Locator; + public readonly passwordInput: Locator; public readonly portInput: Locator; public readonly rowsPerPageSelect: Locator; - public readonly saveButton: Locator; + public readonly saveButton: Locator; public readonly schemaInput: Locator; public readonly searchInput: Locator; public readonly successAlert: Locator; @@ -72,7 +74,7 @@ export class ConnectionsPage extends BasePage { // Action buttons this.addButton = page.getByRole("button", { name: "Add Connection" }); - this.testConnectionButton = page.locator('button:has-text("Test")'); + this.testConnectionButton = page.getByRole("button", { name: "Test" }); this.saveButton = page.getByRole("button", { name: /^save$/i }); // Form inputs (Chakra UI inputs) @@ -91,15 +93,23 @@ export class ConnectionsPage extends BasePage { this.successAlert = page.locator('[data-scope="toast"][data-part="root"]'); // Delete confirmation dialog - this.confirmDeleteButton = page.locator('button:has-text("Delete")').first(); + this.confirmDeleteButton = page.getByRole("button", { name: "Delete" }).first(); this.rowsPerPageSelect = page.locator("select"); // Sorting and filtering this.tableHeader = page.locator('[role="columnheader"]').first(); - this.connectionIdHeader = page.locator("th:has-text('Connection ID')").first(); - this.connectionTypeHeader = page.locator('th:has-text("Connection Type")').first(); - this.hostHeader = page.locator('th:has-text("Host")').first(); + this.connectionIdHeader = page + .locator("th, [role='columnheader']") + .filter({ hasText: "Connection ID" }) + .first(); + this.connectionTypeHeader = page + .locator("th, [role='columnheader']") + .filter({ hasText: "Connection Type" }) + .first(); + this.hostHeader = page.locator("th, [role='columnheader']").filter({ hasText: "Host" }).first(); this.searchInput = page.locator('input[placeholder*="Search"], input[placeholder*="search"]').first(); + // All table body rows (used by connectionRows for web-first assertions) + this.connectionRows = page.locator("tbody tr"); } // Click the Add button to create a new connection @@ -113,6 +123,13 @@ export class ConnectionsPage extends BasePage { // Click edit button for a specific connection public async clickEditButton(connectionId: string): Promise { + // Wait for any stale dialog backdrop to clear before interacting + const backdrop = this.page.locator('[data-scope="dialog"][data-part="backdrop"]'); + + if (await backdrop.isVisible({ timeout: 1000 }).catch(() => false)) { + await expect(backdrop).toBeHidden({ timeout: 5000 }); + } + const row = await this.findConnectionRow(connectionId); if (!row) { @@ -128,6 +145,7 @@ export class ConnectionsPage extends BasePage { } // Check if a connection exists in the current view + public async connectionExists(connectionId: string): Promise { const emptyState = await this.page .locator("text=No connection found!") @@ -160,18 +178,6 @@ export class ConnectionsPage extends BasePage { throw new Error(`Connection ${connectionId} not found`); } - // Find delete button in the row - await this.page.evaluate(() => { - const backdrops = document.querySelectorAll('[data-scope="dialog"][data-part="backdrop"]'); - - backdrops.forEach((backdrop) => { - const { state } = backdrop.dataset; - - if (state === "closed") { - backdrop.remove(); - } - }); - }); const deleteButton = row.getByRole("button", { name: "Delete Connection" }); await expect(deleteButton).toBeVisible({ timeout: 10_000 }); @@ -259,7 +265,7 @@ export class ConnectionsPage extends BasePage { } if (details.extra !== undefined && details.extra !== "") { - const extraAccordion = this.page.locator('button:has-text("Extra Fields JSON")').first(); + const extraAccordion = this.page.getByRole("button", { name: "Extra Fields JSON" }).first(); const accordionVisible = await extraAccordion.isVisible({ timeout: 5000 }).catch(() => false); if (accordionVisible) { @@ -309,13 +315,10 @@ export class ConnectionsPage extends BasePage { await expect .poll( async () => { - const count1 = await this.page.locator("tbody tr").count(); + const count = await this.page.locator("tbody tr").count(); - await this.page.evaluate(() => new Promise((r) => setTimeout(r, 200))); - const count2 = await this.page.locator("tbody tr").count(); - - if (count1 === count2 && count1 > 0) { - stableRowCount = count1; + if (count > 0) { + stableRowCount = count; return true; } @@ -362,6 +365,11 @@ export class ConnectionsPage extends BasePage { return connectionIds; } + // Returns a locator for a specific connection row (for web-first assertions in specs) + public getConnectionRow(connectionId: string): Locator { + return this.page.locator("tbody tr").filter({ hasText: connectionId }).first(); + } + // Navigate to Connections list page public async navigate(): Promise { await this.navigateTo(ConnectionsPage.connectionsListUrl); @@ -385,41 +393,14 @@ export class ConnectionsPage extends BasePage { public async searchConnections(searchTerm: string): Promise { await (searchTerm === "" ? this.searchInput.clear() : this.searchInput.fill(searchTerm)); - // Wait for search to complete by checking results stability - await expect - .poll( - async () => { - const ids = await this.getConnectionIds(); - - // If we expect no results - const isEmptyVisible = await this.emptyState.isVisible().catch(() => false); - - if (isEmptyVisible) { - return ids.length === 0; - } - - // If we expect results, verify they match the search term - if (ids.length === 0) { - return false; // Still loading - } - - if (searchTerm === "") { - // Get count twice to ensure it's stable - const count1 = ids.length; - - await this.page.evaluate(() => new Promise((r) => setTimeout(r, 200))); - const count2 = await this.getConnectionIds().then((allIds) => allIds.length); - - // Stable when count doesn't change - return count1 === count2 && count1 > 0; - } + // Wait until either rows appear or empty state shows + await expect(this.connectionRows.first().or(this.emptyState)).toBeVisible({ + timeout: 10_000, + }); - // All visible IDs should contain the search term (case-insensitive) - return ids.every((id) => id.toLowerCase().includes(searchTerm.toLowerCase())); - }, - { message: "Search results did not match search term", timeout: 20_000 }, - ) - .toBeTruthy(); + if (searchTerm !== "") { + await expect(this.connectionRows).toContainText(new RegExp(searchTerm, "i")); + } } // Verify connection details are displayed in the list @@ -430,15 +411,13 @@ export class ConnectionsPage extends BasePage { throw new Error(`Connection ${connectionId} not found in list`); } - const rowText = await row.textContent(); - - expect(rowText).toContain(connectionId); - expect(rowText).toContain(expectedType); + await expect(row).toContainText(connectionId); + await expect(row).toContainText(expectedType); } private async findConnectionRow(connectionId: string): Promise { // Try search first (faster) - const hasSearch = await this.searchInput.isVisible({ timeout: 500 }).catch(() => false); + const hasSearch = await this.searchInput.isVisible({ timeout: 3000 }).catch(() => false); if (hasSearch) { return await this.findConnectionRowUsingSearch(connectionId); @@ -478,35 +457,10 @@ export class ConnectionsPage extends BasePage { // Wait for either table or empty state await expect(table.or(this.emptyState)).toBeVisible({ timeout: 10_000 }); - // If table exists, wait for rows if (await table.isVisible().catch(() => false)) { - await this.page - .locator("tbody tr") - .first() - .waitFor({ state: "visible", timeout: 10_000 }) - .catch(() => { - // No rows found - }); - - // Wait for row count to stabilize - await expect - .poll( - async () => { - const count1 = await this.page.locator("tbody tr").count(); - - if (count1 === 0) return true; - - await this.page.evaluate(() => new Promise((r) => setTimeout(r, 300))); - const count2 = await this.page.locator("tbody tr").count(); - - return count1 === count2; - }, - { timeout: 15_000 }, - ) - .toBeTruthy() - .catch(() => { - // Timeout - proceed anyway - }); + await expect(this.connectionRows.first()).toBeVisible({ + timeout: 10_000, + }); } } } diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagCodePage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/DagCodePage.ts index 7895b4ceb78f4..af9e91ef0eba4 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagCodePage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagCodePage.ts @@ -24,6 +24,7 @@ export class DagCodePage extends BasePage { public readonly editorScrollable: Locator; public readonly lineNumbers: Locator; public readonly syntaxTokens: Locator; + public readonly viewLines: Locator; public constructor(page: Page) { super(page); @@ -31,12 +32,14 @@ export class DagCodePage extends BasePage { this.lineNumbers = page.locator(".monaco-editor .line-numbers"); this.editorScrollable = page.locator(".monaco-scrollable-element"); this.syntaxTokens = page.locator(".monaco-editor .view-line span span"); + this.viewLines = page.locator(".monaco-editor .view-line"); } public async navigateToCodeTab(dagId: string): Promise { await this.navigateTo(`/dags/${dagId}/code`); await this.waitForCodeReady(); } + public async verifyCodeIsScrollable(): Promise { await this.waitForCodeReady(); @@ -44,59 +47,38 @@ export class DagCodePage extends BasePage { await expect(scrollable).toBeVisible({ timeout: 30_000 }); - // For a sufficiently long file the scroll-height exceeds the client-height - const isScrollable = await scrollable.evaluate((el) => el.scrollHeight > el.clientHeight); - - expect(isScrollable).toBe(true); + await expect + .poll(async () => scrollable.evaluate((el) => el.scrollHeight > el.clientHeight), { + timeout: 10_000, + }) + .toBe(true); } public async verifyLineNumbersDisplayed(): Promise { await this.waitForCodeReady(); await expect(this.lineNumbers.first()).toBeVisible({ timeout: 30_000 }); - - const lineNumberCount = await this.lineNumbers.count(); - - expect(lineNumberCount).toBeGreaterThan(0); - - const firstLineText = await this.lineNumbers.first().textContent(); - - expect(firstLineText?.trim()).toBe("1"); + await expect(this.lineNumbers).not.toHaveCount(0); + await expect(this.lineNumbers.first()).toHaveText("1"); } public async verifySourceCodeDisplayed(): Promise { await this.waitForCodeReady(); - const viewLines = this.page.locator(".monaco-editor .view-line"); - - await expect(viewLines.first()).toBeVisible({ timeout: 30_000 }); - - const lineCount = await viewLines.count(); - - expect(lineCount).toBeGreaterThan(0); + await expect(this.viewLines.first()).toBeVisible({ timeout: 30_000 }); + await expect(this.viewLines).not.toHaveCount(0); } public async verifySyntaxHighlighting(): Promise { await this.waitForCodeReady(); - const tokens = this.syntaxTokens; - - await expect(tokens.first()).toBeVisible({ timeout: 30_000 }); - - const tokenCount = await tokens.count(); - - expect(tokenCount).toBeGreaterThan(1); - - const classNames = await tokens.first().getAttribute("class"); - - expect(classNames).toMatch(/mtk\d+/); + await expect(this.syntaxTokens.first()).toBeVisible({ timeout: 30_000 }); + await expect(this.syntaxTokens).not.toHaveCount(0); + await expect(this.syntaxTokens.first()).toHaveAttribute("class", /mtk\d+/); } private async waitForCodeReady(): Promise { - await this.editorContainer.waitFor({ state: "visible", timeout: 60_000 }); - - const viewLines = this.page.locator(".monaco-editor .view-line"); - - await viewLines.first().waitFor({ state: "visible", timeout: 30_000 }); + await expect(this.editorContainer).toBeVisible({ timeout: 60_000 }); + await expect(this.viewLines.first()).toBeVisible({ timeout: 30_000 }); } } diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsTabPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsTabPage.ts index f89130f0a204e..61360f923e524 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsTabPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsTabPage.ts @@ -27,27 +27,31 @@ export class DagRunsTabPage extends BasePage { public constructor(page: Page) { super(page); - this.markRunAsButton = page.locator('[data-testid="mark-run-as-button"]').first(); - this.runsTable = page.locator('[data-testid="table-list"]'); - this.tableRows = this.runsTable.locator("tbody tr"); - this.triggerButton = page.locator('[data-testid="trigger-dag-button"]'); + this.markRunAsButton = page.getByTestId("mark-run-as-button").first(); + this.runsTable = page.getByTestId("table-list"); + this.tableRows = this.runsTable.locator("tbody").getByRole("row"); + this.triggerButton = page.getByTestId("trigger-dag-button"); + } + + private static escapeRegExp(value: string): string { + return value.replaceAll(/[$()*+.?[\\\]^{|}]/g, "\\$&"); } public async clickRunAndVerifyDetails(): Promise { - const firstRunLink = this.tableRows.first().locator("a[href*='/runs/']").first(); + const firstRunLink = this.tableRows.first().getByRole("link").first(); await expect(firstRunLink).toBeVisible({ timeout: 10_000 }); await firstRunLink.click(); - await this.page.waitForURL(/.*\/dags\/.*\/runs\/[^/]+$/, { timeout: 15_000 }); + await expect(this.page).toHaveURL(/.*\/dags\/.*\/runs\/[^/]+$/, { timeout: 15_000 }); await expect(this.markRunAsButton).toBeVisible({ timeout: 10_000 }); } public async clickRunsTab(): Promise { - const runsTab = this.page.locator('a[href$="/runs"]'); + const runsTab = this.page.getByRole("link", { exact: true, name: "Runs" }); await expect(runsTab).toBeVisible({ timeout: 10_000 }); await runsTab.click(); - await this.page.waitForURL(/.*\/dags\/[^/]+\/runs/, { timeout: 15_000 }); + await expect(this.page).toHaveURL(/.*\/dags\/[^/]+\/runs/, { timeout: 15_000 }); await this.waitForRunsTableToLoad(); } @@ -56,12 +60,12 @@ export class DagRunsTabPage extends BasePage { currentUrl.searchParams.set("state", state.toLowerCase()); await this.navigateTo(currentUrl.pathname + currentUrl.search); - await this.page.waitForURL(/.*state=.*/, { timeout: 15_000 }); + await expect(this.page).toHaveURL(/.*state=.*/, { timeout: 15_000 }); await this.waitForRunsTableToLoad(); } public async markRunAs(state: "failed" | "success"): Promise { - const stateBadge = this.page.locator('[data-testid="state-badge"]').first(); + const stateBadge = this.page.getByTestId("state-badge").first(); await expect(stateBadge).toBeVisible({ timeout: 10_000 }); const currentState = await stateBadge.textContent(); @@ -73,7 +77,7 @@ export class DagRunsTabPage extends BasePage { await expect(this.markRunAsButton).toBeVisible({ timeout: 10_000 }); await this.markRunAsButton.click(); - const stateOption = this.page.locator(`[data-testid="mark-run-as-${state}"]`); + const stateOption = this.page.getByTestId(`mark-run-as-${state}`); await expect(stateOption).toBeVisible({ timeout: 5000 }); await stateOption.click(); @@ -95,13 +99,18 @@ export class DagRunsTabPage extends BasePage { public async navigateToDag(dagId: string): Promise { await this.navigateTo(`/dags/${dagId}`); - await this.page.waitForURL(`**/dags/${dagId}**`, { timeout: 15_000 }); + await expect(this.page).toHaveURL(new RegExp(`/dags/${DagRunsTabPage.escapeRegExp(dagId)}`), { + timeout: 15_000, + }); await expect(this.triggerButton).toBeVisible({ timeout: 10_000 }); } public async navigateToRunDetails(dagId: string, runId: string): Promise { await this.navigateTo(`/dags/${dagId}/runs/${runId}`); - await this.page.waitForURL(`**/dags/${dagId}/runs/${runId}**`, { timeout: 15_000 }); + await expect(this.page).toHaveURL( + new RegExp(`/dags/${DagRunsTabPage.escapeRegExp(dagId)}/runs/${DagRunsTabPage.escapeRegExp(runId)}`), + { timeout: 15_000 }, + ); await expect(this.markRunAsButton).toBeVisible({ timeout: 15_000 }); } @@ -110,7 +119,7 @@ export class DagRunsTabPage extends BasePage { currentUrl.searchParams.set("run_id_pattern", pattern); await this.navigateTo(currentUrl.pathname + currentUrl.search); - await this.page.waitForURL(/.*run_id_pattern=.*/, { timeout: 15_000 }); + await expect(this.page).toHaveURL(/.*run_id_pattern=.*/, { timeout: 15_000 }); await this.waitForRunsTableToLoad(); } @@ -156,7 +165,7 @@ export class DagRunsTabPage extends BasePage { const rowCount = await rows.count(); for (let i = 0; i < Math.min(rowCount, 5); i++) { - const stateBadge = rows.nth(i).locator('[data-testid="state-badge"]'); + const stateBadge = rows.nth(i).getByTestId("state-badge"); await expect(stateBadge).toBeVisible(); await expect(stateBadge).toContainText(expectedState, { ignoreCase: true }); @@ -168,12 +177,12 @@ export class DagRunsTabPage extends BasePage { await expect(firstRow).toBeVisible({ timeout: 10_000 }); - const runIdLink = firstRow.locator("a[href*='/runs/']").first(); + const runIdLink = firstRow.getByRole("link").first(); await expect(runIdLink).toBeVisible(); await expect(runIdLink).not.toBeEmpty(); - const stateBadge = firstRow.locator('[data-testid="state-badge"]'); + const stateBadge = firstRow.getByTestId("state-badge"); await expect(stateBadge).toBeVisible(); @@ -183,10 +192,10 @@ export class DagRunsTabPage extends BasePage { } public async verifyRunsExist(): Promise { - const runLinks = this.runsTable.locator("a[href*='/runs/']"); + const firstRow = this.tableRows.first(); - await expect(runLinks.first()).toBeVisible({ timeout: 30_000 }); - await expect(runLinks).not.toHaveCount(0); + await expect(firstRow).toBeVisible({ timeout: 30_000 }); + await expect(this.tableRows).not.toHaveCount(0); } public async verifySearchResults(pattern: string): Promise { @@ -199,7 +208,7 @@ export class DagRunsTabPage extends BasePage { const count = await rows.count(); for (let i = 0; i < Math.min(count, 5); i++) { - const runIdLink = rows.nth(i).locator("a[href*='/runs/']").first(); + const runIdLink = rows.nth(i).getByRole("link").first(); await expect(runIdLink).toContainText(pattern, { ignoreCase: true }); } @@ -208,9 +217,9 @@ export class DagRunsTabPage extends BasePage { public async waitForRunsTableToLoad(): Promise { await expect(this.runsTable).toBeVisible({ timeout: 10_000 }); - const dataLink = this.runsTable.locator("a[href*='/runs/']").first(); + const firstRow = this.tableRows.first(); const noDataMessage = this.page.getByText(/no.*dag.*runs.*found/i); - await expect(dataLink.or(noDataMessage)).toBeVisible({ timeout: 30_000 }); + await expect(firstRow.or(noDataMessage)).toBeVisible({ timeout: 30_000 }); } } diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/HomePage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/HomePage.ts index 4471bf4ddc2a6..672a6fab612ef 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/HomePage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/HomePage.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import type { Locator, Page } from "@playwright/test"; +import { expect, type Locator, type Page } from "@playwright/test"; import { BasePage } from "tests/e2e/pages/BasePage"; /** @@ -57,7 +57,7 @@ export class HomePage extends BasePage { this.failedDagsCard = page.locator('a[href*="last_dag_run_state=failed"]'); this.runningDagsCard = page.locator('a[href*="last_dag_run_state=running"]'); this.activeDagsCard = page.locator('a[href*="paused=false"]'); - this.dagImportErrorsCard = page.locator('button:has-text("DAG Import Errors")'); + this.dagImportErrorsCard = page.getByRole("button", { name: "DAG Import Errors" }); // Stats section - using role-based selector this.statsSection = page.getByRole("heading", { name: "Stats" }).locator(".."); @@ -120,7 +120,7 @@ export class HomePage extends BasePage { * Wait for dashboard to fully load */ public async waitForDashboardLoad(): Promise { - await this.welcomeHeading.waitFor({ state: "visible", timeout: 30_000 }); + await expect(this.welcomeHeading).toBeVisible({ timeout: 30_000 }); } /** @@ -128,7 +128,7 @@ export class HomePage extends BasePage { */ // eslint-disable-next-line @typescript-eslint/class-methods-use-this private async getStatsCardCount(card: Locator): Promise { - await card.waitFor({ state: "visible" }); // Fail fast if card doesn't exist + await expect(card).toBeVisible(); const badgeText = await card.locator("span").first().textContent(); const match = badgeText?.match(/\d+/); diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/asset.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/asset.spec.ts index be99eebab23a0..59046947c57bf 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/asset.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/asset.spec.ts @@ -64,17 +64,11 @@ test.describe("Assets Page", () => { }); test("verify asset rows when data exists", async () => { - const count = await assets.assetCount(); - - expect(count).toBeGreaterThanOrEqual(0); + await expect(assets.rows.first()).toBeVisible(); }); test("verify asset has a visible name link", async () => { - const names = await assets.assetNames(); - - for (const name of names) { - expect(name.trim().length).toBeGreaterThan(0); - } + await expect(assets.rows.locator("td a").first()).toBeVisible(); }); test("verify clicking an asset navigates to detail page", async ({ page }) => { @@ -85,9 +79,7 @@ test.describe("Assets Page", () => { }); test("verify assets using search", async () => { - const initialCount = await assets.assetCount(); - - expect(initialCount).toBeGreaterThan(0); + await expect(assets.rows.first()).toBeVisible(); const searchTerm = testConfig.asset.name; @@ -107,14 +99,6 @@ test.describe("Assets Page", () => { { intervals: [500], timeout: 30_000 }, ) .toBe(true); - - const names = await assets.assetNames(); - - expect(names.length).toBeGreaterThan(0); - - for (const name of names) { - expect(name.toLowerCase()).toContain(searchTerm.toLowerCase()); - } }); test("verify asset details and dependencies", async ({ page }) => { @@ -127,8 +111,8 @@ test.describe("Assets Page", () => { await assetDetailPage.verifyAssetDetails(assetName); - await assetDetailPage.verifyProducingTasks(1); + await assetDetailPage.verifyProducingTasks(); - await assetDetailPage.verifyScheduledDags(1); + await assetDetailPage.verifyScheduledDags(); }); }); diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts index 8babe2f951327..b97d31d8b4684 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts @@ -109,17 +109,18 @@ test.describe("Backfill", () => { await backfillPage.navigateToBackfillsTab(testDagId); - const initialColumnCount = await backfillPage.getTableColumnCount(); + const tableHeaders = backfillPage.backfillsTable.locator("thead th"); + + await expect(tableHeaders).toHaveCount(7); // Initial state should have 7 columns + const initialColumnCount = await tableHeaders.count(); - expect(initialColumnCount).toBeGreaterThan(0); await expect(backfillPage.getFilterButton()).toBeVisible(); await backfillPage.openFilterMenu(); const filterMenuItems = page.getByRole("menuitem"); - const filterMenuCount = await filterMenuItems.count(); - expect(filterMenuCount).toBeGreaterThan(0); + await expect(filterMenuItems).not.toHaveCount(0); const firstMenuItem = filterMenuItems.first(); const columnToToggle = (await firstMenuItem.textContent())?.trim() ?? ""; @@ -131,9 +132,7 @@ test.describe("Backfill", () => { await expect(backfillPage.getColumnHeader(columnToToggle)).not.toBeVisible(); - const newColumnCount = await backfillPage.getTableColumnCount(); - - expect(newColumnCount).toBeLessThan(initialColumnCount); + await expect(tableHeaders).toHaveCount(initialColumnCount - 1); await backfillPage.openFilterMenu(); await backfillPage.toggleColumn(columnToToggle); @@ -141,9 +140,7 @@ test.describe("Backfill", () => { await expect(backfillPage.getColumnHeader(columnToToggle)).toBeVisible(); - const finalColumnCount = await backfillPage.getTableColumnCount(); - - expect(finalColumnCount).toBe(initialColumnCount); + await expect(tableHeaders).toHaveCount(initialColumnCount); }); }); diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/connections.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/connections.spec.ts index 9d2493f8aab96..b2a99f3ba54d1 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/connections.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/connections.spec.ts @@ -61,7 +61,7 @@ test.describe("Connections Page - List and Display", () => { await connectionsPage.navigate(); // Verify the page is loaded - expect(connectionsPage.page.url()).toContain("/connections"); + await expect(connectionsPage.page).toHaveURL(/\/connections/); // Verify table or list is visible await expect(connectionsPage.connectionsTable).toBeVisible(); @@ -71,9 +71,7 @@ test.describe("Connections Page - List and Display", () => { await connectionsPage.navigate(); // Check that we have at least one row - const count = await connectionsPage.getConnectionCount(); - - expect(count).toBeGreaterThan(0); + await expect(connectionsPage.connectionRows).not.toHaveCount(0); // Verify column headers exist await expect(connectionsPage.connectionIdHeader).toBeVisible(); @@ -166,10 +164,8 @@ test.describe("Connections Page - CRUD Operations", () => { // Create connection via UI await connectionsPage.createConnection(newConnection); - const exists = await connectionsPage.connectionExists(newConnection.connection_id); - expect(exists).toBeTruthy(); - // Verify it appears in the list with correct type + // Verify it appears in the list with correct type (web-first assertion) await connectionsPage.verifyConnectionInList(newConnection.connection_id, newConnection.conn_type); }); @@ -177,18 +173,14 @@ test.describe("Connections Page - CRUD Operations", () => { test.setTimeout(120_000); await connectionsPage.navigate(); - // Verify connection exists before editing (created in beforeAll) - const exists = await connectionsPage.connectionExists(existingConnection.connection_id); - - expect(exists).toBeTruthy(); + // Verify connection exists before editing (web-first assertion) + await expect(connectionsPage.getConnectionRow(existingConnection.connection_id)).toBeVisible(); // Edit the connection await connectionsPage.editConnection(existingConnection.connection_id, updatedConnection); - // Verify the connection still exists after editing - const stillExists = await connectionsPage.connectionExists(existingConnection.connection_id); - - expect(stillExists).toBeTruthy(); + // Verify the connection still exists after editing (web-first assertion) + await expect(connectionsPage.getConnectionRow(existingConnection.connection_id)).toBeVisible(); }); test("should delete a connection", async () => { @@ -206,16 +198,14 @@ test.describe("Connections Page - CRUD Operations", () => { await connectionsPage.navigate(); await connectionsPage.createConnection(tempConnection); - const exists = await connectionsPage.connectionExists(tempConnection.connection_id); - - expect(exists).toBeTruthy(); + // Verify it exists before deleting (web-first assertion) + await expect(connectionsPage.getConnectionRow(tempConnection.connection_id)).toBeVisible(); // Delete the connection await connectionsPage.deleteConnection(tempConnection.connection_id); - const stillExists = await connectionsPage.connectionExists(tempConnection.connection_id); - - expect(stillExists).toBeFalsy(); + // Verify it is gone (web-first assertion) + await expect(connectionsPage.getConnectionRow(tempConnection.connection_id)).not.toBeVisible(); }); }); @@ -281,29 +271,17 @@ test.describe("Connections Page - Search and Filter", () => { test("should filter connections by search term", async () => { await connectionsPage.navigate(); - const initialCount = await connectionsPage.getConnectionCount(); - - expect(initialCount).toBeGreaterThan(0); + // Check that we have at least one row before searching (web-first assertion) + await expect(connectionsPage.connectionRows).not.toHaveCount(0); const searchTerm = "production"; await connectionsPage.searchConnections(searchTerm); - await expect - .poll( - async () => { - const ids = await connectionsPage.getConnectionIds(); - - // Verify we have results AND they match the search term - return ids.length > 0 && ids.every((id) => id.toLowerCase().includes(searchTerm.toLowerCase())); - }, - { intervals: [500], timeout: 10_000 }, - ) - .toBe(true); - + // Verify filtered results contain the search term + await expect(connectionsPage.connectionRows).not.toHaveCount(0); const filteredIds = await connectionsPage.getConnectionIds(); - expect(filteredIds.length).toBeGreaterThan(0); for (const id of filteredIds) { expect(id.toLowerCase()).toContain(searchTerm.toLowerCase()); } @@ -313,27 +291,17 @@ test.describe("Connections Page - Search and Filter", () => { test.setTimeout(120_000); await connectionsPage.navigate(); + // Verify rows exist before searching (web-first assertion) + await expect(connectionsPage.connectionRows).not.toHaveCount(0); const initialCount = await connectionsPage.getConnectionCount(); - expect(initialCount).toBeGreaterThan(0); - - // Search for something + // Search for something and wait for results await connectionsPage.searchConnections("production"); + await expect(connectionsPage.connectionRows).not.toHaveCount(0); - // Wait for search results - await expect - .poll( - async () => { - const count = await connectionsPage.getConnectionCount(); - - return count > 0; // Just verify we have some results - }, - { intervals: [500], timeout: 10_000 }, - ) - .toBe(true); - - // Clear search + // Clear search and verify at least as many rows as before await connectionsPage.searchConnections(""); + await expect(connectionsPage.connectionRows).not.toHaveCount(0); const finalCount = await connectionsPage.getConnectionCount(); diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs-tab.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs-tab.spec.ts index d9aef74f5476b..5c0edc5bd9921 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs-tab.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs-tab.spec.ts @@ -59,7 +59,7 @@ test.describe("DAG Runs Tab", () => { await dagRunsTabPage.navigateToDag(testDagId); await dagRunsTabPage.clickRunsTab(); - await expect(dagRunsTabPage.page).toHaveURL(/\/dags\/.*\/runs/); + await expect(dagRunsTabPage.page).toHaveURL(/.*\/dags\/[^/]+\/runs/); }); test("verify run details display correctly", async () => { diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts index 8ebcff00ea5d6..847a63058ab91 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts @@ -67,17 +67,13 @@ test.describe("Dashboard Metrics Display", () => { await homePage.waitForDashboardLoad(); await homePage.activeDagsCard.click(); - await homePage.page.waitForURL(/paused=false/); - - expect(homePage.page.url()).toContain("paused=false"); + await expect(homePage.page).toHaveURL(/paused=false/); await homePage.navigate(); await homePage.waitForDashboardLoad(); await homePage.runningDagsCard.click(); - await homePage.page.waitForURL(/last_dag_run_state=running/); - - expect(homePage.page.url()).toContain("last_dag_run_state=running"); + await expect(homePage.page).toHaveURL(/last_dag_run_state=running/); }); test("should display welcome heading on dashboard", async () => { diff --git a/airflow-core/src/airflow/ui/vite.config.ts b/airflow-core/src/airflow/ui/vite.config.ts index b622f4eddf226..68d62b04d82ed 100644 --- a/airflow-core/src/airflow/ui/vite.config.ts +++ b/airflow-core/src/airflow/ui/vite.config.ts @@ -44,6 +44,12 @@ export default defineConfig({ resolve: { alias: { openapi: "/openapi-gen", src: "/src" } }, server: { cors: true, // Only used by the dev server. + proxy: { + "/hitl-review": { + changeOrigin: true, + target: "http://localhost:28080", + }, + }, }, test: { coverage: { diff --git a/airflow-core/src/airflow/utils/cli.py b/airflow-core/src/airflow/utils/cli.py index 4df33b26894ff..6af75dd5b5e1c 100644 --- a/airflow-core/src/airflow/utils/cli.py +++ b/airflow-core/src/airflow/utils/cli.py @@ -386,6 +386,14 @@ def setup_logging(filename): return handler.stream +def print_banner() -> None: + """Print the Airflow ASCII art banner, unless JSON logging is enabled.""" + from airflow.configuration import conf + + if not conf.getboolean("logging", "json_logs", fallback=False): + print(settings.HEADER) + + def sigint_handler(sig, frame): """ Return without error on SIGINT or SIGTERM signals in interactive command mode. diff --git a/airflow-core/src/airflow/utils/db.py b/airflow-core/src/airflow/utils/db.py index 71f03ea91cedd..93041bc188d42 100644 --- a/airflow-core/src/airflow/utils/db.py +++ b/airflow-core/src/airflow/utils/db.py @@ -947,8 +947,9 @@ def check_and_run_migrations(): if sys.stdout.isatty() and verb: print() question = f"Please confirm database {verb} (or wait 4 seconds to skip it). Are you sure? [y/N]" + print_fn = log.info if conf.getboolean("logging", "json_logs", fallback=False) else print try: - answer = helpers.prompt_with_timeout(question, timeout=4, default=False) + answer = helpers.prompt_with_timeout(question, timeout=4, default=False, output_fn=print_fn) if answer: try: db_command() @@ -969,10 +970,11 @@ def check_and_run_migrations(): elif source_heads != db_heads: from airflow.version import version - print( - f"ERROR: You need to {verb} the database. Please run `airflow db {command_name}`. " - f"Make sure the command is run using Airflow version {version}.", - file=sys.stderr, + log.error( + "Database migration required. Please run `airflow db %s`. " + "Make sure the command is run using Airflow version %s.", + command_name, + version, ) sys.exit(1) diff --git a/airflow-core/src/airflow/utils/helpers.py b/airflow-core/src/airflow/utils/helpers.py index 50bd8b82a4622..5e2dd1b9dedf2 100644 --- a/airflow-core/src/airflow/utils/helpers.py +++ b/airflow-core/src/airflow/utils/helpers.py @@ -63,12 +63,12 @@ def validate_key(k: str, max_length: int = 250): ) -def ask_yesno(question: str, default: bool | None = None) -> bool: +def ask_yesno(question: str, default: bool | None = None, output_fn=print) -> bool: """Get a yes or no answer from the user.""" yes = {"yes", "y"} no = {"no", "n"} - print(question) + output_fn(question) while True: choice = input().lower() if choice == "" and default is not None: @@ -77,10 +77,10 @@ def ask_yesno(question: str, default: bool | None = None) -> bool: return True if choice in no: return False - print("Please respond with y/yes or n/no.") + output_fn("Please respond with y/yes or n/no.") -def prompt_with_timeout(question: str, timeout: int, default: bool | None = None) -> bool: +def prompt_with_timeout(question: str, timeout: int, default: bool | None = None, output_fn=print) -> bool: """Ask the user a question and timeout if they don't respond.""" def handler(signum, frame): @@ -89,7 +89,7 @@ def handler(signum, frame): signal.signal(signal.SIGALRM, handler) signal.alarm(timeout) try: - return ask_yesno(question, default) + return ask_yesno(question, default, output_fn=output_fn) finally: signal.alarm(0) diff --git a/airflow-core/src/airflow/utils/serve_logs/core.py b/airflow-core/src/airflow/utils/serve_logs/core.py index c09a9f603f30c..5b4ee5aa68669 100644 --- a/airflow-core/src/airflow/utils/serve_logs/core.py +++ b/airflow-core/src/airflow/utils/serve_logs/core.py @@ -52,9 +52,18 @@ def serve_logs(port=None): # Get uvicorn logging configuration from Airflow settings uvicorn_log_level = conf.get("logging", "uvicorn_logging_level", fallback="info").lower() - # Use uvicorn directly for ASGI applications + # Use uvicorn directly for ASGI applications. + # log_config=None: preserve the process's structlog-based logging setup rather than + # letting uvicorn reset it with its own default formatter. + # access_log=False: the log server serves internal file content; HTTP access logs + # are not needed and would be non-JSON noise when json_logs=True. uvicorn.run( - "airflow.utils.serve_logs.log_server:get_app", host="", port=port, log_level=uvicorn_log_level + "airflow.utils.serve_logs.log_server:get_app", + host="", + port=port, + log_level=uvicorn_log_level, + log_config=None, + access_log=False, ) # Log serving is I/O bound and has low concurrency, so single process is sufficient diff --git a/airflow-core/tests/integration/otel/dags/otel_test_dag.py b/airflow-core/tests/integration/otel/dags/otel_test_dag.py index 6c005a9927ee9..25861c8f622ae 100644 --- a/airflow-core/tests/integration/otel/dags/otel_test_dag.py +++ b/airflow-core/tests/integration/otel/dags/otel_test_dag.py @@ -22,12 +22,12 @@ from opentelemetry import trace from airflow import DAG -from airflow.sdk import chain, task -from airflow.sdk.observability.trace import Trace -from airflow.sdk.observability.traces import otel_tracer +from airflow.sdk import task logger = logging.getLogger("airflow.otel_test_dag") +tracer = trace.get_tracer(__name__) + args = { "owner": "airflow", "start_date": datetime(2024, 9, 1), @@ -36,52 +36,13 @@ @task -def task1(ti): - logger.info("Starting Task_1.") - - context_carrier = ti.context_carrier - - otel_task_tracer = otel_tracer.get_otel_tracer_for_task(Trace) - tracer_provider = otel_task_tracer.get_otel_tracer_provider() - - if context_carrier is not None: - logger.info("Found ti.context_carrier: %s.", str(context_carrier)) - logger.info("Extracting the span context from the context_carrier.") - parent_context = otel_task_tracer.extract(context_carrier) - with otel_task_tracer.start_child_span( - span_name="task1_sub_span1", - parent_context=parent_context, - component="dag", - ) as s1: - s1.set_attribute("attr1", "val1") - logger.info("From task sub_span1.") - - with otel_task_tracer.start_child_span("task1_sub_span2") as s2: - s2.set_attribute("attr2", "val2") - logger.info("From task sub_span2.") +def task1(): + logger.info("starting task1") - tracer = trace.get_tracer("trace_test.tracer", tracer_provider=tracer_provider) - with tracer.start_as_current_span(name="task1_sub_span3") as s3: - s3.set_attribute("attr3", "val3") - logger.info("From task sub_span3.") + with tracer.start_as_current_span("sub_span1") as s1: + s1.set_attribute("attr1", "val1") - with otel_task_tracer.start_child_span( - span_name="task1_sub_span4", - parent_context=parent_context, - component="dag", - ) as s4: - s4.set_attribute("attr4", "val4") - logger.info("From task sub_span4.") - - logger.info("Task_1 finished.") - - -@task -def task2(): - logger.info("Starting Task_2.") - for i in range(3): - logger.info("Task_2, iteration '%d'.", i) - logger.info("Task_2 finished.") + logger.info("task1 finished.") with DAG( @@ -90,4 +51,4 @@ def task2(): schedule=None, catchup=False, ) as dag: - chain(task1(), task2()) # type: ignore + task1() diff --git a/airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_between_tasks.py b/airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_between_tasks.py deleted file mode 100644 index 72fb9148a40e5..0000000000000 --- a/airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_between_tasks.py +++ /dev/null @@ -1,158 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from __future__ import annotations - -import logging -import os -import time -from datetime import datetime - -from opentelemetry import trace -from sqlalchemy import select - -from airflow import DAG -from airflow.models import TaskInstance -from airflow.providers.standard.version_compat import AIRFLOW_V_3_0_PLUS -from airflow.sdk import chain, task -from airflow.sdk.observability.trace import Trace -from airflow.sdk.observability.traces import otel_tracer -from airflow.utils.session import create_session - -logger = logging.getLogger("airflow.otel_test_dag_with_pause") - -args = { - "owner": "airflow", - "start_date": datetime(2024, 9, 2), - "retries": 0, -} - - -@task -def task1(ti): - logger.info("Starting Task_1.") - - context_carrier = ti.context_carrier - - otel_task_tracer = otel_tracer.get_otel_tracer_for_task(Trace) - tracer_provider = otel_task_tracer.get_otel_tracer_provider() - - if context_carrier is not None: - logger.info("Found ti.context_carrier: %s.", context_carrier) - logger.info("Extracting the span context from the context_carrier.") - - # If the task takes too long to execute, then the ti should be read from the db - # to make sure that the initial context_carrier is the same. - # Since Airflow 3, direct db access has been removed entirely. - if not AIRFLOW_V_3_0_PLUS: - with create_session() as session: - session_ti: TaskInstance = session.scalars( - select(TaskInstance).where( - TaskInstance.task_id == ti.task_id, - TaskInstance.run_id == ti.run_id, - ) - ).one() - context_carrier = session_ti.context_carrier - - parent_context = Trace.extract(context_carrier) - with otel_task_tracer.start_child_span( - span_name="task1_sub_span1", - parent_context=parent_context, - component="dag", - ) as s1: - s1.set_attribute("attr1", "val1") - logger.info("From task sub_span1.") - - with otel_task_tracer.start_child_span("task1_sub_span2") as s2: - s2.set_attribute("attr2", "val2") - logger.info("From task sub_span2.") - - tracer = trace.get_tracer("trace_test.tracer", tracer_provider=tracer_provider) - with tracer.start_as_current_span(name="task1_sub_span3") as s3: - s3.set_attribute("attr3", "val3") - logger.info("From task sub_span3.") - - if not AIRFLOW_V_3_0_PLUS: - with create_session() as session: - session_ti: TaskInstance = session.scalars( - select(TaskInstance).where( - TaskInstance.task_id == ti.task_id, - TaskInstance.run_id == ti.run_id, - ) - ).one() - context_carrier = session_ti.context_carrier - parent_context = Trace.extract(context_carrier) - - with otel_task_tracer.start_child_span( - span_name="task1_sub_span4", - parent_context=parent_context, - component="dag", - ) as s4: - s4.set_attribute("attr4", "val4") - logger.info("From task sub_span4.") - - logger.info("Task_1 finished.") - - -@task -def paused_task(): - logger.info("Starting Paused_task.") - - dag_folder = os.path.dirname(os.path.abspath(__file__)) - control_file = os.path.join(dag_folder, "dag_control.txt") - - # Create the file and write 'pause' to it. - with open(control_file, "w") as file: - file.write("pause") - - # Pause execution until the word 'pause' is replaced on the file. - while True: - # If there is an exception, then writing to the file failed. Let it exit. - file_contents = None - with open(control_file) as file: - file_contents = file.read() - - if "pause" in file_contents: - logger.info("Task has been paused.") - time.sleep(1) - continue - logger.info("Resuming task execution.") - # Break the loop and finish with the task execution. - break - - # Cleanup the control file. - if os.path.exists(control_file): - os.remove(control_file) - print("Control file has been cleaned up.") - - logger.info("Paused_task finished.") - - -@task -def task2(): - logger.info("Starting Task_2.") - for i in range(3): - logger.info("Task_2, iteration '%d'.", i) - logger.info("Task_2 finished.") - - -with DAG( - "otel_test_dag_with_pause_between_tasks", - default_args=args, - schedule=None, - catchup=False, -) as dag: - chain(task1(), paused_task(), task2()) # type: ignore diff --git a/airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_in_task.py b/airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_in_task.py deleted file mode 100644 index dfc5c30243f08..0000000000000 --- a/airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_in_task.py +++ /dev/null @@ -1,151 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from __future__ import annotations - -import logging -import os -import time -from datetime import datetime - -from opentelemetry import trace -from sqlalchemy import select - -from airflow import DAG -from airflow.models import TaskInstance -from airflow.providers.standard.version_compat import AIRFLOW_V_3_0_PLUS -from airflow.sdk import chain, task -from airflow.sdk.observability.trace import Trace -from airflow.sdk.observability.traces import otel_tracer -from airflow.utils.session import create_session - -logger = logging.getLogger("airflow.otel_test_dag_with_pause_in_task") - -args = { - "owner": "airflow", - "start_date": datetime(2024, 9, 2), - "retries": 0, -} - - -@task -def task1(ti): - logger.info("Starting Task_1.") - - context_carrier = ti.context_carrier - - dag_folder = os.path.dirname(os.path.abspath(__file__)) - control_file = os.path.join(dag_folder, "dag_control.txt") - - # Create the file and write 'pause' to it. - with open(control_file, "w") as file: - file.write("pause") - - # Pause execution until the word 'pause' is replaced on the file. - while True: - # If there is an exception, then writing to the file failed. Let it exit. - file_contents = None - with open(control_file) as file: - file_contents = file.read() - - if "pause" in file_contents: - logger.info("Task has been paused.") - time.sleep(1) - continue - logger.info("Resuming task execution.") - # Break the loop and finish with the task execution. - break - - otel_task_tracer = otel_tracer.get_otel_tracer_for_task(Trace) - tracer_provider = otel_task_tracer.get_otel_tracer_provider() - - if context_carrier is not None: - logger.info("Found ti.context_carrier: %s.", context_carrier) - logger.info("Extracting the span context from the context_carrier.") - - # If the task takes too long to execute, then the ti should be read from the db - # to make sure that the initial context_carrier is the same. - # Since Airflow 3, direct db access has been removed entirely. - if not AIRFLOW_V_3_0_PLUS: - with create_session() as session: - session_ti: TaskInstance = session.scalars( - select(TaskInstance).where( - TaskInstance.task_id == ti.task_id, - TaskInstance.run_id == ti.run_id, - ) - ).one() - context_carrier = session_ti.context_carrier - - parent_context = Trace.extract(context_carrier) - with otel_task_tracer.start_child_span( - span_name="task1_sub_span1", - parent_context=parent_context, - component="dag", - ) as s1: - s1.set_attribute("attr1", "val1") - logger.info("From task sub_span1.") - - with otel_task_tracer.start_child_span("task1_sub_span2") as s2: - s2.set_attribute("attr2", "val2") - logger.info("From task sub_span2.") - - tracer = trace.get_tracer("trace_test.tracer", tracer_provider=tracer_provider) - with tracer.start_as_current_span(name="task1_sub_span3") as s3: - s3.set_attribute("attr3", "val3") - logger.info("From task sub_span3.") - - if not AIRFLOW_V_3_0_PLUS: - with create_session() as session: - session_ti: TaskInstance = session.scalars( - select(TaskInstance).where( - TaskInstance.task_id == ti.task_id, - TaskInstance.run_id == ti.run_id, - ) - ).one() - context_carrier = session_ti.context_carrier - parent_context = Trace.extract(context_carrier) - - with otel_task_tracer.start_child_span( - span_name="task1_sub_span4", - parent_context=parent_context, - component="dag", - ) as s4: - s4.set_attribute("attr4", "val4") - logger.info("From task sub_span4.") - - # Cleanup the control file. - if os.path.exists(control_file): - os.remove(control_file) - print("Control file has been cleaned up.") - - logger.info("Task_1 finished.") - - -@task -def task2(): - logger.info("Starting Task_2.") - for i in range(3): - logger.info("Task_2, iteration '%d'.", i) - logger.info("Task_2 finished.") - - -with DAG( - "otel_test_dag_with_pause_in_task", - default_args=args, - schedule=None, - catchup=False, -) as dag: - chain(task1(), task2()) # type: ignore diff --git a/airflow-core/tests/integration/otel/test_otel.py b/airflow-core/tests/integration/otel/test_otel.py index 0e4546e301d77..60af1060ce12a 100644 --- a/airflow-core/tests/integration/otel/test_otel.py +++ b/airflow-core/tests/integration/otel/test_otel.py @@ -250,7 +250,7 @@ def serialize_and_get_dags(cls) -> dict[str, SerializedDAG]: dag_bag = DagBag(dag_folder=cls.dag_folder, include_examples=False) dag_ids = dag_bag.dag_ids - assert len(dag_ids) == 3 + assert len(dag_ids) == 1 dag_dict: dict[str, SerializedDAG] = {} with create_session() as session: @@ -317,7 +317,7 @@ def dag_execution_for_testing_metrics(self, capfd): try: # Start the processes here and not as fixtures or in a common setup, # so that the test can capture their output. - scheduler_process, apiserver_process = self.start_worker_and_scheduler() + scheduler_process, apiserver_process = self.start_scheduler() dag_id = "otel_test_dag" @@ -441,7 +441,7 @@ def test_dag_execution_succeeds(self, capfd): try: # Start the processes here and not as fixtures or in a common setup, # so that the test can capture their output. - scheduler_process, apiserver_process = self.start_worker_and_scheduler() + scheduler_process, apiserver_process = self.start_scheduler() dag_id = "otel_test_dag" @@ -486,10 +486,8 @@ def test_dag_execution_succeeds(self, capfd): log.info("out-start --\n%s\n-- out-end", out) log.info("err-start --\n%s\n-- err-end", err) - # host = "host.docker.internal" host = "jaeger" service_name = os.environ.get("OTEL_SERVICE_NAME", "test") - # service_name ``= "my-service-name" r = requests.get(f"http://{host}:16686/api/traces?service={service_name}") data = r.json() @@ -510,16 +508,12 @@ def get_parent_span_id(span): nested = get_span_hierarchy() assert nested == { - "otel_test_dag": None, - "task1": None, - "task1_sub_span1": None, - "task1_sub_span2": None, - "task1_sub_span3": "task1_sub_span2", - "task1_sub_span4": None, - "task2": None, + "sub_span1": "task_run.task1", + "task_run.task1": "dag_run.otel_test_dag", + "dag_run.otel_test_dag": None, } - def start_worker_and_scheduler(self): + def start_scheduler(self): scheduler_process = subprocess.Popen( self.scheduler_command_args, env=os.environ.copy(), diff --git a/airflow-core/tests/unit/always/test_project_structure.py b/airflow-core/tests/unit/always/test_project_structure.py index ffd7aa53c5c71..1169de1bd1754 100644 --- a/airflow-core/tests/unit/always/test_project_structure.py +++ b/airflow-core/tests/unit/always/test_project_structure.py @@ -100,6 +100,7 @@ def test_providers_modules_should_have_tests(self): "providers/cncf/kubernetes/tests/unit/cncf/kubernetes/utils/test_xcom_sidecar.py", "providers/common/sql/tests/unit/common/sql/datafusion/test_base.py", "providers/common/sql/tests/unit/common/sql/datafusion/test_exceptions.py", + "providers/common/ai/tests/unit/common/ai/test_exceptions.py", "providers/common/compat/tests/unit/common/compat/lineage/test_entities.py", "providers/common/compat/tests/unit/common/compat/standard/test_operators.py", "providers/common/compat/tests/unit/common/compat/standard/test_triggers.py", @@ -427,37 +428,6 @@ class TestGoogleProviderProjectStructure(ExampleCoverageTest, AssetsCoverageTest "airflow.providers.google.marketing_platform.operators.GoogleDisplayVideo360UploadLineItemsOperator", "airflow.providers.google.marketing_platform.operators.GoogleDisplayVideo360DownloadLineItemsOperator", "airflow.providers.google.marketing_platform.sensors.GoogleDisplayVideo360RunQuerySensor", - "airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook", - "airflow.providers.google.cloud.links.datacatalog.DataCatalogEntryGroupLink", - "airflow.providers.google.cloud.links.datacatalog.DataCatalogEntryLink", - "airflow.providers.google.cloud.links.datacatalog.DataCatalogTagTemplateLink", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateEntryOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateEntryGroupOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateTagOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateTagTemplateOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateTagTemplateFieldOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteEntryGroupOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteTagOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteTagTemplateOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteTagTemplateFieldOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogGetEntryOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogGetEntryGroupOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogGetTagTemplateOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogListTagsOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogLookupEntryOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogRenameTagTemplateFieldOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogSearchCatalogOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogUpdateEntryOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogUpdateTagOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogUpdateTagTemplateOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateEntryOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogUpdateTagTemplateFieldOperator", - "airflow.providers.google.cloud.operators.vertex_ai.generative_model.GenerateFromCachedContentOperator", - "airflow.providers.google.cloud.operators.vertex_ai.generative_model.CreateCachedContentOperator", - "airflow.providers.google.cloud.operators.vertex_ai.generative_model.CountTokensOperator", - "airflow.providers.google.cloud.operators.vertex_ai.generative_model.SupervisedFineTuningTrainOperator", - "airflow.providers.google.cloud.operators.vertex_ai.generative_model.GenerativeModelGenerateContentOperator", - "airflow.providers.google.cloud.operators.vertex_ai.generative_model.TextEmbeddingModelGetEmbeddingsOperator", } BASE_CLASSES = { @@ -486,8 +456,6 @@ class TestGoogleProviderProjectStructure(ExampleCoverageTest, AssetsCoverageTest "airflow.providers.google.cloud.operators.vertex_ai.auto_ml.AutoMLTrainingJobBaseOperator", "airflow.providers.google.cloud.operators.vertex_ai.endpoint_service.UpdateEndpointOperator", "airflow.providers.google.cloud.operators.vertex_ai.batch_prediction_job.GetBatchPredictionJobOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteEntryOperator", - "airflow.providers.google.cloud.operators.vertex_ai.generative_model.DeleteExperimentRunOperator", } ASSETS_NOT_REQUIRED = { @@ -519,11 +487,6 @@ class TestGoogleProviderProjectStructure(ExampleCoverageTest, AssetsCoverageTest "airflow.providers.google.cloud.operators.cloud_storage_transfer_service." "CloudDataTransferServiceResumeOperationOperator", "airflow.providers.google.cloud.operators.compute.ComputeEngineBaseOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteEntryGroupOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteEntryOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteTagOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteTagTemplateFieldOperator", - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteTagTemplateOperator", "airflow.providers.google.cloud.operators.datafusion.CloudDataFusionDeleteInstanceOperator", "airflow.providers.google.cloud.operators.datafusion.CloudDataFusionDeletePipelineOperator", "airflow.providers.google.cloud.operators.dataproc.DataprocDeleteBatchOperator", diff --git a/airflow-core/tests/unit/always/test_secrets.py b/airflow-core/tests/unit/always/test_secrets.py index dfa39352b92be..d09f04df7dfbd 100644 --- a/airflow-core/tests/unit/always/test_secrets.py +++ b/airflow-core/tests/unit/always/test_secrets.py @@ -225,3 +225,74 @@ def test_backend_variable_order(self, mock_secret_get, mock_meta_get): ) def test_variable_env_var_do_not_access_team_specific(self): assert Variable.get_variable_from_secrets(key="_team___myvar") is None + + +@skip_if_force_lowest_dependencies_marker +class TestSecretBackendKwargEnvVars: + """Test per-key env var overrides for secrets backend kwargs.""" + + def setup_method(self) -> None: + SecretCache.reset() + + @conf_vars( + { + ( + "secrets", + "backend", + ): "airflow.providers.amazon.aws.secrets.systems_manager.SystemsManagerParameterStoreBackend", + } + ) + @mock.patch.dict( + "os.environ", + {"AIRFLOW__SECRETS__BACKEND_KWARG__CONNECTIONS_PREFIX": "/airflow/connections"}, + ) + def test_backend_kwarg_env_vars_basic(self): + """Per-key env var is picked up when no JSON blob is set.""" + backends = initialize_secrets_backends() + systems_manager = next( + b for b in backends if b.__class__.__name__ == "SystemsManagerParameterStoreBackend" + ) + assert systems_manager.connections_prefix == "/airflow/connections" + + @conf_vars( + { + ( + "secrets", + "backend", + ): "airflow.providers.amazon.aws.secrets.systems_manager.SystemsManagerParameterStoreBackend", + ("secrets", "backend_kwargs"): '{"connections_prefix": "/old"}', + } + ) + @mock.patch.dict( + "os.environ", + {"AIRFLOW__SECRETS__BACKEND_KWARG__CONNECTIONS_PREFIX": "/new"}, + ) + def test_backend_kwarg_env_vars_override_json(self): + """Per-key env var overrides the same key in the JSON blob.""" + backends = initialize_secrets_backends() + systems_manager = next( + b for b in backends if b.__class__.__name__ == "SystemsManagerParameterStoreBackend" + ) + assert systems_manager.connections_prefix == "/new" + + @conf_vars( + { + ( + "secrets", + "backend", + ): "airflow.providers.amazon.aws.secrets.systems_manager.SystemsManagerParameterStoreBackend", + ("secrets", "backend_kwargs"): '{"connections_prefix": "/airflow"}', + } + ) + @mock.patch.dict( + "os.environ", + {"AIRFLOW__SECRETS__BACKEND_KWARG__VARIABLES_PREFIX": "/airflow/variables"}, + ) + def test_backend_kwarg_env_vars_merge_with_json(self): + """Per-key env var is merged with (not replacing) the JSON blob.""" + backends = initialize_secrets_backends() + systems_manager = next( + b for b in backends if b.__class__.__name__ == "SystemsManagerParameterStoreBackend" + ) + assert systems_manager.connections_prefix == "/airflow" + assert systems_manager.variables_prefix == "/airflow/variables" diff --git a/airflow-core/tests/unit/always/test_secrets_metastore.py b/airflow-core/tests/unit/always/test_secrets_metastore.py new file mode 100644 index 0000000000000..d13419563f701 --- /dev/null +++ b/airflow-core/tests/unit/always/test_secrets_metastore.py @@ -0,0 +1,110 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import pytest + +from airflow.models.connection import Connection +from airflow.models.variable import Variable +from airflow.secrets.metastore import MetastoreBackend +from airflow.utils.session import create_session + +from tests_common.test_utils.db import clear_db_connections, clear_db_variables + +pytestmark = pytest.mark.db_test + + +class TestMetastoreBackendSessionSafety: + """MetastoreBackend must not corrupt the shared scoped session. + + Regression tests for https://github.com/apache/airflow/issues/62244. + """ + + def setup_method(self) -> None: + clear_db_connections() + clear_db_variables() + + def teardown_method(self) -> None: + clear_db_connections() + clear_db_variables() + + @pytest.mark.parametrize("conn_exists", [True, False], ids=["found", "not_found"]) + def test_get_connection_preserves_pending_session_objects(self, conn_exists): + """get_connection must not remove unrelated pending objects from session.new.""" + if conn_exists: + with create_session() as session: + session.add(Connection(conn_id="target_conn", conn_type="mysql")) + session.commit() + + with create_session() as session: + # Simulate pending work from another function sharing the session + pending = Connection(conn_id="pending_conn", conn_type="http") + session.add(pending) + + # Same session passed to simulate shared scoped session behavior + backend = MetastoreBackend() + result = backend.get_connection("target_conn", session=session) + + if conn_exists: + assert result is not None + assert result.conn_id == "target_conn" + else: + assert result is None + # The pending object must still be in session.new — expunge(conn) should only + # detach the queried Connection, not wipe unrelated pending objects. + assert pending in session.new + + @pytest.mark.parametrize("var_exists", [True, False], ids=["found", "not_found"]) + def test_get_variable_preserves_pending_session_objects(self, var_exists): + """get_variable must not remove unrelated pending objects from session.new.""" + if var_exists: + Variable.set(key="test_key", value="test_value") + + with create_session() as session: + # Use any ORM model as the pending object to detect session corruption + pending = Connection(conn_id="pending_conn", conn_type="http") + session.add(pending) + + backend = MetastoreBackend() + result = backend.get_variable("test_key", session=session) + + if var_exists: + assert result == "test_value" + else: + assert result is None + # The pending object must still be in session.new — expunge(var_value) should only + # detach the queried Variable, not wipe unrelated pending objects. + assert pending in session.new + + def test_get_connection_returns_detached_object(self): + """Returned connection must be detached so callers can use it freely.""" + from sqlalchemy import inspect as sa_inspect + + with create_session() as session: + session.add(Connection(conn_id="test_conn", conn_type="mysql", host="localhost")) + session.commit() + + backend = MetastoreBackend() + conn = backend.get_connection("test_conn") + + assert conn is not None + # Object should be detached — not tracked by any session + assert sa_inspect(conn).detached + # Attributes should still be accessible + assert conn.conn_id == "test_conn" + assert conn.host == "localhost" diff --git a/airflow-core/tests/unit/api_fastapi/auth/test_tokens.py b/airflow-core/tests/unit/api_fastapi/auth/test_tokens.py index e477c42af4f5a..6b848f723a004 100644 --- a/airflow-core/tests/unit/api_fastapi/auth/test_tokens.py +++ b/airflow-core/tests/unit/api_fastapi/auth/test_tokens.py @@ -264,6 +264,37 @@ async def test_jwt_generate_validate_roundtrip_with_jwks(private_key, algorithm, assert await validator.avalidated_claims(token) +@pytest.mark.parametrize("private_key", ["rsa_private_key", "ed25519_private_key"], indirect=True) +async def test_jwt_validate_roundtrip_with_jwks_and_guess_algorithm(private_key, tmp_path: pathlib.Path): + jwk_content = json.dumps({"keys": [key_to_jwk_dict(private_key, "custom-kid")]}) + + jwks = tmp_path.joinpath("jwks.json") + await anyio.Path(jwks).write_text(jwk_content) + + priv_key = tmp_path.joinpath("key.pem") + await anyio.Path(priv_key).write_bytes(key_to_pem(private_key)) + + with conf_vars( + { + ("api_auth", "trusted_jwks_url"): str(jwks), + ("api_auth", "jwt_kid"): "custom-kid", + ("api_auth", "jwt_issuer"): "http://my-issuer.localdomain", + ("api_auth", "jwt_private_key_path"): str(priv_key), + ("api_auth", "jwt_algorithm"): "GUESS", + ("api_auth", "jwt_secret"): "", + } + ): + gen = JWTGenerator(audience="airflow1", valid_for=300) + token = gen.generate({"sub": "test"}) + + validator = JWTValidator( + audience="airflow1", + leeway=0, + **get_sig_validation_args(make_secret_key_if_needed=False), + ) + assert await validator.avalidated_claims(token) + + class TestRevokeToken: pytestmark = [pytest.mark.db_test] diff --git a/airflow-core/tests/unit/api_fastapi/common/test_exceptions.py b/airflow-core/tests/unit/api_fastapi/common/test_exceptions.py index d21e5a3b01b81..c3958f0297a36 100644 --- a/airflow-core/tests/unit/api_fastapi/common/test_exceptions.py +++ b/airflow-core/tests/unit/api_fastapi/common/test_exceptions.py @@ -119,6 +119,67 @@ def teardown_method(self) -> None: clear_db_runs() clear_db_dags() + @pytest.mark.parametrize( + ("table", "expected_exception"), + [ + [ + "Pool", + HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "reason": "Unique constraint violation", + "statement": "hidden", + "orig_error": "hidden", + "message": MESSAGE, + }, + ), + ], + [ + "Variable", + HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "reason": "Unique constraint violation", + "statement": "hidden", + "orig_error": "hidden", + "message": MESSAGE, + }, + ), + ], + ], + ) + @patch("airflow.api_fastapi.common.exceptions.get_random_string", return_value=MOCKED_ID) + @conf_vars({("api", "expose_stacktrace"): "False"}) + @provide_session + def test_handle_single_column_unique_constraint_error_without_stacktrace( + self, + mock_get_random_string, + session, + table, + expected_exception, + ) -> None: + # Take Pool and Variable tables as test cases + # Note: SQLA2 uses a more optimized bulk insert strategy when multiple objects are added to the + # session. Instead of individual INSERT statements, a single INSERT with the SELECT FROM VALUES + # pattern is used. + if table == "Pool": + session.add(Pool(pool=TEST_POOL, slots=1, description="test pool", include_deferred=False)) + session.flush() # Avoid SQLA2.0 bulk insert optimization + session.add(Pool(pool=TEST_POOL, slots=1, description="test pool", include_deferred=False)) + elif table == "Variable": + session.add(Variable(key=TEST_VARIABLE_KEY, val="test_val")) + session.flush() + session.add(Variable(key=TEST_VARIABLE_KEY, val="test_val")) + + with pytest.raises(IntegrityError) as exeinfo_integrity_error: + session.commit() + + with pytest.raises(HTTPException) as exeinfo_response_error: + self.unique_constraint_error_handler.exception_handler(None, exeinfo_integrity_error.value) # type: ignore + + assert exeinfo_response_error.value.status_code == expected_exception.status_code + assert exeinfo_response_error.value.detail == expected_exception.detail + @pytest.mark.parametrize( ("table", "expected_exception"), generate_test_cases_parametrize( @@ -131,7 +192,6 @@ def teardown_method(self) -> None: "reason": "Unique constraint violation", "statement": "INSERT INTO slot_pool (pool, slots, description, include_deferred, team_name) VALUES (?, ?, ?, ?, ?)", "orig_error": "UNIQUE constraint failed: slot_pool.pool", - "message": MESSAGE, }, ), HTTPException( @@ -140,7 +200,6 @@ def teardown_method(self) -> None: "reason": "Unique constraint violation", "statement": "INSERT INTO slot_pool (pool, slots, description, include_deferred, team_name) VALUES (%s, %s, %s, %s, %s)", "orig_error": "(1062, \"Duplicate entry 'test_pool' for key 'slot_pool.slot_pool_pool_uq'\")", - "message": MESSAGE, }, ), HTTPException( @@ -149,7 +208,6 @@ def teardown_method(self) -> None: "reason": "Unique constraint violation", "statement": "INSERT INTO slot_pool (pool, slots, description, include_deferred, team_name) VALUES (%(pool)s, %(slots)s, %(description)s, %(include_deferred)s, %(team_name)s) RETURNING slot_pool.id", "orig_error": 'duplicate key value violates unique constraint "slot_pool_pool_uq"\nDETAIL: Key (pool)=(test_pool) already exists.\n', - "message": MESSAGE, }, ), ], @@ -160,7 +218,6 @@ def teardown_method(self) -> None: "reason": "Unique constraint violation", "statement": 'INSERT INTO variable ("key", val, description, is_encrypted, team_name) VALUES (?, ?, ?, ?, ?)', "orig_error": "UNIQUE constraint failed: variable.key", - "message": MESSAGE, }, ), HTTPException( @@ -169,7 +226,6 @@ def teardown_method(self) -> None: "reason": "Unique constraint violation", "statement": "INSERT INTO variable (`key`, val, description, is_encrypted, team_name) VALUES (%s, %s, %s, %s, %s)", "orig_error": "(1062, \"Duplicate entry 'test_key' for key 'variable.variable_key_uq'\")", - "message": MESSAGE, }, ), HTTPException( @@ -178,7 +234,6 @@ def teardown_method(self) -> None: "reason": "Unique constraint violation", "statement": "INSERT INTO variable (key, val, description, is_encrypted, team_name) VALUES (%(key)s, %(val)s, %(description)s, %(is_encrypted)s, %(team_name)s) RETURNING variable.id", "orig_error": 'duplicate key value violates unique constraint "variable_key_uq"\nDETAIL: Key (key)=(test_key) already exists.\n', - "message": MESSAGE, }, ), ], @@ -186,9 +241,9 @@ def teardown_method(self) -> None: ), ) @patch("airflow.api_fastapi.common.exceptions.get_random_string", return_value=MOCKED_ID) - @conf_vars({("api", "expose_stacktrace"): "False"}) + @conf_vars({("api", "expose_stacktrace"): "True"}) @provide_session - def test_handle_single_column_unique_constraint_error( + def test_handle_single_column_unique_constraint_error_with_stacktrace( self, mock_get_random_string, session, @@ -214,7 +269,46 @@ def test_handle_single_column_unique_constraint_error( with pytest.raises(HTTPException) as exeinfo_response_error: self.unique_constraint_error_handler.exception_handler(None, exeinfo_integrity_error.value) # type: ignore + exeinfo_response_error.value.detail.pop("message", None) # type: ignore[attr-defined] + assert exeinfo_response_error.value.status_code == expected_exception.status_code + assert exeinfo_response_error.value.detail == expected_exception.detail + + @patch("airflow.api_fastapi.common.exceptions.get_random_string", return_value=MOCKED_ID) + @conf_vars({("api", "expose_stacktrace"): "False"}) + @provide_session + def test_handle_multiple_columns_unique_constraint_error_without_stacktrace( + self, + mock_get_random_string, + session, + ) -> None: + expected_exception = HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "reason": "Unique constraint violation", + "statement": "hidden", + "orig_error": "hidden", + "message": MESSAGE, + }, + ) + session.add( + DagRun(dag_id="test_dag_id", run_id="test_run_id", run_type="manual", state=DagRunState.RUNNING) + ) + session.add( + DagRun(dag_id="test_dag_id", run_id="test_run_id", run_type="manual", state=DagRunState.RUNNING) + ) + with pytest.raises(IntegrityError) as exeinfo_integrity_error: + session.commit() + + with pytest.raises(HTTPException) as exeinfo_response_error: + self.unique_constraint_error_handler.exception_handler(None, exeinfo_integrity_error.value) # type: ignore + assert exeinfo_response_error.value.status_code == expected_exception.status_code + # The SQL statement is an implementation detail, so we match on the statement pattern (contains + # the table name and is an INSERT) instead of insisting on an exact match. + response_detail = exeinfo_response_error.value.detail + expected_detail = expected_exception.detail + + assert response_detail == expected_detail assert exeinfo_response_error.value.detail == expected_exception.detail @pytest.mark.parametrize( @@ -229,7 +323,6 @@ def test_handle_single_column_unique_constraint_error( "reason": "Unique constraint violation", "statement": "INSERT INTO dag_run (dag_id, queued_at, logical_date, start_date, end_date, state, run_id, creating_job_id, run_type, triggered_by, triggering_user_name, conf, data_interval_start, data_interval_end, run_after, last_scheduling_decision, log_template_id, updated_at, clear_number, backfill_id, bundle_version, scheduled_by_job_id, context_carrier, created_dag_version_id, partition_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (SELECT max(log_template.id) AS max_1 \nFROM log_template), ?, ?, ?, ?, ?, ?, ?, ?)", "orig_error": "UNIQUE constraint failed: dag_run.dag_id, dag_run.run_id", - "message": MESSAGE, }, ), HTTPException( @@ -238,7 +331,6 @@ def test_handle_single_column_unique_constraint_error( "reason": "Unique constraint violation", "statement": "INSERT INTO dag_run (dag_id, queued_at, logical_date, start_date, end_date, state, run_id, creating_job_id, run_type, triggered_by, triggering_user_name, conf, data_interval_start, data_interval_end, run_after, last_scheduling_decision, log_template_id, updated_at, clear_number, backfill_id, bundle_version, scheduled_by_job_id, context_carrier, created_dag_version_id, partition_key) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, (SELECT max(log_template.id) AS max_1 \nFROM log_template), %s, %s, %s, %s, %s, %s, %s, %s)", "orig_error": "(1062, \"Duplicate entry 'test_dag_id-test_run_id' for key 'dag_run.dag_run_dag_id_run_id_key'\")", - "message": MESSAGE, }, ), HTTPException( @@ -247,7 +339,6 @@ def test_handle_single_column_unique_constraint_error( "reason": "Unique constraint violation", "statement": "INSERT INTO dag_run (dag_id, queued_at, logical_date, start_date, end_date, state, run_id, creating_job_id, run_type, triggered_by, triggering_user_name, conf, data_interval_start, data_interval_end, run_after, last_scheduling_decision, log_template_id, updated_at, clear_number, backfill_id, bundle_version, scheduled_by_job_id, context_carrier, created_dag_version_id, partition_key) VALUES (%(dag_id)s, %(queued_at)s, %(logical_date)s, %(start_date)s, %(end_date)s, %(state)s, %(run_id)s, %(creating_job_id)s, %(run_type)s, %(triggered_by)s, %(triggering_user_name)s, %(conf)s, %(data_interval_start)s, %(data_interval_end)s, %(run_after)s, %(last_scheduling_decision)s, (SELECT max(log_template.id) AS max_1 \nFROM log_template), %(updated_at)s, %(clear_number)s, %(backfill_id)s, %(bundle_version)s, %(scheduled_by_job_id)s, %(context_carrier)s, %(created_dag_version_id)s, %(partition_key)s) RETURNING dag_run.id", "orig_error": 'duplicate key value violates unique constraint "dag_run_dag_id_run_id_key"\nDETAIL: Key (dag_id, run_id)=(test_dag_id, test_run_id) already exists.\n', - "message": MESSAGE, }, ), ], @@ -255,9 +346,9 @@ def test_handle_single_column_unique_constraint_error( ), ) @patch("airflow.api_fastapi.common.exceptions.get_random_string", return_value=MOCKED_ID) - @conf_vars({("api", "expose_stacktrace"): "False"}) + @conf_vars({("api", "expose_stacktrace"): "True"}) @provide_session - def test_handle_multiple_columns_unique_constraint_error( + def test_handle_multiple_columns_unique_constraint_error_with_stacktrace( self, mock_get_random_string, session, @@ -290,6 +381,8 @@ def test_handle_multiple_columns_unique_constraint_error( actual_statement = response_detail.pop("statement", None) # type: ignore[attr-defined] expected_detail.pop("statement", None) + # Removes the stacktrace from response to remove during comparison. + response_detail.pop("message", None) # type: ignore[attr-defined] assert response_detail == expected_detail assert "INSERT INTO dag_run" in actual_statement assert exeinfo_response_error.value.detail == expected_exception.detail diff --git a/airflow-core/tests/unit/api_fastapi/common/test_http_access_log.py b/airflow-core/tests/unit/api_fastapi/common/test_http_access_log.py new file mode 100644 index 0000000000000..3547fdf22adc2 --- /dev/null +++ b/airflow-core/tests/unit/api_fastapi/common/test_http_access_log.py @@ -0,0 +1,132 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import structlog +import structlog.testing +from starlette.applications import Starlette +from starlette.responses import PlainTextResponse +from starlette.routing import Route +from starlette.testclient import TestClient + +from airflow.api_fastapi.common.http_access_log import _HEALTH_PATHS, HttpAccessLogMiddleware + + +def _make_app(raise_exc: bool = False) -> Starlette: + async def homepage(request): + if raise_exc: + raise RuntimeError("boom") + return PlainTextResponse("ok") + + async def health(request): + return PlainTextResponse("healthy") + + app = Starlette( + routes=[ + Route("/", homepage), + Route("/api/v2/monitor/health", health), + ] + ) + app.add_middleware(HttpAccessLogMiddleware) + return app + + +def test_logs_request_fields(): + with structlog.testing.capture_logs() as logs: + client = TestClient(_make_app(), raise_server_exceptions=False) + client.get("/?foo=bar") + + assert len(logs) == 1 + record = logs[0] + assert record["event"] == "request finished" + assert record["method"] == "GET" + assert record["path"] == "/" + assert record["query"] == "foo=bar" + assert record["status_code"] == 200 + assert "duration_us" in record + assert isinstance(record["duration_us"], int) + assert record["duration_us"] >= 0 + assert "client_addr" in record + + +def test_health_path_not_logged(): + with structlog.testing.capture_logs() as logs: + client = TestClient(_make_app(), raise_server_exceptions=False) + client.get("/api/v2/monitor/health") + + assert logs == [] + + +def test_request_id_bound_to_context(): + """request_id header is bound to structlog contextvars during the request.""" + captured_context: dict = {} + + async def homepage(request): + captured_context.update(structlog.contextvars.get_contextvars()) + return PlainTextResponse("ok") + + app = Starlette(routes=[Route("/", homepage)]) + app.add_middleware(HttpAccessLogMiddleware) + + TestClient(app).get("/", headers={"x-request-id": "test-id-123"}) + + assert captured_context.get("request_id") == "test-id-123" + + +def test_no_request_id_when_header_absent(): + """No request_id is bound when the header is absent.""" + captured_context: dict = {} + + async def homepage(request): + captured_context.update(structlog.contextvars.get_contextvars()) + return PlainTextResponse("ok") + + app = Starlette(routes=[Route("/", homepage)]) + app.add_middleware(HttpAccessLogMiddleware) + + TestClient(app).get("/") + + assert "request_id" not in captured_context + + +def test_exception_logs_500_status(): + with structlog.testing.capture_logs() as logs: + client = TestClient(_make_app(raise_exc=True), raise_server_exceptions=False) + client.get("/") + + assert len(logs) == 1 + assert logs[0]["status_code"] == 500 + + +def test_non_http_scope_not_logged(): + """Non-HTTP scopes (e.g. lifespan) are passed through without logging.""" + + async def lifespan_app(scope, receive, send): + pass + + middleware = HttpAccessLogMiddleware(lifespan_app) + + import asyncio + + with structlog.testing.capture_logs() as logs: + asyncio.get_event_loop().run_until_complete(middleware({"type": "lifespan"}, None, None)) + + assert logs == [] + + +def test_health_paths_constant(): + assert "/api/v2/monitor/health" in _HEALTH_PATHS diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py index 4659bc8873fec..7912e995d8ea6 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py @@ -25,6 +25,7 @@ from sqlalchemy import delete, func, select, update from airflow._shared.timezones import timezone +from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity, DagDetails from airflow.models import DagModel from airflow.models.asset import ( AssetActive, @@ -1444,6 +1445,27 @@ def test_should_respond_400_if_materialization_runs_denied(self, test_client, se == f"Dag with dag_id: '{self.DAG_ASSET1_ID}' does not allow asset materialization runs" ) + @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") + def test_should_respond_403_when_user_cannot_trigger_dag(self, test_client): + with mock.patch( + "airflow.api_fastapi.core_api.routes.public.assets.get_auth_manager", + autospec=True, + ) as mock_get_auth_manager: + mock_get_auth_manager.return_value.is_authorized_dag.return_value = False + + response = test_client.post("/assets/1/materialize") + + assert response.status_code == 403 + assert response.json()["detail"] == ( + f"User is not authorized to trigger a run for DAG: {self.DAG_ASSET1_ID} that materializes this asset" + ) + mock_get_auth_manager.return_value.is_authorized_dag.assert_called_once_with( + method="POST", + access_entity=DagAccessEntity.RUN, + details=DagDetails(id=self.DAG_ASSET1_ID), + user=mock.ANY, + ) + class TestGetAssetQueuedEvents(TestQueuedEventEndpoint): @pytest.mark.usefixtures("time_freezer") diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py index 2f8d43ab219e9..1f63247cfa9ab 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py @@ -938,6 +938,34 @@ def test_patch_should_response_200_redacted_password( assert response.json() == expected_response _check_last_log(session, dag_id=None, event="patch_connection", logical_date=None, check_masked=True) + def test_patch_with_update_mask_validates_extra_as_json(self, test_client): + """When update_mask includes 'extra', the extra field validator should still reject invalid JSON.""" + self.create_connection() + response = test_client.patch( + f"/connections/{TEST_CONN_ID}", + json={ + "connection_id": TEST_CONN_ID, + "conn_type": TEST_CONN_TYPE, + "extra": "not valid json", + }, + params={"update_mask": ["extra"]}, + ) + assert response.status_code == 422 + + def test_patch_with_update_mask_rejects_extra_fields(self, test_client): + """Partial model should still forbid unknown fields.""" + self.create_connection() + response = test_client.patch( + f"/connections/{TEST_CONN_ID}", + json={ + "connection_id": TEST_CONN_ID, + "conn_type": TEST_CONN_TYPE, + "unknown_field": "value", + }, + params={"update_mask": ["host"]}, + ) + assert response.status_code == 422 + class TestConnection(TestConnectionEndpoint): def setup_method(self): diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py index e00d075f2a7aa..3a55532e3d73b 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py @@ -793,6 +793,23 @@ def test_bad_filters(self, test_client): body = response.json() assert body["detail"] == expected_detail + @pytest.mark.usefixtures("make_dag_with_multiple_versions") + @pytest.mark.parametrize( + ("dag_id", "query_params", "expected_dag_run_ids"), + [ + ("dag_with_multiple_versions", {"bundle_version": "some_commit_hash1"}, ["run1"]), + ("dag_with_multiple_versions", {"bundle_version": "some_commit_hash2"}, ["run2"]), + ("dag_with_multiple_versions", {"bundle_version": "some_commit_hash3"}, ["run3"]), + ("~", {"bundle_version": "some_commit_hash2"}, ["run2"]), + ("~", {"bundle_version": "does_not_exist"}, []), + ], + ) + def test_filter_by_bundle_version(self, test_client, dag_id, query_params, expected_dag_run_ids): + response = test_client.get(f"/dags/{dag_id}/dagRuns", params=query_params) + assert response.status_code == 200 + body = response.json() + assert [each["dag_run_id"] for each in body["dag_runs"]] == expected_dag_run_ids + def test_invalid_state(self, test_client): response = test_client.get(f"/dags/{DAG1_ID}/dagRuns", params={"state": ["invalid"]}) assert response.status_code == 422 diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_plugins.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_plugins.py index d3d2388ce5328..109ef6bac04b2 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_plugins.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_plugins.py @@ -34,7 +34,7 @@ class TestGetPlugins: # Filters ( {}, - 14, + 15, [ "InformaticaProviderPlugin", "MetadataCollectionPlugin", @@ -42,6 +42,7 @@ class TestGetPlugins: "databricks_workflow", "decreasing_priority_weight_strategy_plugin", "edge_executor", + "hitl_review", "hive", "plugin-a", "plugin-b", @@ -54,10 +55,10 @@ class TestGetPlugins: ), ( {"limit": 3, "offset": 3}, - 14, + 15, ["databricks_workflow", "decreasing_priority_weight_strategy_plugin", "edge_executor"], ), - ({"limit": 1}, 14, ["InformaticaProviderPlugin"]), + ({"limit": 1}, 15, ["InformaticaProviderPlugin"]), ], ) def test_should_respond_200( @@ -147,17 +148,17 @@ def test_invalid_external_view_destination_should_log_warning_and_continue(self, # Verify warning was logged assert any("Skipping invalid plugin due to error" in rec.message for rec in caplog.records) - response = test_client.get("/plugins", params={"limit": 6, "offset": 9}) + response = test_client.get("/plugins", params={"limit": 7, "offset": 9}) assert response.status_code == 200 body = response.json() plugins_page = body["plugins"] - # Even though limit=6, only 5 valid plugins should come back - assert len(plugins_page) == 5 + # Even though limit=7, only 6 valid plugins should come back + assert len(plugins_page) == 6 assert "test_plugin_invalid" not in [p["name"] for p in plugins_page] - assert body["total_entries"] == 14 + assert body["total_entries"] == 15 @skip_if_force_lowest_dependencies_marker diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py index 4a0ef3e1be94a..6bf7213c9c8bb 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py @@ -39,7 +39,8 @@ from airflow.models.taskinstancehistory import TaskInstanceHistory from airflow.models.taskmap import TaskMap from airflow.models.trigger import Trigger -from airflow.sdk import BaseOperator +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.sdk import BaseOperator, TaskGroup from airflow.utils.platform import getuser from airflow.utils.state import DagRunState, State, TaskInstanceState from airflow.utils.types import DagRunType @@ -1646,6 +1647,47 @@ def test_should_respond_200_for_pagination(self, test_client, session): assert (num_entries_batch1 + num_entries_batch2) == ti_count assert response_batch1 != response_batch2 + def test_task_group_filter_uses_run_version_not_latest(self, test_client, dag_maker, session): + """ + Task group lookup should use the DAG version from the run, not the latest version. + + When a task group is renamed between versions, clicking on a historical run's + task group in the grid should still resolve correctly against the version + that run was created with — not the latest version where the group may have + a different name, i.e serialized_dag might not have that taskgroup anymore. + """ + dag_id = "test_tg_version" + + # Version 1: task group named "process_data" + with dag_maker(dag_id, session=session): + with TaskGroup(group_id="process_data"): + EmptyOperator(task_id="step_1") + dag_maker.create_dagrun(run_id="run_v1") + session.commit() + + # Version 2: task group renamed to "process_data_v2" + with dag_maker(dag_id, session=session): + with TaskGroup(group_id="process_data_v2"): + EmptyOperator(task_id="step_1") + session.commit() + + # The run was created with v1 which had "process_data". + # Querying with the old group name must succeed. + response = test_client.get( + f"/dags/{dag_id}/dagRuns/run_v1/taskInstances", + params={"task_group_id": "process_data"}, + ) + assert response.status_code == 200, response.json() + assert response.json()["total_entries"] == 1 + assert response.json()["task_instances"][0]["task_id"] == "process_data.step_1" + + # The new group name should NOT be found in the old run's version. + response = test_client.get( + f"/dags/{dag_id}/dagRuns/run_v1/taskInstances", + params={"task_group_id": "process_data_v2"}, + ) + assert response.status_code == 404 + class TestGetTaskDependencies(TestTaskInstanceEndpoint): def setup_method(self): diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py index 0617fa184cca4..467b03554f0b1 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py @@ -494,6 +494,23 @@ def test_patch_should_respond_404(self, test_client): body = response.json() assert f"The Variable with key: `{TEST_VARIABLE_KEY}` was not found" == body["detail"] + @pytest.mark.enable_redact + def test_patch_with_update_mask_description_only(self, test_client, session): + """PATCH with update_mask=['description'] should only update description, keeping value unchanged.""" + self.create_variables() + response = test_client.patch( + f"/variables/{TEST_VARIABLE_KEY}", + json={ + "key": TEST_VARIABLE_KEY, + "value": "ignored_value", + "description": "updated description", + }, + params={"update_mask": ["description"]}, + ) + assert response.status_code == 200 + assert response.json()["description"] == "updated description" + assert response.json()["key"] == TEST_VARIABLE_KEY + class TestPostVariable(TestVariableEndpoint): @pytest.mark.enable_redact diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_auth.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_auth.py index 98a46ea08a686..287f2f9318bb0 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_auth.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_auth.py @@ -76,3 +76,41 @@ def test_with_unauthenticated_user(self, unauthenticated_test_client): response = unauthenticated_test_client.get("/auth/me") assert response.status_code == 401 assert response.json() == {"detail": "Not authenticated"} + + +class TestGenerateToken: + def test_generate_api_token(self, test_client): + """Test generating an API token returns correct response shape.""" + response = test_client.post("/auth/token", json={"token_type": "api"}) + + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert data["token_type"] == "api" + assert data["expires_in_seconds"] == 86400 # default jwt_expiration_time + + def test_generate_cli_token(self, test_client): + """Test generating a CLI token uses jwt_cli_expiration_time config.""" + response = test_client.post("/auth/token", json={"token_type": "cli"}) + + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert data["token_type"] == "cli" + # cli expiration comes from jwt_cli_expiration_time config + assert isinstance(data["expires_in_seconds"], int) + assert data["expires_in_seconds"] > 0 + + def test_default_token_type_is_api(self, test_client): + """Test that the default token type is API when not specified.""" + response = test_client.post("/auth/token", json={}) + + assert response.status_code == 200 + data = response.json() + assert data["token_type"] == "api" + + def test_unauthenticated_request(self, unauthenticated_test_client): + """Test that unauthenticated requests are rejected.""" + response = unauthenticated_test_client.post("/auth/token", json={"token_type": "api"}) + assert response.status_code == 401 + assert response.json() == {"detail": "Not authenticated"} diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py index b14ef9cf1a069..21ae10d23b3a9 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py @@ -153,7 +153,7 @@ def test_should_response_200( expected_response = [] for backfill in response_params: expected_response.append(backfill_responses[backfill]) - with assert_queries_count(2 if test_params.get("dag_id") is None else 3): + with assert_queries_count(3 if test_params.get("dag_id") is None else 4): response = test_client.get("/backfills", params=test_params) assert response.status_code == 200 assert response.json() == { @@ -168,3 +168,27 @@ def test_should_response_401(self, unauthenticated_test_client): def test_should_response_403(self, unauthorized_test_client): response = unauthorized_test_client.get("/backfills", params={}) assert response.status_code == 403 + + @mock.patch("airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager.get_authorized_dag_ids") + def test_should_only_return_authorized_dag_backfills( + self, mock_get_authorized_dag_ids, test_client, session, testing_dag_bundle + ): + dags = self._create_dag_models() + from_date = timezone.utcnow() + to_date = timezone.utcnow() + backfills = [ + Backfill(dag_id=dags[0].dag_id, from_date=from_date, to_date=to_date), + Backfill(dag_id=dags[1].dag_id, from_date=from_date, to_date=to_date), + Backfill(dag_id=dags[2].dag_id, from_date=from_date, to_date=to_date), + ] + session.add_all(backfills) + session.commit() + + mock_get_authorized_dag_ids.return_value = {"TEST_DAG_2", "TEST_DAG_3"} + response = test_client.get("/backfills") + + mock_get_authorized_dag_ids.assert_called_once_with(user=mock.ANY, method="GET") + assert response.status_code == 200 + body = response.json() + assert body["total_entries"] == 2 + assert {b["dag_id"] for b in body["backfills"]} == {"TEST_DAG_2", "TEST_DAG_3"} diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py index 6ffe5932fbe88..ca5f44b1fc5b6 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py @@ -51,6 +51,8 @@ "text-transform": "uppercase", }, }, + "icon": "https://somehost.com/static/custom-logo.svg", + "icon_dark_mode": "/static/custom-logo-dark.svg", } expected_config_response = { diff --git a/airflow-core/tests/unit/api_fastapi/core_api/test_base.py b/airflow-core/tests/unit/api_fastapi/core_api/test_base.py new file mode 100644 index 0000000000000..15a47efac11f7 --- /dev/null +++ b/airflow-core/tests/unit/api_fastapi/core_api/test_base.py @@ -0,0 +1,78 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +import pytest +from pydantic import Field, ValidationError, field_validator + +from airflow.api_fastapi.core_api.base import StrictBaseModel, make_partial_model + + +class SampleModel(StrictBaseModel): + """A sample model with required and optional fields for testing.""" + + name: str = Field(max_length=50) + age: int + email: str | None = Field(default=None) + + @field_validator("name") + @classmethod + def name_must_not_be_empty(cls, v: str) -> str: + if not v.strip(): + raise ValueError("name must not be empty") + return v + + +SampleModelPartial = make_partial_model(SampleModel) + + +class TestMakePartialModel: + def test_all_fields_become_optional(self): + instance = SampleModelPartial() + assert instance.name is None + assert instance.age is None + assert instance.email is None + + def test_partial_model_accepts_subset_of_fields(self): + instance = SampleModelPartial(name="Alice") + assert instance.name == "Alice" + assert instance.age is None + + def test_full_model_still_requires_fields(self): + with pytest.raises(ValidationError): + SampleModel(email="test@example.com") + + def test_validators_are_preserved(self): + with pytest.raises(ValidationError, match="name must not be empty"): + SampleModelPartial(name=" ") + + def test_field_metadata_preserved(self): + with pytest.raises(ValidationError): + SampleModelPartial(name="x" * 51) + + def test_extra_forbid_preserved(self): + with pytest.raises(ValidationError): + SampleModelPartial(unknown_field="test") + + def test_already_optional_fields_stay_optional(self): + instance = SampleModelPartial(email="test@example.com") + assert instance.email == "test@example.com" + assert instance.name is None + + def test_partial_model_name(self): + assert SampleModelPartial.__name__ == "SampleModelPartial" diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/conftest.py b/airflow-core/tests/unit/api_fastapi/execution_api/conftest.py index 9e26937b63c06..78bd0548df9d2 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/conftest.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/conftest.py @@ -16,51 +16,43 @@ # under the License. from __future__ import annotations -from unittest.mock import AsyncMock - import pytest +from fastapi import FastAPI, Request from fastapi.testclient import TestClient from airflow.api_fastapi.app import cached_app -from airflow.api_fastapi.auth.tokens import JWTValidator -from airflow.api_fastapi.execution_api.app import lifespan +from airflow.api_fastapi.execution_api.datamodels.token import TIToken +from airflow.api_fastapi.execution_api.security import _jwt_bearer + + +def _get_execution_api_app(root_app: FastAPI) -> FastAPI: + """Find the mounted execution API sub-app.""" + for route in root_app.routes: + if hasattr(route, "path") and route.path == "/execution": + return route.app + raise RuntimeError("Execution API sub-app not found") + + +@pytest.fixture +def exec_app(client): + """Return the execution API sub-app.""" + return _get_execution_api_app(client.app) @pytest.fixture def client(request: pytest.FixtureRequest): app = cached_app(apps="execution") + exec_app = _get_execution_api_app(app) - with TestClient(app, headers={"Authorization": "Bearer fake"}) as client: - auth = AsyncMock(spec=JWTValidator) - - # Create a side_effect function that dynamically extracts the task instance ID from validators - def smart_validated_claims(cred, validators=None): - # Extract task instance ID from validators if present - # This handles the JWTBearerTIPathDep case where the validator contains the task ID from the path - if ( - validators - and "sub" in validators - and isinstance(validators["sub"], dict) - and "value" in validators["sub"] - ): - return { - "sub": validators["sub"]["value"], - "exp": 9999999999, # Far future expiration - "iat": 1000000000, # Past issuance time - "aud": "test-audience", - } + async def mock_jwt_bearer(request: Request): + from uuid import UUID - # For other cases (like JWTBearerDep) where no specific validators are provided - # Return a default UUID with all required claims - return { - "sub": "00000000-0000-0000-0000-000000000000", - "exp": 9999999999, # Far future expiration - "iat": 1000000000, # Past issuance time - "aud": "test-audience", - } + ti_id = UUID(request.path_params.get("task_instance_id", "00000000-0000-0000-0000-000000000000")) + return TIToken(id=ti_id, claims={"sub": str(ti_id), "scope": "execution"}) - # Set the side_effect for avalidated_claims - auth.avalidated_claims.side_effect = smart_validated_claims - lifespan.registry.register_value(JWTValidator, auth) + exec_app.dependency_overrides[_jwt_bearer] = mock_jwt_bearer + with TestClient(app, headers={"Authorization": "Bearer fake"}) as client: yield client + + exec_app.dependency_overrides.pop(_jwt_bearer, None) diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/test_app.py b/airflow-core/tests/unit/api_fastapi/execution_api/test_app.py index 640d920137c7b..b0cb1d85c2e33 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/test_app.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/test_app.py @@ -43,6 +43,23 @@ def test_access_api_contract(client): assert response.headers["airflow-api-version"] == bundle.versions[0].value +def test_ti_self_routes_have_task_instance_id_param(client): + """Every route with ti:self scope must have a {task_instance_id} path parameter.""" + from fastapi.params import Security as SecurityParam + from fastapi.routing import APIRoute + + app = client.app + + for route in app.routes: + if not isinstance(route, APIRoute): + continue + for dep in route.dependencies: + if isinstance(dep, SecurityParam) and "ti:self" in (dep.scopes or []): + assert "task_instance_id" in route.dependant.path_param_names, ( + f"Route {route.path} has ti:self scope but no {{task_instance_id}} path parameter" + ) + + class TestCorrelationIdMiddleware: def test_correlation_id_echoed_in_response_headers(self, client): """Test that correlation-id from request is echoed back in response headers.""" diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/test_security.py b/airflow-core/tests/unit/api_fastapi/execution_api/test_security.py new file mode 100644 index 0000000000000..8fff2c9f7322a --- /dev/null +++ b/airflow-core/tests/unit/api_fastapi/execution_api/test_security.py @@ -0,0 +1,138 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from uuid import UUID + +import pytest +from fastapi import APIRouter, FastAPI, Request, Security +from fastapi.testclient import TestClient + +from airflow.api_fastapi.execution_api.datamodels.token import TIToken +from airflow.api_fastapi.execution_api.security import ExecutionAPIRoute, _jwt_bearer, require_auth + + +class TestExecutionAPIRoute: + """Unit tests for ExecutionAPIRoute precomputing allowed_token_types from Security scopes.""" + + def test_defaults_to_execution_only(self): + route = ExecutionAPIRoute( + path="/test", + endpoint=lambda: None, + dependencies=[Security(require_auth)], + ) + assert route.allowed_token_types == frozenset({"execution"}) + + def test_extracts_token_scopes(self): + route = ExecutionAPIRoute( + path="/test", + endpoint=lambda: None, + dependencies=[ + Security(require_auth), + Security(require_auth, scopes=["token:execution", "token:workload"]), + ], + ) + assert route.allowed_token_types == frozenset({"execution", "workload"}) + + def test_ignores_non_token_scopes(self): + route = ExecutionAPIRoute( + path="/test", + endpoint=lambda: None, + dependencies=[ + Security(require_auth, scopes=["ti:self", "token:execution"]), + ], + ) + assert route.allowed_token_types == frozenset({"execution"}) + + def test_rejects_invalid_token_types(self): + with pytest.raises(ValueError, match="Invalid token types"): + ExecutionAPIRoute( + path="/test", + endpoint=lambda: None, + dependencies=[ + Security(require_auth, scopes=["token:bogus"]), + ], + ) + + +class TestTokenTypeScopeEnforcement: + """End-to-end: ExecutionAPIRoute + require_auth enforce token types via Security scopes.""" + + @pytest.fixture + def token_type_app(self): + """ + Mirrors the real router structure: an authenticated_router with Security(require_auth), + a child ti_id_router with ExecutionAPIRoute and ti:self, and a specific endpoint on that + router opting in to workload tokens via endpoint-level Security scopes. + """ + app = FastAPI() + + authenticated_router = APIRouter(dependencies=[Security(require_auth)]) + ti_id_router = APIRouter( + route_class=ExecutionAPIRoute, + dependencies=[Security(require_auth, scopes=["ti:self"])], + ) + + @ti_id_router.get("/{task_instance_id}/state") + def default_endpoint(task_instance_id: str): + return {"ok": True} + + @ti_id_router.get( + "/{task_instance_id}/run", + dependencies=[Security(require_auth, scopes=["token:execution", "token:workload"])], + ) + def workload_endpoint(task_instance_id: str): + return {"ok": True} + + authenticated_router.include_router(ti_id_router, prefix="/task-instances") + app.include_router(authenticated_router) + + return app + + TI_ID = "00000000-0000-0000-0000-000000000001" + + def _override_jwt(self, app, scope: str): + ti_id = self.TI_ID + + async def mock_jwt(request: Request): + return TIToken(id=UUID(ti_id), claims={"scope": scope}) + + app.dependency_overrides[_jwt_bearer] = mock_jwt + + def test_workload_token_rejected_on_default_route(self, token_type_app): + self._override_jwt(token_type_app, "workload") + client = TestClient(token_type_app) + + resp = client.get(f"/task-instances/{self.TI_ID}/state", headers={"Authorization": "Bearer fake"}) + assert resp.status_code == 403 + assert "Token type 'workload' not allowed" in resp.json()["detail"] + + def test_workload_token_accepted_on_opted_in_route(self, token_type_app): + self._override_jwt(token_type_app, "workload") + client = TestClient(token_type_app) + + resp = client.get(f"/task-instances/{self.TI_ID}/run", headers={"Authorization": "Bearer fake"}) + assert resp.status_code == 200 + + def test_execution_token_accepted_on_both_routes(self, token_type_app): + self._override_jwt(token_type_app, "execution") + client = TestClient(token_type_app) + + state = client.get(f"/task-instances/{self.TI_ID}/state", headers={"Authorization": "Bearer fake"}) + run = client.get(f"/task-instances/{self.TI_ID}/run", headers={"Authorization": "Bearer fake"}) + assert state.status_code == 200 + assert run.status_code == 200 diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py index ea1153f01cba5..835a8c46139f7 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py @@ -34,6 +34,7 @@ from airflow.exceptions import AirflowSkipException from airflow.models import RenderedTaskInstanceFields, TaskReschedule, Trigger from airflow.models.asset import AssetActive, AssetAliasModel, AssetEvent, AssetModel +from airflow.models.log import Log from airflow.models.taskinstance import TaskInstance from airflow.models.taskinstancehistory import TaskInstanceHistory from airflow.providers.standard.operators.empty import EmptyOperator @@ -44,6 +45,7 @@ from tests_common.test_utils.db import ( clear_db_assets, clear_db_dags, + clear_db_logs, clear_db_runs, clear_db_serialized_dags, clear_rendered_ti_fields, @@ -76,13 +78,16 @@ def _create_asset_aliases(session, num: int = 2) -> None: @pytest.fixture -def client_with_extra_route(): ... +def _use_real_jwt_bearer(exec_app): + """Remove the mock jwt_bearer override so the real JWTBearer.__call__ runs.""" + from airflow.api_fastapi.execution_api.security import _jwt_bearer + exec_app.dependency_overrides.pop(_jwt_bearer, None) -def test_id_matches_sub_claim(client, session, create_task_instance): - # Test that this is validated at the router level, so we don't have to test it in each component - # We validate it is set correctly, and test it once +@pytest.mark.usefixtures("_use_real_jwt_bearer") +def test_id_matches_sub_claim(client, session, create_task_instance): + """Test that scope validation (ti:self) is enforced at the router level.""" ti = create_task_instance( task_id="test_ti_run_state_conflict_if_not_queued", state="queued", @@ -90,17 +95,10 @@ def test_id_matches_sub_claim(client, session, create_task_instance): session.commit() validator = mock.AsyncMock(spec=JWTValidator) - claims = {"sub": ti.id} - - def side_effect(cred, validators): - if not validators: - return claims - if str(validators["sub"]["value"]) != str(ti.id): - raise RuntimeError("Fake auth denied") - return claims - - validator.avalidated_claims.side_effect = side_effect - + validator.avalidated_claims.return_value = { + "sub": str(ti.id), + "scope": "execution", + } lifespan.registry.register_value(JWTValidator, validator) payload = { @@ -113,25 +111,22 @@ def side_effect(cred, validators): resp = client.patch("/execution/task-instances/9c230b40-da03-451d-8bd7-be30471be383/run", json=payload) assert resp.status_code == 403 - assert validator.avalidated_claims.call_args_list[1] == mock.call( - mock.ANY, {"sub": {"essential": True, "value": "9c230b40-da03-451d-8bd7-be30471be383"}} - ) validator.avalidated_claims.reset_mock() resp = client.patch(f"/execution/task-instances/{ti.id}/run", json=payload) - assert resp.status_code == 200, resp.json() - validator.avalidated_claims.assert_awaited() class TestTIRunState: def setup_method(self): + clear_db_logs() clear_db_runs() clear_db_serialized_dags() clear_db_dags() def teardown_method(self): + clear_db_logs() clear_db_runs() clear_db_serialized_dags() clear_db_dags() @@ -183,7 +178,8 @@ def test_ti_run_state_to_running( ) assert response.status_code == 200 - assert response.json() == { + result = response.json() + assert result == { "dag_run": { "dag_id": ti.dag_id, "run_id": "test", @@ -209,6 +205,8 @@ def test_ti_run_state_to_running( "connections": [], "xcom_keys_to_clear": [], } + # upstream_map_indexes is now computed by Task SDK, not returned by the server in HEAD version + assert "upstream_map_indexes" not in result # Refresh the Task Instance from the database so that we can check the updated values session.refresh(ti) @@ -793,14 +791,57 @@ def test_ti_run_with_triggering_user_name( assert dag_run["run_id"] == "test" assert dag_run["state"] == "running" + def test_ti_run_creates_audit_log(self, client, session, create_task_instance, time_machine): + """Test that transitioning to RUNNING creates an audit log record.""" + instant_str = "2024-09-30T12:00:00Z" + instant = timezone.parse(instant_str) + time_machine.move_to(instant, tick=False) + + ti = create_task_instance( + task_id="test_ti_run_creates_audit_log", + state=State.QUEUED, + dagrun_state=DagRunState.RUNNING, + session=session, + start_date=instant, + dag_id=str(uuid4()), + ) + session.commit() + + response = client.patch( + f"/execution/task-instances/{ti.id}/run", + json={ + "state": "running", + "hostname": "random-hostname", + "unixname": "random-unixname", + "pid": 100, + "start_date": instant_str, + }, + ) + + assert response.status_code == 200 + + logs = session.scalars(select(Log).where(Log.dag_id == ti.dag_id)).all() + assert len(logs) == 1 + assert logs[0].event == TaskInstanceState.RUNNING.value + assert logs[0].task_id == ti.task_id + assert logs[0].dag_id == ti.dag_id + assert logs[0].run_id == ti.run_id + assert logs[0].map_index == ti.map_index + assert logs[0].try_number == ti.try_number + assert logs[0].logical_date == instant + assert logs[0].owner == ti.task.owner + assert logs[0].extra == '{"host_name": "random-hostname"}' + class TestTIUpdateState: def setup_method(self): clear_db_assets() + clear_db_logs() clear_db_runs() def teardown_method(self): clear_db_assets() + clear_db_logs() clear_db_runs() @pytest.mark.parametrize( @@ -838,6 +879,82 @@ def test_ti_update_state_to_terminal( assert ti.state == expected_state assert ti.end_date == end_date + @pytest.mark.parametrize( + ("payload", "expected_event"), + [ + pytest.param( + {"state": State.SUCCESS, "end_date": DEFAULT_END_DATE.isoformat()}, + State.SUCCESS, + id="success", + ), + pytest.param( + {"state": State.FAILED, "end_date": DEFAULT_END_DATE.isoformat()}, + State.FAILED, + id="failed", + ), + pytest.param( + {"state": State.SKIPPED, "end_date": DEFAULT_END_DATE.isoformat()}, + State.SKIPPED, + id="skipped", + ), + pytest.param( + {"state": State.UP_FOR_RETRY, "end_date": DEFAULT_END_DATE.isoformat()}, + TaskInstanceState.UP_FOR_RETRY.value, + id="up_for_retry", + ), + pytest.param( + { + "state": "deferred", + "trigger_kwargs": {"key": "value", "moment": "2026-02-18T00:00:00Z"}, + "trigger_timeout": "P1D", + "classpath": "my-classpath", + "next_method": "execute_callback", + }, + TaskInstanceState.DEFERRED.value, + id="deferred", + ), + pytest.param( + { + "state": "up_for_reschedule", + "reschedule_date": "2026-02-18T11:03:00+00:00", + "end_date": DEFAULT_END_DATE.isoformat(), + }, + TaskInstanceState.UP_FOR_RESCHEDULE.value, + id="up_for_reschedule", + ), + ], + ) + def test_ti_update_state_creates_audit_log( + self, client, session, create_task_instance, payload, expected_event + ): + """Test that state transition creates an audit log record.""" + ti = create_task_instance( + task_id="test_ti_update_state_creates_audit_log", + start_date=DEFAULT_START_DATE, + state=State.RUNNING, + hostname="random-hostname", + ) + session.commit() + + response = client.patch( + f"/execution/task-instances/{ti.id}/state", + json=payload, + ) + + assert response.status_code == 204 + + logs = session.scalars(select(Log).where(Log.dag_id == ti.dag_id)).all() + assert len(logs) == 1 + assert logs[0].event == expected_event + assert logs[0].task_id == ti.task_id + assert logs[0].dag_id == ti.dag_id + assert logs[0].run_id == ti.run_id + assert logs[0].map_index == ti.map_index + assert logs[0].try_number == ti.try_number + assert logs[0].logical_date == ti.dag_run.logical_date + assert logs[0].owner == ti.task.owner + assert logs[0].extra == '{"host_name": "random-hostname"}' + @pytest.mark.parametrize( ("state", "end_date", "expected_state", "rendered_map_index"), [ @@ -1063,8 +1180,34 @@ def test_ti_update_state_database_error(self, client, session, create_task_insta mock.patch( "airflow.api_fastapi.common.db.common.Session.execute", side_effect=[ - mock.Mock(one=lambda: ("running", 1, 0, "dag")), # First call returns "queued" - mock.Mock(one=lambda: ("running", 1, 0, "dag")), # Second call returns "queued" + mock.Mock( + one=lambda: ( + "running", + 1, + 0, + "dag", + "task", + "run", + -1, + "localhost", + timezone.utcnow(), + "test_owner", + ) + ), # First call returns "queued" + mock.Mock( + one=lambda: ( + "running", + 1, + 0, + "dag", + "task", + "run", + -1, + "localhost", + timezone.utcnow(), + "test_owner", + ) + ), # Second call returns "queued" SQLAlchemyError("Database error"), # Last call raises an error ], ), @@ -2925,3 +3068,88 @@ def test_ti_patch_rendered_map_index_empty_string(self, client, session, create_ ) assert response.status_code == 422 + + +@pytest.mark.usefixtures("_use_real_jwt_bearer") +class TestTokenTypeValidation: + """Test token scope enforcement (workload vs execution).""" + + def test_workload_scope_rejected_on_default_endpoints(self, client, session, create_task_instance): + """workload scoped tokens should be rejected on endpoints without token:workload Security scope.""" + ti = create_task_instance(task_id="test_ti_run_heartbeat", state=State.RUNNING) + session.commit() + + validator = mock.AsyncMock(spec=JWTValidator) + validator.avalidated_claims.side_effect = lambda cred, validators: { + "sub": str(ti.id), + "scope": "workload", + "exp": 9999999999, + "iat": 1000000000, + } + lifespan.registry.register_value(JWTValidator, validator) + + payload = {"hostname": "test-host", "pid": 100} + resp = client.put(f"/execution/task-instances/{ti.id}/heartbeat", json=payload) + assert resp.status_code == 403 + assert "Token type 'workload' not allowed" in resp.json()["detail"] + + def test_execution_scope_accepted_on_all_endpoints(self, client, session, create_task_instance): + """execution scoped tokens should be able to call all endpoints.""" + ti = create_task_instance(task_id="test_ti_star", state=State.RUNNING) + session.commit() + + validator = mock.AsyncMock(spec=JWTValidator) + validator.avalidated_claims.side_effect = lambda cred, validators: { + "sub": str(ti.id), + "scope": "execution", + "exp": 9999999999, + "iat": 1000000000, + } + lifespan.registry.register_value(JWTValidator, validator) + + payload = {"state": "success", "end_date": "2024-10-31T13:00:00Z"} + resp = client.patch(f"/execution/task-instances/{ti.id}/state", json=payload) + assert resp.status_code in [200, 204] + + def test_invalid_scope_value_rejected(self, client, session, create_task_instance): + """Tokens with unrecognized scope values should be rejected.""" + ti = create_task_instance(task_id="test_invalid_scope", state=State.QUEUED) + session.commit() + + validator = mock.AsyncMock(spec=JWTValidator) + validator.avalidated_claims.side_effect = lambda cred, validators: { + "sub": str(ti.id), + "scope": "bogus:scope", + "exp": 9999999999, + "iat": 1000000000, + } + lifespan.registry.register_value(JWTValidator, validator) + + payload = { + "state": "running", + "hostname": "test-host", + "unixname": "test-user", + "pid": 100, + "start_date": "2024-10-31T12:00:00Z", + } + + resp = client.patch(f"/execution/task-instances/{ti.id}/run", json=payload) + assert resp.status_code == 403 + assert "Invalid token scope" in resp.json()["detail"] + + def test_no_scope_defaults_to_execution(self, client, session, create_task_instance): + """Tokens without scope claim should default to 'execution'.""" + ti = create_task_instance(task_id="test_no_scope", state=State.RUNNING) + session.commit() + + validator = mock.AsyncMock(spec=JWTValidator) + validator.avalidated_claims.side_effect = lambda cred, validators: { + "sub": str(ti.id), + "exp": 9999999999, + "iat": 1000000000, + } + lifespan.registry.register_value(JWTValidator, validator) + + payload = {"state": "success", "end_date": "2024-10-31T13:00:00Z"} + resp = client.patch(f"/execution/task-instances/{ti.id}/state", json=payload) + assert resp.status_code in [200, 204] diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_variables.py b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_variables.py index 59b206441dea6..93cd8ca672e9c 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_variables.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_variables.py @@ -41,15 +41,15 @@ def setup_method(): @pytest.fixture def access_denied(client): - from airflow.api_fastapi.execution_api.deps import JWTBearerDep from airflow.api_fastapi.execution_api.routes.variables import has_variable_access + from airflow.api_fastapi.execution_api.security import CurrentTIToken last_route = client.app.routes[-1] assert isinstance(last_route, Mount) assert isinstance(last_route.app, FastAPI) exec_app = last_route.app - async def _(request: Request, variable_key: str, token=JWTBearerDep): + async def _(request: Request, variable_key: str, token=CurrentTIToken): await has_variable_access(request, variable_key, token) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py index f805971bf5207..2135cb970a48b 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py @@ -48,8 +48,8 @@ def reset_db(): @pytest.fixture def access_denied(client): - from airflow.api_fastapi.execution_api.deps import JWTBearerDep from airflow.api_fastapi.execution_api.routes.xcoms import has_xcom_access + from airflow.api_fastapi.execution_api.security import CurrentTIToken last_route = client.app.routes[-1] assert isinstance(last_route.app, FastAPI) @@ -61,7 +61,7 @@ async def _( run_id: str = Path(), task_id: str = Path(), xcom_key: str = Path(alias="key"), - token=JWTBearerDep, + token=CurrentTIToken, ): await has_xcom_access(dag_id, run_id, task_id, xcom_key, request, token) raise HTTPException( @@ -394,7 +394,7 @@ def test_xcom_round_trip(self, client, create_task_instance, session, orig_value Test that deserialization works when XCom values are stored directly in the DB with API Server. This tests the case where the XCom value is stored from the Task API where the value is serialized - via Client SDK into JSON object and passed via the API Server to the DB. It by-passes + via Client SDK into JSON object and passed via the API Server to the DB. It bypasses the XComModel.serialize_value and stores valid Python JSON compatible objects to DB. This test is to ensure that the deserialization works correctly in this case as well as diff --git a/airflow-core/tests/unit/cli/commands/test_api_server_command.py b/airflow-core/tests/unit/cli/commands/test_api_server_command.py index 58a0923a2f93c..93c2c43e27b39 100644 --- a/airflow-core/tests/unit/cli/commands/test_api_server_command.py +++ b/airflow-core/tests/unit/cli/commands/test_api_server_command.py @@ -193,9 +193,10 @@ def test_args_to_uvicorn(self, ssl_cert_and_key, cli_args, expected_additional_k "timeout_keep_alive": args.worker_timeout, "timeout_graceful_shutdown": args.worker_timeout, "timeout_worker_healthcheck": args.worker_timeout, - "access_log": True, + "access_log": False, "log_level": "info", "proxy_headers": args.proxy_headers, + "log_config": None, **expected_additional_kwargs, }, ) @@ -246,9 +247,10 @@ def test_run_command_daemon( timeout_worker_healthcheck=60, ssl_keyfile=None, ssl_certfile=None, - access_log=True, + access_log=False, log_level="info", proxy_headers=False, + log_config=None, ) if demonize: diff --git a/airflow-core/tests/unit/cli/commands/test_connection_command.py b/airflow-core/tests/unit/cli/commands/test_connection_command.py index 8b22a570eb968..c62dfb4c5b357 100644 --- a/airflow-core/tests/unit/cli/commands/test_connection_command.py +++ b/airflow-core/tests/unit/cli/commands/test_connection_command.py @@ -29,6 +29,7 @@ from airflow.cli import cli_config, cli_parser from airflow.cli.commands import connection_command +from airflow.cli.commands.connection_command import _mask_uri_credentials from airflow.exceptions import AirflowException from airflow.models import Connection from airflow.utils.db import merge_conn @@ -95,6 +96,81 @@ def test_cli_connections_list_as_json(self): assert conn_type in stdout assert conn_id in stdout + def test_cli_connections_list_default_hides_sensitive_values(self): + """By default list shows only conn_id and conn_type, not passwords or URI.""" + args = self.parser.parse_args(["connections", "list", "--output", "json"]) + with redirect_stdout(StringIO()) as stdout_io: + connection_command.connections_list(args) + stdout = stdout_io.getvalue() + # Should not contain full URI or password fields + assert "get_uri" not in stdout + assert "password" not in stdout + assert "conn_id" in stdout + assert "conn_type" in stdout + + def test_cli_connections_list_show_values_shows_full_details(self): + """With --show-values, list includes connection details.""" + args = self.parser.parse_args(["connections", "list", "--output", "json", "--show-values"]) + with redirect_stdout(StringIO()) as stdout_io: + connection_command.connections_list(args) + stdout = stdout_io.getvalue() + assert "get_uri" in stdout + assert "conn_id" in stdout + + def test_cli_connections_list_show_values_hide_sensitive_masks_values(self): + """With --show-values --hide-sensitive, sensitive fields are masked.""" + args = self.parser.parse_args( + [ + "connections", + "list", + "--output", + "json", + "--show-values", + "--hide-sensitive", + ] + ) + with redirect_stdout(StringIO()) as stdout_io: + connection_command.connections_list(args) + stdout = stdout_io.getvalue() + assert "***" in stdout + # Password should be masked + assert '"password": "***"' in stdout + # get_uri should be selectively masked (credentials only) + # Note: Default connections may not have credentials, so we check for *** + if "***" in stdout: + # If there are masked credentials, they should be in ***:*** format in get_uri + assert '"get_uri":' in stdout + + def test_cli_connections_list_hide_sensitive_without_show_values_fails(self): + """--hide-sensitive without --show-values should fail.""" + args = self.parser.parse_args(["connections", "list", "--hide-sensitive"]) + with pytest.raises(SystemExit, match="--hide-sensitive can only be used with --show-values"): + connection_command.connections_list(args) + + +class TestUriMasking: + """Test URI credential masking functionality.""" + + @pytest.mark.parametrize( + ("uri", "expected"), + [ + # URIs with credentials + ("postgresql://user:pass@host:5432/db", "postgresql://***:***@host:5432/db"), + ("mysql://admin:secret@localhost:3306/test", "mysql://***:***@localhost:3306/test"), + ("http://api:key123@api.example.com:8080/v1", "http://***:***@api.example.com:8080/v1"), + # URIs without credentials + ("sqlite:///tmp/test.db", "sqlite:///tmp/test.db"), + ("filesystem://", "filesystem://"), + ("redis://localhost:6379/0", "redis://localhost:6379/0"), + # Edge cases + ("", ""), + ("invalid-uri", "***"), # Falls back to full masking on parse error + ], + ) + def test_mask_uri_credentials(self, uri, expected): + result = _mask_uri_credentials(uri) + assert result == expected + class TestCliExportConnections: parser = cli_parser.get_parser() diff --git a/airflow-core/tests/unit/cli/commands/test_gunicorn_monitor.py b/airflow-core/tests/unit/cli/commands/test_gunicorn_monitor.py index 4964618a3859a..f80410c0a6c48 100644 --- a/airflow-core/tests/unit/cli/commands/test_gunicorn_monitor.py +++ b/airflow-core/tests/unit/cli/commands/test_gunicorn_monitor.py @@ -342,8 +342,6 @@ def mock_init(self, options): def test_load_config_gunicorn_cmd_args_overrides_options(self, monkeypatch): """Test that GUNICORN_CMD_ARGS takes precedence over programmatic options.""" monkeypatch.setenv("GUNICORN_CMD_ARGS", "--workers 8") - from gunicorn.config import Config - from airflow.api_fastapi.gunicorn_app import AirflowGunicornApp def mock_init(self, options): @@ -416,8 +414,9 @@ def test_create_basic_app(self): assert options["bind"] == "0.0.0.0:8080" assert options["workers"] == 4 assert options["timeout"] == 120 - assert options["worker_class"] == "uvicorn.workers.UvicornWorker" + assert options["worker_class"] == "airflow.api_fastapi.gunicorn_app.AirflowUvicornWorker" assert options["preload_app"] is True + assert "accesslog" not in options def test_create_app_with_ssl(self): """Test creating an app with SSL settings.""" @@ -455,8 +454,8 @@ def test_create_app_with_proxy_headers(self): assert options["forwarded_allow_ips"] == "*" - def test_create_app_with_access_log(self): - """Test creating an app with access logging enabled.""" + def test_create_app_never_sets_accesslog(self): + """accesslog is never set; HttpAccessLogMiddleware handles HTTP access logging.""" from airflow.api_fastapi.gunicorn_app import create_gunicorn_app with mock.patch("airflow.api_fastapi.gunicorn_app.AirflowGunicornApp") as mock_app_class: @@ -465,26 +464,18 @@ def test_create_app_with_access_log(self): port=8080, num_workers=4, worker_timeout=120, - access_log=True, ) options = mock_app_class.call_args[0][0] - assert options["accesslog"] == "-" - - def test_create_app_without_access_log(self): - """Test creating an app with access logging disabled.""" - from airflow.api_fastapi.gunicorn_app import create_gunicorn_app + assert "accesslog" not in options - with mock.patch("airflow.api_fastapi.gunicorn_app.AirflowGunicornApp") as mock_app_class: - create_gunicorn_app( - host="0.0.0.0", - port=8080, - num_workers=4, - worker_timeout=120, - access_log=False, - ) + def test_airflow_uvicorn_worker_config(self): + """AirflowUvicornWorker disables uvicorn's access log and log_config override.""" + from uvicorn.workers import UvicornWorker - options = mock_app_class.call_args[0][0] + from airflow.api_fastapi.gunicorn_app import AirflowUvicornWorker - assert "accesslog" not in options + assert AirflowUvicornWorker.CONFIG_KWARGS["log_config"] is None + assert AirflowUvicornWorker.CONFIG_KWARGS["access_log"] is False + assert issubclass(AirflowUvicornWorker, UvicornWorker) diff --git a/airflow-core/tests/unit/cli/commands/test_task_command.py b/airflow-core/tests/unit/cli/commands/test_task_command.py index 7ba2f3cc4bf09..8d96d5579eccb 100644 --- a/airflow-core/tests/unit/cli/commands/test_task_command.py +++ b/airflow-core/tests/unit/cli/commands/test_task_command.py @@ -310,6 +310,68 @@ def test_mapped_task_render(self): assert "[3]" not in output assert "property: op_args" in output + @pytest.mark.usefixtures("testing_dag_bundle") + def test_mapped_task_render_out_of_range_map_index(self): + """Raise ValueError when map_index exceeds the parse-time mapped count.""" + with pytest.raises(ValueError, match=r"map_index 5 is out of range.*3 mapped instance"): + task_command.task_render( + self.parser.parse_args( + [ + "tasks", + "render", + "test_mapped_classic", + "consumer_literal", + "2022-01-01", + "--map-index", + "5", + ] + ) + ) + + @pytest.mark.usefixtures("testing_dag_bundle") + def test_mapped_task_render_boundary_map_index(self): + """Render should succeed for the last valid map_index (count - 1).""" + with redirect_stdout(io.StringIO()) as stdout: + task_command.task_render( + self.parser.parse_args( + [ + "tasks", + "render", + "test_mapped_classic", + "consumer_literal", + "2022-01-01", + "--map-index", + "2", + ] + ) + ) + output = stdout.getvalue() + assert "[3]" in output + assert "property: op_args" in output + + @pytest.mark.usefixtures("testing_dag_bundle") + def test_mapped_task_render_dynamic_skips_validation(self): + """Dynamic (XCom-based) mapping should skip map_index validation.""" + # consumer depends on XCom from make_arg_lists, so parse-time count + # is not available. Validation should be skipped (NotFullyPopulated). + # The render may fail for other reasons, but not with our + # "out of range" ValueError. + with pytest.raises(Exception) as exc_info: # noqa: PT011 + task_command.task_render( + self.parser.parse_args( + [ + "tasks", + "render", + "test_mapped_classic", + "consumer", + "2022-01-01", + "--map-index", + "999", + ] + ) + ) + assert "out of range" not in str(exc_info.value) + def test_mapped_task_render_with_template(self, dag_maker): """ tasks render should render and displays templated fields for a given mapping task diff --git a/airflow-core/tests/unit/cli/commands/test_variable_command.py b/airflow-core/tests/unit/cli/commands/test_variable_command.py index 4488b0e758d74..21d2fb66822b5 100644 --- a/airflow-core/tests/unit/cli/commands/test_variable_command.py +++ b/airflow-core/tests/unit/cli/commands/test_variable_command.py @@ -19,6 +19,8 @@ import json import os +from contextlib import redirect_stdout +from io import StringIO import pytest import yaml @@ -232,6 +234,96 @@ def test_variables_list(self): # Test command is received variable_command.variables_list(self.parser.parse_args(["variables", "list"])) + def test_variables_list_show_values(self): + """Test variables list with --show-values flag shows actual values.""" + # Create test variables + Variable.set("test_key1", "test_value1") + Variable.set("test_key2", "test_value2") + + args = self.parser.parse_args(["variables", "list", "--output", "json", "--show-values"]) + with redirect_stdout(StringIO()) as stdout_io: + variable_command.variables_list(args) + output = stdout_io.getvalue() + + # Parse JSON output and verify values are shown + data = json.loads(output) + assert len(data) >= 2 + key_value_map = {item["key"]: item["val"] for item in data} + assert "test_value1" in key_value_map["test_key1"] + assert "test_value2" in key_value_map["test_key2"] + + def test_variables_list_hide_sensitive(self): + """Test variables list with --hide-sensitive masks all values.""" + # Create test variables + Variable.set("test_key1", "test_value1") + Variable.set("test_key2", "test_value2") + + args = self.parser.parse_args( + ["variables", "list", "--output", "json", "--show-values", "--hide-sensitive"] + ) + with redirect_stdout(StringIO()) as stdout_io: + variable_command.variables_list(args) + output = stdout_io.getvalue() + + # Parse JSON output and verify values are masked + data = json.loads(output) + assert len(data) >= 2 + for item in data: + if "test_key" in item["key"]: + assert item["val"] == "***" + + def test_variables_list_hide_sensitive_without_show_values_fails(self): + """--hide-sensitive without --show-values should fail.""" + args = self.parser.parse_args(["variables", "list", "--hide-sensitive"]) + with pytest.raises(SystemExit, match="--hide-sensitive can only be used with --show-values"): + variable_command.variables_list(args) + + def test_variables_list_default_hides_values(self): + """By default, variables list should only show keys, not values.""" + Variable.set("test_key1", "test_value1") + Variable.set("test_key2", "test_value2") + + args = self.parser.parse_args(["variables", "list", "--output", "json"]) + with redirect_stdout(StringIO()) as stdout_io: + variable_command.variables_list(args) + output = stdout_io.getvalue() + + data = json.loads(output) + assert len(data) >= 2 + for item in data: + if "test_key" in item["key"]: + assert "val" not in item + + def test_variables_list_edge_cases(self): + """Test variables list with None and empty values.""" + Variable.set("empty_var", "") + Variable.set("none_var", None) + Variable.set("normal_var", "normal_value") + + args = self.parser.parse_args(["variables", "list", "--output", "json", "--show-values"]) + with redirect_stdout(StringIO()) as stdout_io: + variable_command.variables_list(args) + output = stdout_io.getvalue() + + data = json.loads(output) + key_value_map = {item["key"]: item["val"] for item in data} + + assert key_value_map["empty_var"] == "" + assert key_value_map["none_var"] == "None" + assert key_value_map["normal_var"] == "normal_value" + + args = self.parser.parse_args( + ["variables", "list", "--output", "json", "--show-values", "--hide-sensitive"] + ) + with redirect_stdout(StringIO()) as stdout_io: + variable_command.variables_list(args) + output = stdout_io.getvalue() + + data = json.loads(output) + for item in data: + if item["key"] in ["empty_var", "none_var", "normal_var"]: + assert item["val"] == "***" + def test_variables_delete(self): """Test variable_delete command""" variable_command.variables_set(self.parser.parse_args(["variables", "set", "foo", "bar"])) diff --git a/airflow-core/tests/unit/config_templates/deprecated.cfg b/airflow-core/tests/unit/config_templates/deprecated.cfg index 1a79045424816..a3f0fbbc0b0dc 100644 --- a/airflow-core/tests/unit/config_templates/deprecated.cfg +++ b/airflow-core/tests/unit/config_templates/deprecated.cfg @@ -15,5 +15,5 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -[core] -sql_alchemy_conn = mysql:// +[webserver] +secret_key = my_secret_key diff --git a/airflow-core/tests/unit/config_templates/deprecated_cmd.cfg b/airflow-core/tests/unit/config_templates/deprecated_cmd.cfg index dbe819fbb63f8..bcfeec4bc602a 100644 --- a/airflow-core/tests/unit/config_templates/deprecated_cmd.cfg +++ b/airflow-core/tests/unit/config_templates/deprecated_cmd.cfg @@ -15,5 +15,5 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -[core] -sql_alchemy_conn_cmd = echo -n "postgresql://" +[webserver] +secret_key_cmd = echo -n "test_secret_key" diff --git a/airflow-core/tests/unit/config_templates/deprecated_secret.cfg b/airflow-core/tests/unit/config_templates/deprecated_secret.cfg index ca2f5aa637199..b05540d99a496 100644 --- a/airflow-core/tests/unit/config_templates/deprecated_secret.cfg +++ b/airflow-core/tests/unit/config_templates/deprecated_secret.cfg @@ -15,5 +15,5 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -[core] -sql_alchemy_conn_secret = secret_path +[webserver] +secret_key_secret = secret_path diff --git a/airflow-core/tests/unit/core/test_configuration.py b/airflow-core/tests/unit/core/test_configuration.py index 9c7ec9a3d167f..be0f191f8c9b7 100644 --- a/airflow-core/tests/unit/core/test_configuration.py +++ b/airflow-core/tests/unit/core/test_configuration.py @@ -57,9 +57,8 @@ HOME_DIR = os.path.expanduser("~") -# The conf has been updated with sql_alchemy_con and deactivate_stale_dags_interval to test the +# The conf has been updated with deactivate_stale_dags_interval to test the # functionality of deprecated options support. -conf.deprecated_options[("database", "sql_alchemy_conn")] = ("core", "sql_alchemy_conn", "2.3.0") conf.deprecated_options[("scheduler", "parsing_cleanup_interval")] = ( "scheduler", "deactivate_stale_dags_interval", @@ -925,6 +924,62 @@ def test_order_of_secrets_backends_and_kwargs_on_workers( for key, value in expected_backend_kwargs.items(): assert getattr(secrets_backend, key) == value + def test_build_kwarg_env_prefix(self): + """Test that _build_kwarg_env_prefix generates the correct prefixes.""" + from airflow._shared.configuration.parser import _build_kwarg_env_prefix + + assert _build_kwarg_env_prefix("secrets", "backend_kwargs") == "AIRFLOW__SECRETS__BACKEND_KWARG__" + assert ( + _build_kwarg_env_prefix("workers", "secrets_backend_kwargs") + == "AIRFLOW__WORKERS__SECRETS_BACKEND_KWARG__" + ) + + @mock.patch.dict( + "os.environ", + { + "AIRFLOW__SECRETS__BACKEND_KWARG__ROLE_ID": "abc", + "AIRFLOW__SECRETS__BACKEND_KWARG__": "ignored", # empty key — must be ignored + "OTHER_VAR": "irrelevant", + }, + ) + def test_collect_kwarg_env_vars(self): + """Test that _collect_kwarg_env_vars collects matching vars and ignores empty keys.""" + from airflow._shared.configuration.parser import _collect_kwarg_env_vars + + result = _collect_kwarg_env_vars("AIRFLOW__SECRETS__BACKEND_KWARG__") + assert result == {"role_id": "abc"} + + @conf_vars( + { + ( + "workers", + "secrets_backend", + ): "airflow.providers.amazon.aws.secrets.systems_manager.SystemsManagerParameterStoreBackend", + } + ) + @mock.patch.dict( + "os.environ", + {"AIRFLOW__WORKERS__SECRETS_BACKEND_KWARG__CONNECTIONS_PREFIX": "/worker/connections"}, + ) + def test_worker_backend_kwarg_env_vars(self): + """Per-key env var is picked up for the workers secrets backend.""" + backends = ensure_secrets_loaded(DEFAULT_SECRETS_SEARCH_PATH_WORKERS) + secrets_backend = backends[0] + assert secrets_backend.__class__.__name__ == "SystemsManagerParameterStoreBackend" + assert secrets_backend.connections_prefix == "/worker/connections" + + @mock.patch("airflow._shared.secrets_masker.mask_secret") + @mock.patch("airflow.sdk.log.mask_secret") + @mock.patch.dict( + "os.environ", + {"AIRFLOW__SECRETS__BACKEND_KWARG__ROLE_ID": "super-secret-role"}, + ) + def test_mask_secrets_includes_backend_kwarg_env_vars(self, mock_sdk_mask, mock_core_mask): + """Per-key BACKEND_KWARG__* env var values are registered with the masker at startup.""" + conf.mask_secrets() + all_core_masked = [call.args[0] for call in mock_core_mask.call_args_list] + assert "super-secret-role" in all_core_masked + def test_lookup_sequence_override_excludes_env_vars(self, monkeypatch): """Test that overriding lookup sequence to exclude env vars means env vars are not respected.""" @@ -1183,38 +1238,25 @@ def test_deprecated_values_from_conf(self): ("old", "new"), [ ( - ("core", "sql_alchemy_conn", "postgres+psycopg2://localhost/postgres"), - ("database", "sql_alchemy_conn", "postgresql://localhost/postgres"), + ("webserver", "secret_key", "test_secret_value"), + ("api", "secret_key", "test_secret_value"), ), ], ) - def test_deprecated_env_vars_upgraded_and_removed(self, old, new): - test_conf = AirflowConfigParser( - default_config=""" -[core] -executor=LocalExecutor -[database] -sql_alchemy_conn=sqlite://test -""" - ) + def test_deprecated_env_vars_lookup(self, old, new): + test_conf = AirflowConfigParser() old_section, old_key, old_value = old new_section, new_key, new_value = new old_env_var = test_conf._env_var_name(old_section, old_key) new_env_var = test_conf._env_var_name(new_section, new_key) - with mock.patch.dict("os.environ", **{old_env_var: old_value}): + env_patch = {old_env_var: old_value} + with mock.patch.dict("os.environ", env_patch): # Can't start with the new env var existing... os.environ.pop(new_env_var, None) - with pytest.warns(FutureWarning): - test_conf.validate() - assert test_conf.get(new_section, new_key) == new_value - # We also need to make sure the deprecated env var is removed - # so that any subprocesses don't use it in place of our updated - # value. - assert old_env_var not in os.environ - # and make sure we track the old value as well, under the new section/key - assert test_conf.upgraded_values[(new_section, new_key)] == old_value + with pytest.warns(DeprecationWarning, match="the old setting has been used"): + assert test_conf.get(new_section, new_key) == new_value @pytest.mark.parametrize( "conf_dict", @@ -1328,19 +1370,19 @@ def test_conf_as_dict_when_deprecated_value_in_config(self, display_source: bool include_env=False, include_cmds=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("mysql://", "airflow.cfg") if display_source else "mysql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("my_secret_key", "airflow.cfg") if display_source else "my_secret_key" ) - # database should be None because the deprecated value is set in config - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in config + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "mysql://" + assert conf.get("api", "secret_key") == "my_secret_key" @pytest.mark.parametrize("display_source", [True, False]) - @mock.patch.dict("os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN": "postgresql://"}, clear=True) + @mock.patch.dict("os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY": "env_secret_key"}, clear=True) def test_conf_as_dict_when_deprecated_value_in_both_env_and_config(self, display_source: bool): with use_config(config="deprecated.cfg"): cfg_dict = conf.as_dict( @@ -1350,19 +1392,19 @@ def test_conf_as_dict_when_deprecated_value_in_both_env_and_config(self, display include_env=True, include_cmds=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("postgresql://", "env var") if display_source else "postgresql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("env_secret_key", "env var") if display_source else "env_secret_key" ) - # database should be None because the deprecated value is set in env value - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in env value + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "postgresql://" + assert conf.get("api", "secret_key") == "env_secret_key" @pytest.mark.parametrize("display_source", [True, False]) - @mock.patch.dict("os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN": "postgresql://"}, clear=True) + @mock.patch.dict("os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY": "env_secret_key"}, clear=True) def test_conf_as_dict_when_deprecated_value_in_both_env_and_config_exclude_env( self, display_source: bool ): @@ -1374,52 +1416,51 @@ def test_conf_as_dict_when_deprecated_value_in_both_env_and_config_exclude_env( include_env=False, include_cmds=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("mysql://", "airflow.cfg") if display_source else "mysql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("my_secret_key", "airflow.cfg") if display_source else "my_secret_key" ) - # database should be None because the deprecated value is set in env value - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in config (env excluded) + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "mysql://" + assert conf.get("api", "secret_key") == "my_secret_key" @pytest.mark.parametrize("display_source", [True, False]) - @mock.patch.dict("os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN": "postgresql://"}, clear=True) + @mock.patch.dict("os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY": "env_secret_key"}, clear=True) def test_conf_as_dict_when_deprecated_value_in_env(self, display_source: bool): with use_config(config="empty.cfg"): cfg_dict = conf.as_dict( display_source=display_source, raw=True, display_sensitive=True, include_env=True ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("postgresql://", "env var") if display_source else "postgresql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("env_secret_key", "env var") if display_source else "env_secret_key" ) - # database should be None because the deprecated value is set in env value - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in env value + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "postgresql://" + assert conf.get("api", "secret_key") == "env_secret_key" @pytest.mark.parametrize("display_source", [True, False]) @mock.patch.dict("os.environ", {}, clear=True) def test_conf_as_dict_when_both_conf_and_env_are_empty(self, display_source: bool): + default_secret_key = conf.get_default_value("api", "secret_key") with use_config(config="empty.cfg"): cfg_dict = conf.as_dict(display_source=display_source, raw=True, display_sensitive=True) - assert cfg_dict["core"].get("sql_alchemy_conn") is None - # database should be taken from default because the deprecated value is missing in config - assert cfg_dict["database"].get("sql_alchemy_conn") == ( - (f"sqlite:///{HOME_DIR}/airflow/airflow.db", "default") - if display_source - else f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert cfg_dict.get("webserver", {}).get("secret_key") is None + # api should be taken from default because the deprecated value is missing in config + assert cfg_dict["api"].get("secret_key") == ( + (default_secret_key, "default") if display_source else default_secret_key ) if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert conf.get("api", "secret_key") == default_secret_key @pytest.mark.parametrize("display_source", [True, False]) @mock.patch.dict("os.environ", {}, clear=True) @@ -1432,20 +1473,20 @@ def test_conf_as_dict_when_deprecated_value_in_cmd_config(self, display_source: include_env=True, include_cmds=True, ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("postgresql://", "cmd") if display_source else "postgresql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("test_secret_key", "cmd") if display_source else "test_secret_key" ) - # database should be None because the deprecated value is set in env value - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in cmd + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "postgresql://" + assert conf.get("api", "secret_key") == "test_secret_key" @pytest.mark.parametrize("display_source", [True, False]) @mock.patch.dict( - "os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN_CMD": "echo -n 'postgresql://'"}, clear=True + "os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY_CMD": "echo -n 'test_secret_key'"}, clear=True ) def test_conf_as_dict_when_deprecated_value_in_cmd_env(self, display_source: bool): with use_config(config="empty.cfg"): @@ -1456,22 +1497,23 @@ def test_conf_as_dict_when_deprecated_value_in_cmd_env(self, display_source: boo include_env=True, include_cmds=True, ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("postgresql://", "cmd") if display_source else "postgresql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("test_secret_key", "cmd") if display_source else "test_secret_key" ) - # database should be None because the deprecated value is set in env value - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in cmd env + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "postgresql://" + assert conf.get("api", "secret_key") == "test_secret_key" @pytest.mark.parametrize("display_source", [True, False]) @mock.patch.dict( - "os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN_CMD": "echo -n 'postgresql://'"}, clear=True + "os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY_CMD": "echo -n 'test_secret_key'"}, clear=True ) def test_conf_as_dict_when_deprecated_value_in_cmd_disabled_env(self, display_source: bool): + default_secret_key = conf.get_default_value("api", "secret_key") with use_config(config="empty.cfg"): cfg_dict = conf.as_dict( display_source=display_source, @@ -1480,21 +1522,20 @@ def test_conf_as_dict_when_deprecated_value_in_cmd_disabled_env(self, display_so include_env=True, include_cmds=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") is None - assert cfg_dict["database"].get("sql_alchemy_conn") == ( - (f"sqlite:///{HOME_DIR}/airflow/airflow.db", "default") - if display_source - else f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert cfg_dict.get("webserver", {}).get("secret_key") is None + assert cfg_dict["api"].get("secret_key") == ( + (default_secret_key, "default") if display_source else default_secret_key ) if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert conf.get("api", "secret_key") == default_secret_key @pytest.mark.parametrize("display_source", [True, False]) @mock.patch.dict("os.environ", {}, clear=True) def test_conf_as_dict_when_deprecated_value_in_cmd_disabled_config(self, display_source: bool): + default_secret_key = conf.get_default_value("api", "secret_key") with use_config(config="deprecated_cmd.cfg"): cfg_dict = conf.as_dict( display_source=display_source, @@ -1503,25 +1544,23 @@ def test_conf_as_dict_when_deprecated_value_in_cmd_disabled_config(self, display include_env=True, include_cmds=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") is None - assert cfg_dict["database"].get("sql_alchemy_conn") == ( - (f"sqlite:///{HOME_DIR}/airflow/airflow.db", "default") - if display_source - else f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert cfg_dict.get("webserver", {}).get("secret_key") is None + assert cfg_dict["api"].get("secret_key") == ( + (default_secret_key, "default") if display_source else default_secret_key ) if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert conf.get("api", "secret_key") == default_secret_key @pytest.mark.parametrize("display_source", [True, False]) - @mock.patch.dict("os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN_SECRET": "secret_path'"}, clear=True) + @mock.patch.dict("os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY_SECRET": "secret_path"}, clear=True) @mock.patch("airflow.configuration.get_custom_secret_backend") def test_conf_as_dict_when_deprecated_value_in_secrets( self, get_custom_secret_backend, display_source: bool ): - get_custom_secret_backend.return_value.get_config.return_value = "postgresql://" + get_custom_secret_backend.return_value.get_config.return_value = "secret_from_backend" with use_config(config="empty.cfg"): cfg_dict = conf.as_dict( display_source=display_source, @@ -1530,24 +1569,25 @@ def test_conf_as_dict_when_deprecated_value_in_secrets( include_env=True, include_secret=True, ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("postgresql://", "secret") if display_source else "postgresql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("secret_from_backend", "secret") if display_source else "secret_from_backend" ) - # database should be None because the deprecated value is set in env value - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in secret + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "postgresql://" + assert conf.get("api", "secret_key") == "secret_from_backend" @pytest.mark.parametrize("display_source", [True, False]) - @mock.patch.dict("os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN_SECRET": "secret_path'"}, clear=True) + @mock.patch.dict("os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY_SECRET": "secret_path"}, clear=True) @mock.patch("airflow.configuration.get_custom_secret_backend") def test_conf_as_dict_when_deprecated_value_in_secrets_disabled_env( self, get_custom_secret_backend, display_source: bool ): - get_custom_secret_backend.return_value.get_config.return_value = "postgresql://" + default_secret_key = conf.get_default_value("api", "secret_key") + get_custom_secret_backend.return_value.get_config.return_value = "secret_from_backend" with use_config(config="empty.cfg"): cfg_dict = conf.as_dict( display_source=display_source, @@ -1556,17 +1596,15 @@ def test_conf_as_dict_when_deprecated_value_in_secrets_disabled_env( include_env=True, include_secret=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") is None - assert cfg_dict["database"].get("sql_alchemy_conn") == ( - (f"sqlite:///{HOME_DIR}/airflow/airflow.db", "default") - if display_source - else f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert cfg_dict.get("webserver", {}).get("secret_key") is None + assert cfg_dict["api"].get("secret_key") == ( + (default_secret_key, "default") if display_source else default_secret_key ) if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert conf.get("api", "secret_key") == default_secret_key @pytest.mark.parametrize("display_source", [True, False]) @mock.patch("airflow.configuration.get_custom_secret_backend") @@ -1574,7 +1612,8 @@ def test_conf_as_dict_when_deprecated_value_in_secrets_disabled_env( def test_conf_as_dict_when_deprecated_value_in_secrets_disabled_config( self, get_custom_secret_backend, display_source: bool ): - get_custom_secret_backend.return_value.get_config.return_value = "postgresql://" + default_secret_key = conf.get_default_value("api", "secret_key") + get_custom_secret_backend.return_value.get_config.return_value = "secret_from_backend" with use_config(config="deprecated_secret.cfg"): cfg_dict = conf.as_dict( display_source=display_source, @@ -1583,17 +1622,15 @@ def test_conf_as_dict_when_deprecated_value_in_secrets_disabled_config( include_env=True, include_secret=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") is None - assert cfg_dict["database"].get("sql_alchemy_conn") == ( - (f"sqlite:///{HOME_DIR}/airflow/airflow.db", "default") - if display_source - else f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert cfg_dict.get("webserver", {}).get("secret_key") is None + assert cfg_dict["api"].get("secret_key") == ( + (default_secret_key, "default") if display_source else default_secret_key ) if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert conf.get("api", "secret_key") == default_secret_key def test_as_dict_should_not_falsely_emit_future_warning(self): from airflow.configuration import AirflowConfigParser @@ -1815,7 +1852,6 @@ def test_sensitive_values(): ("sentry", "sentry_dsn"), ("database", "sql_alchemy_engine_args"), ("keycloak_auth_manager", "client_secret"), - ("core", "sql_alchemy_conn"), ("celery_broker_transport_options", "sentinel_kwargs"), ("celery", "broker_url"), ("celery", "flower_basic_auth"), diff --git a/airflow-core/tests/unit/jobs/test_scheduler_job.py b/airflow-core/tests/unit/jobs/test_scheduler_job.py index ae6388f1b8a01..7c8d2ff4ffba4 100644 --- a/airflow-core/tests/unit/jobs/test_scheduler_job.py +++ b/airflow-core/tests/unit/jobs/test_scheduler_job.py @@ -24,7 +24,7 @@ import re from collections import Counter, deque from collections.abc import Callable, Generator, Iterator -from contextlib import ExitStack +from contextlib import ExitStack, contextmanager from datetime import timedelta from pathlib import Path from typing import TYPE_CHECKING @@ -81,7 +81,6 @@ from airflow.models.taskinstance import TaskInstance from airflow.models.team import Team from airflow.models.trigger import Trigger -from airflow.observability.trace import Trace from airflow.partition_mappers.base import PartitionMapper as CorePartitionMapper from airflow.providers.standard.operators.bash import BashOperator from airflow.providers.standard.operators.empty import EmptyOperator @@ -93,9 +92,7 @@ from airflow.serialization.serialized_objects import LazyDeserializedDAG from airflow.timetables.base import DagRunInfo, DataInterval from airflow.utils.session import create_session, provide_session -from airflow.utils.span_status import SpanStatus from airflow.utils.state import CallbackState, DagRunState, State, TaskInstanceState -from airflow.utils.thread_safe_dict import ThreadSafeDict from airflow.utils.types import DagRunTriggeredByType, DagRunType from tests_common.pytest_plugin import AIRFLOW_ROOT_PATH @@ -2956,20 +2953,18 @@ def test_purge_without_heartbeat_skips_when_missing_dag_version(self, dag_maker, ti.state = TaskInstanceState.RUNNING ti.queued_by_job_id = scheduler_job.id ti.last_heartbeat_at = timezone.utcnow() - timedelta(hours=1) - # Simulate missing dag_version + # Simulate missing dag_version (legacy Airflow 2 task) ti.dag_version_id = None session.merge(ti) session.commit() - with caplog.at_level("WARNING", logger="airflow.jobs.scheduler_job_runner"): + with caplog.at_level("INFO", logger="airflow.jobs.scheduler_job_runner"): self.job_runner._purge_task_instances_without_heartbeats([ti], session=session) - # Should log a warning and skip processing - assert any("DAG Version not found for TaskInstance" in rec.message for rec in caplog.records) - mock_executor.send_callback.assert_not_called() - # State should be unchanged (not failed) - ti.refresh_from_db(session=session) - assert ti.state == TaskInstanceState.RUNNING + # dag_version_id should be backfilled from the latest DagVersion in the DB + # (dag_maker creates one) and the callback should be sent + assert any("Backfilled dag_version_id" in rec.message for rec in caplog.records) + mock_executor.send_callback.assert_called_once() @staticmethod def mock_failure_callback(context): @@ -3285,190 +3280,6 @@ def test_runs_are_created_after_max_active_runs_was_reached(self, dag_maker, ses dag_runs = DagRun.find(dag_id=dag.dag_id, session=session) assert len(dag_runs) == 2 - @pytest.mark.parametrize( - ("ti_state", "final_ti_span_status"), - [ - pytest.param(State.SUCCESS, SpanStatus.ENDED, id="dr_ended_successfully"), - pytest.param(State.RUNNING, SpanStatus.ACTIVE, id="dr_still_running"), - ], - ) - def test_recreate_unhealthy_scheduler_spans_if_needed(self, ti_state, final_ti_span_status, dag_maker): - with dag_maker( - dag_id="test_recreate_unhealthy_scheduler_spans_if_needed", - start_date=DEFAULT_DATE, - max_active_runs=1, - dagrun_timeout=datetime.timedelta(seconds=60), - ): - EmptyOperator(task_id="dummy") - - session = settings.Session() - - old_job = Job() - old_job.job_type = SchedulerJobRunner.job_type - - session.add(old_job) - session.commit() - - assert old_job.is_alive() is False - - new_job = Job() - new_job.job_type = SchedulerJobRunner.job_type - session.add(new_job) - session.flush() - - self.job_runner = SchedulerJobRunner(job=new_job) - self.job_runner.active_spans = ThreadSafeDict() - assert len(self.job_runner.active_spans.get_all()) == 0 - - dr = dag_maker.create_dagrun() - dr.state = State.RUNNING - dr.span_status = SpanStatus.ACTIVE - dr.scheduled_by_job_id = old_job.id - - ti = dr.get_task_instances(session=session)[0] - ti.state = ti_state - ti.start_date = timezone.utcnow() - ti.span_status = SpanStatus.ACTIVE - ti.queued_by_job_id = old_job.id - session.merge(ti) - session.merge(dr) - session.commit() - - assert dr.scheduled_by_job_id != self.job_runner.job.id - assert dr.scheduled_by_job_id == old_job.id - assert dr.run_id is not None - assert dr.state == State.RUNNING - assert dr.span_status == SpanStatus.ACTIVE - assert self.job_runner.active_spans.get("dr:" + str(dr.id)) is None - - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is None - assert ti.state == ti_state - assert ti.span_status == SpanStatus.ACTIVE - - self.job_runner._recreate_unhealthy_scheduler_spans_if_needed(dr, session) - - assert self.job_runner.active_spans.get("dr:" + str(dr.id)) is not None - - if final_ti_span_status == SpanStatus.ACTIVE: - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is not None - assert len(self.job_runner.active_spans.get_all()) == 2 - else: - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is None - assert len(self.job_runner.active_spans.get_all()) == 1 - - assert dr.span_status == SpanStatus.ACTIVE - assert ti.span_status == final_ti_span_status - - def test_end_spans_of_externally_ended_ops(self, dag_maker): - with dag_maker( - dag_id="test_end_spans_of_externally_ended_ops", - start_date=DEFAULT_DATE, - max_active_runs=1, - dagrun_timeout=datetime.timedelta(seconds=60), - ): - EmptyOperator(task_id="dummy") - - session = settings.Session() - - job = Job() - job.job_type = SchedulerJobRunner.job_type - session.add(job) - - self.job_runner = SchedulerJobRunner(job=job) - self.job_runner.active_spans = ThreadSafeDict() - assert len(self.job_runner.active_spans.get_all()) == 0 - - dr = dag_maker.create_dagrun() - dr.state = State.SUCCESS - dr.span_status = SpanStatus.SHOULD_END - - ti = dr.get_task_instances(session=session)[0] - ti.state = State.SUCCESS - ti.span_status = SpanStatus.SHOULD_END - ti.context_carrier = {} - session.merge(ti) - session.merge(dr) - session.commit() - - dr_span = Trace.start_root_span(span_name="dag_run_span", start_as_current=False) - ti_span = Trace.start_child_span(span_name="ti_span", start_as_current=False) - - self.job_runner.active_spans.set("dr:" + str(dr.id), dr_span) - self.job_runner.active_spans.set(f"ti:{ti.id}", ti_span) - - assert dr.span_status == SpanStatus.SHOULD_END - assert ti.span_status == SpanStatus.SHOULD_END - - assert self.job_runner.active_spans.get("dr:" + str(dr.id)) is not None - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is not None - - self.job_runner._end_spans_of_externally_ended_ops(session) - - assert dr.span_status == SpanStatus.ENDED - assert ti.span_status == SpanStatus.ENDED - - assert self.job_runner.active_spans.get("dr:" + str(dr.id)) is None - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is None - - @pytest.mark.parametrize( - ("state", "final_span_status"), - [ - pytest.param(State.SUCCESS, SpanStatus.ENDED, id="dr_ended_successfully"), - pytest.param(State.RUNNING, SpanStatus.NEEDS_CONTINUANCE, id="dr_still_running"), - ], - ) - def test_end_active_spans(self, state, final_span_status, dag_maker): - with dag_maker( - dag_id="test_end_active_spans", - start_date=DEFAULT_DATE, - max_active_runs=1, - dagrun_timeout=datetime.timedelta(seconds=60), - ): - EmptyOperator(task_id="dummy") - - session = settings.Session() - - job = Job() - job.job_type = SchedulerJobRunner.job_type - - self.job_runner = SchedulerJobRunner(job=job) - self.job_runner.active_spans = ThreadSafeDict() - assert len(self.job_runner.active_spans.get_all()) == 0 - - dr = dag_maker.create_dagrun() - dr.state = state - dr.span_status = SpanStatus.ACTIVE - - ti = dr.get_task_instances(session=session)[0] - ti.state = state - ti.span_status = SpanStatus.ACTIVE - ti.context_carrier = {} - session.merge(ti) - session.merge(dr) - session.commit() - - dr_span = Trace.start_root_span(span_name="dag_run_span", start_as_current=False) - ti_span = Trace.start_child_span(span_name="ti_span", start_as_current=False) - - self.job_runner.active_spans.set("dr:" + str(dr.id), dr_span) - self.job_runner.active_spans.set(f"ti:{ti.id}", ti_span) - - assert dr.span_status == SpanStatus.ACTIVE - assert ti.span_status == SpanStatus.ACTIVE - - assert self.job_runner.active_spans.get("dr:" + str(dr.id)) is not None - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is not None - assert len(self.job_runner.active_spans.get_all()) == 2 - - self.job_runner._end_active_spans(session) - - assert dr.span_status == final_span_status - assert ti.span_status == final_span_status - - assert self.job_runner.active_spans.get("dr:" + str(dr.id)) is None - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is None - assert len(self.job_runner.active_spans.get_all()) == 0 - def test_dagrun_timeout_verify_max_active_runs(self, dag_maker, session): """ Test if a dagrun will not be scheduled if max_dag_runs @@ -3524,7 +3335,7 @@ def test_dagrun_timeout_verify_max_active_runs(self, dag_maker, session): bundle_version=orm_dag.bundle_version, context_from_server=DagRunContext( dag_run=dr, - last_ti=dr.get_last_ti(dag, session), + last_ti=dr.get_task_instance("dummy", session), ), msg="timed_out", ) @@ -3720,7 +3531,7 @@ def test_dagrun_timeout_callbacks_are_stored_in_database(self, dag_maker, sessio bundle_version=None, context_from_server=DagRunContext( dag_run=dr, - last_ti=dr.get_last_ti(dag, session), + last_ti=dr.get_task_instance("empty", session), ), ) @@ -7551,6 +7362,37 @@ def test_misconfigured_dags_doesnt_crash_scheduler(self, mock_create, session, d job_runner._create_dag_runs([dm1], session) assert "Failed creating DagRun" in caplog.text + def test_activate_referenced_assets_no_in_check_inside_query(self, session, testing_dag_bundle): + dag_id1 = "test_asset_dag1" + asset1_name = "asset1" + asset_extra = {"foo": "bar"} + + asset1 = Asset(name=asset1_name, uri="s3://bucket/key/1", extra=asset_extra) + dag1 = DAG(dag_id=dag_id1, start_date=DEFAULT_DATE, schedule=[asset1]) + sync_dag_to_db(dag1, session=session) + + @contextmanager + def assert_no_in_clause(session): + from sqlalchemy import event + + def fail_on_in_clause_found(execute_statement): + if " IN " in str(execute_statement).upper(): + execute_statement = str(execute_statement).upper() + pytest.fail( + f"Query contains IN clause which was removed in PR #62114, query: {execute_statement}" + ) + + event.listen(session, "do_orm_execute", fail_on_in_clause_found) + try: + yield + finally: + event.remove(session, "do_orm_execute", fail_on_in_clause_found) + + asset_models = select(AssetModel).cte() + + with assert_no_in_clause(session): + SchedulerJobRunner._activate_referenced_assets(asset_models, session=session) + def test_activate_referenced_assets_with_no_existing_warning(self, session, testing_dag_bundle): dag_warnings = session.scalars(select(DagWarning)).all() assert dag_warnings == [] @@ -7565,8 +7407,8 @@ def test_activate_referenced_assets_with_no_existing_warning(self, session, test dag1 = DAG(dag_id=dag_id1, start_date=DEFAULT_DATE, schedule=[asset1, asset1_1, asset1_2]) sync_dag_to_db(dag1, session=session) - asset_models = session.scalars(select(AssetModel)).all() - assert len(asset_models) == 3 + asset_models = select(AssetModel).cte() + assert len(session.execute(select(asset_models)).all()) == 3 SchedulerJobRunner._activate_referenced_assets(asset_models, session=session) session.flush() @@ -7603,7 +7445,7 @@ def test_activate_referenced_assets_with_existing_warnings(self, session, testin ) session.flush() - asset_models = session.scalars(select(AssetModel)).all() + asset_models = select(AssetModel).cte() SchedulerJobRunner._activate_referenced_assets(asset_models, session=session) session.flush() @@ -7652,7 +7494,7 @@ def test_activate_referenced_assets_with_multiple_conflict_asset_in_one_dag( session.add(DagWarning(dag_id=dag_id, warning_type="asset conflict", message="will not exist")) session.flush() - asset_models = session.scalars(select(AssetModel)).all() + asset_models = select(AssetModel).cte() SchedulerJobRunner._activate_referenced_assets(asset_models, session=session) session.flush() @@ -9157,3 +8999,144 @@ def test_consumer_dag_listen_to_two_partitioned_asset_with_key_1_mapper( assert asset_event.source_task_id == "hi" assert "asset-event-producer-" in asset_event.source_dag_id assert asset_event.source_run_id == "test" + + +# --------------------------------------------------------------------------- +# Tests for nullable dag_version in scheduler callbacks (AIP-66) +# +# Verifies that TaskCallbackRequest and EmailRequest are always created +# with correct bundle_name/bundle_version whether ti.dag_version is a real +# DagVersion object (normal) or None (legacy tasks migrated from Airflow 2). +# +# Fallback mirrors DagCallbackRequest in dagrun.py: +# bundle_name <- ti.dag_version.bundle_name OR ti.dag_model.bundle_name +# bundle_version <- ti.dag_version.bundle_version OR ti.dag_run.bundle_version +# --------------------------------------------------------------------------- + + +def _make_ti_with_dag_version( + dag_version, dag_model_bundle_name="fallback-bundle", dag_run_bundle_version="v1.0-fallback" +): + """Build a minimal mock TaskInstance for dag_version nullable tests.""" + ti = mock.MagicMock() + ti.dag_version = dag_version + ti.dag_model = mock.MagicMock() + ti.dag_model.bundle_name = dag_model_bundle_name + ti.dag_model.relative_fileloc = "/dags/test_dag.py" + ti.dag_run = mock.MagicMock() + ti.dag_run.bundle_version = dag_run_bundle_version + return ti + + +def _make_dag_version(bundle_name="my-bundle", bundle_version="v2.0"): + """Create a simple mock DagVersion.""" + dv = mock.MagicMock() + dv.bundle_name = bundle_name + dv.bundle_version = bundle_version + return dv + + +def _extract_bundle_name(ti): + """Mirror the inline fallback logic from scheduler_job_runner.py.""" + return ti.dag_version.bundle_name if ti.dag_version else ti.dag_model.bundle_name + + +def _extract_bundle_version(ti): + """Mirror the inline fallback logic from scheduler_job_runner.py.""" + return ti.dag_version.bundle_version if ti.dag_version else ti.dag_run.bundle_version + + +class TestSchedulerCallbackBundleInfoDagVersionNullable: + """ + Verify the bundle_name / bundle_version extraction logic used at all four + TaskCallbackRequest / EmailRequest creation sites in scheduler_job_runner.py. + + When dag_version is present -> use dag_version.bundle_name / bundle_version. + When dag_version is None -> fall back to dag_model.bundle_name / dag_run.bundle_version. + """ + + # ── With dag_version present ────────────────────────────────────────── + + @pytest.mark.parametrize( + ("dv_bundle_name", "dv_bundle_version"), + [ + pytest.param("my-bundle", "v2.0", id="normal"), + pytest.param("dags-folder", None, id="version_none"), + pytest.param("custom-bundle", "v99.0", id="custom"), + ], + ) + def test_bundle_info_from_dag_version_when_present(self, dv_bundle_name, dv_bundle_version): + """When dag_version is set, bundle info must come from it.""" + dv = _make_dag_version(bundle_name=dv_bundle_name, bundle_version=dv_bundle_version) + ti = _make_ti_with_dag_version(dag_version=dv, dag_model_bundle_name="SHOULD-NOT-USE") + + assert _extract_bundle_name(ti) == dv_bundle_name + assert _extract_bundle_version(ti) == dv_bundle_version + + # ── With dag_version None (legacy Airflow 2 task) ───────────────────── + + @pytest.mark.parametrize( + ("model_bundle_name", "run_bundle_version"), + [ + pytest.param("fallback-bundle", "v1.0-fallback", id="normal_fallback"), + pytest.param("dags-folder", None, id="version_none_fallback"), + pytest.param("another-bundle", "v3.5", id="custom_fallback"), + ], + ) + def test_bundle_info_falls_back_when_dag_version_none(self, model_bundle_name, run_bundle_version): + """When dag_version is None, bundle info must fall back to dag_model / dag_run.""" + ti = _make_ti_with_dag_version( + dag_version=None, + dag_model_bundle_name=model_bundle_name, + dag_run_bundle_version=run_bundle_version, + ) + + assert _extract_bundle_name(ti) == model_bundle_name + assert _extract_bundle_version(ti) == run_bundle_version + + # ── No AttributeError crash ──────────────────────────────────────────── + + @pytest.mark.parametrize( + "dag_version_present", + [ + pytest.param(True, id="dag_version_present"), + pytest.param(False, id="dag_version_none"), + ], + ) + def test_no_attribute_error_regardless_of_dag_version(self, dag_version_present): + """ + The old code crashed with AttributeError when ti.dag_version was None. + The new fallback must never raise regardless of dag_version state. + """ + ti = _make_ti_with_dag_version(dag_version=_make_dag_version() if dag_version_present else None) + + name = _extract_bundle_name(ti) + version = _extract_bundle_version(ti) + + assert isinstance(name, str) + assert version is None or isinstance(version, str) + + # ── Precedence: dag_version wins over fallback ───────────────────────── + + def test_dag_version_takes_precedence_over_fallback_values(self): + """When dag_version is set, dag_model/dag_run fallbacks must NOT be used.""" + dv = _make_dag_version(bundle_name="preferred-bundle", bundle_version="preferred-v1") + ti = _make_ti_with_dag_version( + dag_version=dv, + dag_model_bundle_name="fallback-bundle", + dag_run_bundle_version="fallback-v1", + ) + + assert _extract_bundle_name(ti) == "preferred-bundle" + assert _extract_bundle_version(ti) == "preferred-v1" + + def test_fallback_values_used_only_when_dag_version_is_none(self): + """When dag_version is None, fallback values must be used.""" + ti = _make_ti_with_dag_version( + dag_version=None, + dag_model_bundle_name="fallback-bundle", + dag_run_bundle_version="fallback-v1", + ) + + assert _extract_bundle_name(ti) == "fallback-bundle" + assert _extract_bundle_version(ti) == "fallback-v1" diff --git a/airflow-core/tests/unit/models/test_cleartasks.py b/airflow-core/tests/unit/models/test_cleartasks.py index 202611610a4c9..54dffde7f41ad 100644 --- a/airflow-core/tests/unit/models/test_cleartasks.py +++ b/airflow-core/tests/unit/models/test_cleartasks.py @@ -738,3 +738,167 @@ def test_clear_task_instances_with_run_on_latest_version(self, run_on_latest_ver assert TaskInstanceState.REMOVED not in [ti.state for ti in dr.task_instances] for ti in dr.task_instances: assert ti.dag_version_id == old_dag_version.id + + def test_clear_only_new_tasks(self, dag_maker, session): + """Test that only_new queues only newly added tasks without clearing existing ones.""" + + with dag_maker( + "test_clear_new_task_instances", + bundle_version="v1", + ) as dag: + task0 = EmptyOperator(task_id="0") + task1 = EmptyOperator(task_id="1") + dr = dag_maker.create_dagrun( + state=State.RUNNING, + run_type=DagRunType.SCHEDULED, + ) + + old_dag_version = DagVersion.get_latest_version(dr.dag_id) + ti0, ti1 = sorted(dr.task_instances, key=lambda ti: ti.task_id) + ti0.refresh_from_task(dag.get_task("0")) + ti1.refresh_from_task(dag.get_task("1")) + + run_task_instance(ti0, task0) + run_task_instance(ti1, task1) + dr.state = DagRunState.SUCCESS + session.merge(dr) + session.flush() + + with dag_maker( + "test_clear_new_task_instances", + bundle_version="v2", + ) as dag: + EmptyOperator(task_id="0") + EmptyOperator(task_id="1") + EmptyOperator(task_id="2") + EmptyOperator(task_id="3") + + new_dag_version = DagVersion.get_latest_version(dag.dag_id) + + assert old_dag_version.id != new_dag_version.id + + count = dag.clear( + run_id=dr.run_id, + only_new=True, + session=session, + ) + assert count == 2 + + session.flush() + + updated_dr = session.scalar( + select(DagRun).where(DagRun.dag_id == dr.dag_id, DagRun.run_id == dr.run_id) + ) + + assert updated_dr.created_dag_version_id == new_dag_version.id + assert updated_dr.bundle_version == new_dag_version.bundle_version + + all_tis = sorted(updated_dr.task_instances, key=lambda ti: ti.task_id) + assert len(all_tis) == 4 + assert [ti.task_id for ti in all_tis] == ["0", "1", "2", "3"] + + ti0_after, ti1_after, ti2, ti3 = all_tis + assert ti0_after.state == TaskInstanceState.SUCCESS + assert ti1_after.state == TaskInstanceState.SUCCESS + + assert ti2.state is None + assert ti3.state is None + + for ti in all_tis: + assert ti.dag_version_id == new_dag_version.id + + def test_clear_only_new_tasks_dry_run(self, dag_maker, session): + """Test that only_new with dry_run returns new tasks and changes can be rolled back.""" + with dag_maker( + "test_clear_new_task_instances_dry_run", + bundle_version="v1", + ) as dag: + task0 = EmptyOperator(task_id="0") + task1 = EmptyOperator(task_id="1") + dr = dag_maker.create_dagrun( + state=State.RUNNING, + run_type=DagRunType.SCHEDULED, + ) + + old_dag_version = DagVersion.get_latest_version(dr.dag_id) + ti0, ti1 = sorted(dr.task_instances, key=lambda ti: ti.task_id) + ti0.refresh_from_task(dag.get_task("0")) + ti1.refresh_from_task(dag.get_task("1")) + + run_task_instance(ti0, task0) + run_task_instance(ti1, task1) + dr.state = DagRunState.SUCCESS + session.merge(dr) + session.flush() + + with dag_maker( + "test_clear_new_task_instances_dry_run", + bundle_version="v2", + ) as dag: + EmptyOperator(task_id="0") + EmptyOperator(task_id="1") + EmptyOperator(task_id="2") + EmptyOperator(task_id="3") + + new_dag_version = DagVersion.get_latest_version(dag.dag_id) + + assert old_dag_version.id != new_dag_version.id + + new_tis = dag.clear( + run_id=dr.run_id, + only_new=True, + dry_run=True, + session=session, + ) + + assert len(new_tis) == 2 + assert sorted(new_tis) == ["2", "3"] + + session.rollback() + dr.refresh_from_db(session) + + assert dr.created_dag_version_id == old_dag_version.id + assert len(dr.task_instances) == 2 # should be only the 2 earlier tasks + + def test_clear_only_new_no_new_tasks(self, dag_maker, session): + """Test that only_new returns 0 when no new tasks are added.""" + with dag_maker( + "test_clear_no_new_task_instances", + bundle_version="v1", + ) as dag: + task0 = EmptyOperator(task_id="0") + task1 = EmptyOperator(task_id="1") + dr = dag_maker.create_dagrun( + state=State.RUNNING, + run_type=DagRunType.SCHEDULED, + ) + + old_dag_version = DagVersion.get_latest_version(dr.dag_id) + ti0, ti1 = sorted(dr.task_instances, key=lambda ti: ti.task_id) + ti0.refresh_from_task(dag.get_task("0")) + ti1.refresh_from_task(dag.get_task("1")) + + run_task_instance(ti0, task0) + run_task_instance(ti1, task1) + dr.state = DagRunState.SUCCESS + session.merge(dr) + session.flush() + + with dag_maker( + "test_clear_no_new_task_instances", + bundle_version="v2", + ) as dag: + EmptyOperator(task_id="0") + EmptyOperator(task_id="1") + + new_dag_version = DagVersion.get_latest_version(dag.dag_id) + + assert old_dag_version.id != new_dag_version.id + + count = dag.clear( + run_id=dr.run_id, + only_new=True, + session=session, + ) + + assert count == 0 diff --git a/airflow-core/tests/unit/models/test_dag.py b/airflow-core/tests/unit/models/test_dag.py index e801fb579369a..046c85ea79901 100644 --- a/airflow-core/tests/unit/models/test_dag.py +++ b/airflow-core/tests/unit/models/test_dag.py @@ -922,8 +922,8 @@ def test_dag_handle_callback_crash(self, mock_stats, testing_dag_bundle): ) # should not raise any exception - dag_run.handle_dag_callback(dag=dag, success=False) - dag_run.handle_dag_callback(dag=dag, success=True) + dag_run.execute_dag_callbacks(dag=dag, success=False) + dag_run.execute_dag_callbacks(dag=dag, success=True) mock_stats.incr.assert_called_with( "dag.callback_exceptions", @@ -963,8 +963,8 @@ def test_dag_handle_callback_with_removed_task(self, dag_maker, session, testing assert dag_run.get_task_instance(task_removed.task_id).state == TaskInstanceState.REMOVED # should not raise any exception - dag_run.handle_dag_callback(dag=dag, success=False) - dag_run.handle_dag_callback(dag=dag, success=True) + dag_run.execute_dag_callbacks(dag=dag, success=False) + dag_run.execute_dag_callbacks(dag=dag, success=True) @time_machine.travel(timezone.datetime(2025, 11, 11)) @pytest.mark.parametrize(("catchup", "expected_next_dagrun"), [(True, DEFAULT_DATE), (False, None)]) diff --git a/airflow-core/tests/unit/models/test_dagrun.py b/airflow-core/tests/unit/models/test_dagrun.py index 3e62554901c30..14722f83b0cce 100644 --- a/airflow-core/tests/unit/models/test_dagrun.py +++ b/airflow-core/tests/unit/models/test_dagrun.py @@ -23,10 +23,11 @@ from functools import reduce from typing import TYPE_CHECKING from unittest import mock -from unittest.mock import call +from unittest.mock import ANY, call import pendulum import pytest +from opentelemetry.sdk.trace import TracerProvider from sqlalchemy import func, select from sqlalchemy.orm import joinedload @@ -54,9 +55,7 @@ from airflow.settings import get_policy_plugin_manager from airflow.task.trigger_rule import TriggerRule from airflow.triggers.base import StartTriggerArgs -from airflow.utils.span_status import SpanStatus from airflow.utils.state import DagRunState, State, TaskInstanceState -from airflow.utils.thread_safe_dict import ThreadSafeDict from airflow.utils.types import DagRunTriggeredByType, DagRunType from tests_common.test_utils import db @@ -434,9 +433,16 @@ def on_success_callable(context): } dag_run = self.create_dag_run(dag=dag, task_states=initial_task_states, session=session) - with mock.patch.object(dag_run, "handle_dag_callback") as handle_dag_callback: + with mock.patch.object(dag_run, "execute_dag_callbacks") as execute_dag_callbacks: _, callback = dag_run.update_state() - assert handle_dag_callback.mock_calls == [mock.call(dag=dag, success=True, reason="success")] + assert execute_dag_callbacks.mock_calls == [ + mock.call(dag=dag, success=True, relevant_ti=ANY, reason="success") + ] + # Make sure the correct TI is passed on success + call_args = execute_dag_callbacks.call_args + ti_passed = call_args.kwargs["relevant_ti"] + assert ti_passed.task_id == "test_state_succeeded2" + assert dag_run.state == DagRunState.SUCCESS # Callbacks are not added until handle_callback = False is passed to dag_run.update_state() assert callback is None @@ -461,13 +467,62 @@ def on_failure_callable(context): dag_task1.set_downstream(dag_task2) dag_run = self.create_dag_run(dag=dag, task_states=initial_task_states, session=session) - with mock.patch.object(dag_run, "handle_dag_callback") as handle_dag_callback: + with mock.patch.object(dag_run, "execute_dag_callbacks") as execute_dag_callbacks: _, callback = dag_run.update_state() - assert handle_dag_callback.mock_calls == [mock.call(dag=dag, success=False, reason="task_failure")] + assert execute_dag_callbacks.mock_calls == [ + mock.call(dag=dag, success=False, relevant_ti=ANY, reason="task_failure") + ] + # Make sure the correct TI is passed on failure + call_args = execute_dag_callbacks.call_args + ti_passed = call_args.kwargs["relevant_ti"] + assert ti_passed.task_id == "test_state_failed2" + assert dag_run.state == DagRunState.FAILED # Callbacks are not added until handle_callback = False is passed to dag_run.update_state() assert callback is None + def test_dagrun_failure_callback_on_tasks_deadlocked(self, dag_maker, session): + def on_failure_callable(context): + assert context["dag_run"].dag_id == "test_dagrun_failure_callback_on_tasks_deadlocked" + + with dag_maker( + dag_id="test_dagrun_failure_callback_on_tasks_deadlocked", + schedule=datetime.timedelta(days=1), + start_date=datetime.datetime(2017, 1, 1), + on_failure_callback=on_failure_callable, + ): + up = EmptyOperator(task_id="upstream") + middle = EmptyOperator(task_id="wrong") + down = EmptyOperator(task_id="downstream") + + middle.trigger_rule = TriggerRule.ONE_FAILED + middle.set_upstream(up) + middle.set_downstream(down) + + dr = dag_maker.create_dagrun() + + ti_up: TI = dr.get_task_instance(task_id=up.task_id, session=session) + ti_middle: TI = dr.get_task_instance(task_id=middle.task_id, session=session) + ti_up.set_state(state=TaskInstanceState.SUCCESS, session=session) + ti_middle.set_state(state=None, session=session) + ti_middle.task.trigger_rule = "invalid" + + serialized_dag = dr.get_dag() + + with mock.patch.object(dr, "execute_dag_callbacks") as execute_dag_callbacks: + _, callback = dr.update_state(execute_callbacks=True) + assert execute_dag_callbacks.mock_calls == [ + mock.call(dag=serialized_dag, success=False, relevant_ti=ti_middle, reason="all_tasks_deadlocked") + ] + # Make sure the correct TI is passed on deadlock + call_args = execute_dag_callbacks.call_args + ti_passed = call_args.kwargs["relevant_ti"] + assert ti_passed.task_id == "wrong" + + assert dr.state == DagRunState.FAILED + # Callbacks is None as execute_callbacks=True + assert callback is None + def test_on_success_callback_when_task_skipped(self, session, testing_dag_bundle): mock_on_success = mock.MagicMock() mock_on_success.__name__ = "mock_on_success" @@ -504,142 +559,6 @@ def test_on_success_callback_when_task_skipped(self, session, testing_dag_bundle assert dag_run.state == DagRunState.SUCCESS mock_on_success.assert_called_once() - def test_start_dr_spans_if_needed_new_span(self, dag_maker, session): - with dag_maker( - dag_id="test_start_dr_spans_if_needed_new_span", - schedule=datetime.timedelta(days=1), - start_date=datetime.datetime(2017, 1, 1), - ) as dag: - dag_task1 = EmptyOperator(task_id="test_task1") - dag_task2 = EmptyOperator(task_id="test_task2") - dag_task1.set_downstream(dag_task2) - - initial_task_states = { - "test_task1": TaskInstanceState.QUEUED, - "test_task2": TaskInstanceState.QUEUED, - } - - dag_run = self.create_dag_run(dag=dag, task_states=initial_task_states, session=session) - - active_spans = ThreadSafeDict() - dag_run.set_active_spans(active_spans) - - tis = dag_run.get_task_instances() - - assert dag_run.active_spans is not None - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is None - assert dag_run.span_status == SpanStatus.NOT_STARTED - - dag_run.start_dr_spans_if_needed(tis=tis) - - assert dag_run.span_status == SpanStatus.ACTIVE - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is not None - - def test_start_dr_spans_if_needed_span_with_continuance(self, dag_maker, session): - with dag_maker( - dag_id="test_start_dr_spans_if_needed_span_with_continuance", - schedule=datetime.timedelta(days=1), - start_date=datetime.datetime(2017, 1, 1), - ) as dag: - dag_task1 = EmptyOperator(task_id="test_task1") - dag_task2 = EmptyOperator(task_id="test_task2") - dag_task1.set_downstream(dag_task2) - - initial_task_states = { - "test_task1": TaskInstanceState.RUNNING, - "test_task2": TaskInstanceState.QUEUED, - } - - dag_run = self.create_dag_run(dag=dag, task_states=initial_task_states, session=session) - - active_spans = ThreadSafeDict() - dag_run.set_active_spans(active_spans) - - dag_run.span_status = SpanStatus.NEEDS_CONTINUANCE - - tis = dag_run.get_task_instances() - - first_ti = tis[0] - first_ti.span_status = SpanStatus.NEEDS_CONTINUANCE - - assert dag_run.active_spans is not None - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is None - assert dag_run.active_spans.get(f"ti:{first_ti.id}") is None - assert dag_run.span_status == SpanStatus.NEEDS_CONTINUANCE - assert first_ti.span_status == SpanStatus.NEEDS_CONTINUANCE - - dag_run.start_dr_spans_if_needed(tis=tis) - - assert dag_run.span_status == SpanStatus.ACTIVE - assert first_ti.span_status == SpanStatus.ACTIVE - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is not None - assert dag_run.active_spans.get(f"ti:{first_ti.id}") is not None - - def test_end_dr_span_if_needed(self, testing_dag_bundle, dag_maker, session): - with dag_maker( - dag_id="test_end_dr_span_if_needed", - schedule=datetime.timedelta(days=1), - start_date=datetime.datetime(2017, 1, 1), - ) as dag: - dag_task1 = EmptyOperator(task_id="test_task1") - dag_task2 = EmptyOperator(task_id="test_task2") - dag_task1.set_downstream(dag_task2) - - initial_task_states = { - "test_task1": TaskInstanceState.SUCCESS, - "test_task2": TaskInstanceState.SUCCESS, - } - - dag_run = self.create_dag_run(dag=dag, task_states=initial_task_states, session=session) - - active_spans = ThreadSafeDict() - dag_run.set_active_spans(active_spans) - - from airflow.observability.trace import Trace - - dr_span = Trace.start_root_span(span_name="test_span", start_as_current=False) - - active_spans.set("dr:" + str(dag_run.id), dr_span) - - assert dag_run.active_spans is not None - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is not None - - dag_run.end_dr_span_if_needed() - - assert dag_run.span_status == SpanStatus.ENDED - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is None - - def test_end_dr_span_if_needed_with_span_from_another_scheduler( - self, testing_dag_bundle, dag_maker, session - ): - with dag_maker( - dag_id="test_end_dr_span_if_needed_with_span_from_another_scheduler", - schedule=datetime.timedelta(days=1), - start_date=datetime.datetime(2017, 1, 1), - ) as dag: - dag_task1 = EmptyOperator(task_id="test_task1") - dag_task2 = EmptyOperator(task_id="test_task2") - dag_task1.set_downstream(dag_task2) - - initial_task_states = { - "test_task1": TaskInstanceState.SUCCESS, - "test_task2": TaskInstanceState.SUCCESS, - } - - dag_run = self.create_dag_run(dag=dag, task_states=initial_task_states, session=session) - - active_spans = ThreadSafeDict() - dag_run.set_active_spans(active_spans) - - dag_run.span_status = SpanStatus.ACTIVE - - assert dag_run.active_spans is not None - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is None - - dag_run.end_dr_span_if_needed() - - assert dag_run.span_status == SpanStatus.SHOULD_END - def test_dagrun_update_state_with_handle_callback_success(self, testing_dag_bundle, dag_maker, session): def on_success_callable(context): assert context["dag_run"].dag_id == "test_dagrun_update_state_with_handle_callback_success" @@ -682,13 +601,12 @@ def on_success_callable(context): bundle_version=None, context_from_server=DagRunContext( dag_run=dag_run, - last_ti=dag_run.get_last_ti(dag, session), + last_ti=dag_run.get_task_instance(task_id="test_state_succeeded2"), ), msg="success", ) def test_dagrun_update_state_with_handle_callback_failure(self, testing_dag_bundle, dag_maker, session): - def on_failure_callable(context): assert context["dag_run"].dag_id == "test_dagrun_update_state_with_handle_callback_failure" @@ -732,7 +650,7 @@ def on_failure_callable(context): bundle_version=None, context_from_server=DagRunContext( dag_run=dag_run, - last_ti=dag_run.get_last_ti(dag, session), + last_ti=dag_run.get_task_instance(task_id="test_state_failed2"), ), ) @@ -1339,11 +1257,16 @@ def on_success_callable(context): ) dag_run.dag = scheduler_dag - with mock.patch.object(dag_run, "handle_dag_callback") as handle_dag_callback: + with mock.patch.object(dag_run, "execute_dag_callbacks") as execute_dag_callbacks: _, callback = dag_run.update_state() - assert handle_dag_callback.mock_calls == [ - mock.call(dag=scheduler_dag, success=True, reason="success") + assert execute_dag_callbacks.mock_calls == [ + mock.call(dag=scheduler_dag, success=True, relevant_ti=ANY, reason="success") ] + # Make sure the correct TI is passed on success + call_args = execute_dag_callbacks.call_args + ti_passed = call_args.kwargs["relevant_ti"] + assert ti_passed.task_id == "task_2" + assert dag_run.state == DagRunState.SUCCESS # Callbacks are not added until handle_callback = False is passed to dag_run.update_state() assert callback is None @@ -3107,103 +3030,11 @@ def my_teardown(): } -class TestDagRunGetLastTi: - def test_get_last_ti_with_multiple_tis(self, dag_maker, session): - """Test get_last_ti returns the last TI (first created) when multiple TIs exist""" - with dag_maker("test_dag", session=session) as dag: - BashOperator(task_id="task1", bash_command="echo 1") - BashOperator(task_id="task2", bash_command="echo 2") - BashOperator(task_id="task3", bash_command="echo 3") - - dr = dag_maker.create_dagrun() - - tis = dr.get_task_instances(session=session) - assert len(tis) == 3 - - # Mark some TIs with different states - tis[0].state = TaskInstanceState.SUCCESS - tis[1].state = TaskInstanceState.FAILED - tis[2].state = TaskInstanceState.RUNNING - session.commit() - - last_ti = dr.get_last_ti(dag, session=session) - - # Should return the last TI in the list (index -1) - assert last_ti is not None - assert last_ti == tis[-1] - assert last_ti.task_id == "task3" - - def test_get_last_ti_filters_none_state_in_partial_dag(self, dag_maker, session): - """Test get_last_ti filters out NONE state TIs when dag is partial""" - with dag_maker("test_dag", session=session) as dag: - BashOperator(task_id="task1", bash_command="echo 1") - BashOperator(task_id="task2", bash_command="echo 2") - - dr = dag_maker.create_dagrun() - - dag.partial = True - - # Create task instances with different states - tis = dr.get_task_instances(session=session) - tis[0].state = State.NONE # Should be filtered out in partial DAG - tis[1].state = TaskInstanceState.RUNNING - session.commit() - - last_ti = dr.get_last_ti(dag, session=session) - - assert last_ti is not None - assert last_ti.state != State.NONE - assert last_ti.task_id == "task2" - - def test_get_last_ti_filters_removed_tasks(self, dag_maker, session): - """Test get_last_ti filters out REMOVED task instances""" - with dag_maker("test_dag", session=session) as dag: - BashOperator(task_id="task1", bash_command="echo 1") - BashOperator(task_id="task2", bash_command="echo 2") - BashOperator(task_id="task3", bash_command="echo 3") - - dr = dag_maker.create_dagrun() - - tis = dr.get_task_instances(session=session) - assert len(tis) == 3 - - ti_by_id = {ti.task_id: ti for ti in tis} - - # Mark some TIs as removed - ti_by_id["task1"].state = TaskInstanceState.REMOVED - ti_by_id["task2"].state = TaskInstanceState.REMOVED - ti_by_id["task3"].state = TaskInstanceState.SUCCESS - session.commit() - - last_ti = dr.get_last_ti(dag, session=session) - - # Should return the TI that is not REMOVED - assert last_ti is not None - assert last_ti.state != TaskInstanceState.REMOVED - assert last_ti.task_id == "task3" - - def test_get_last_ti_with_single_ti(self, dag_maker, session): - """Test get_last_ti works with single task instance""" - with dag_maker("test_dag", session=session) as dag: - BashOperator(task_id="single_task", bash_command="echo 1") - - dr = dag_maker.create_dagrun() - - tis = dr.get_task_instances(session=session) - assert len(tis) == 1 - - last_ti = dr.get_last_ti(dag, session=session) - - assert last_ti is not None - assert last_ti == tis[0] - assert last_ti.task_id == "single_task" - - class TestDagRunHandleDagCallback: - """Test the handle_dag_callback method (only uses in dag.test).""" + """Test the execute_dag_callbacks method (only uses in dag.test).""" - def test_handle_dag_callback_success(self, dag_maker, session): - """Test handle_dag_callback executes success callback with RuntimeTaskInstance context""" + def test_execute_dag_callbacks_success(self, dag_maker, session): + """Test execute_dag_callbacks executes success callback with RuntimeTaskInstance context""" called = False context_received = None @@ -3220,7 +3051,9 @@ def on_success(context): dag.on_success_callback = on_success dag.has_on_success_callback = True - dr.handle_dag_callback(dag, success=True, reason="test_success") + dr.execute_dag_callbacks( + dag, success=True, relevant_ti=dr.get_task_instance("test_task"), reason="test_success" + ) assert called is True assert context_received is not None @@ -3232,8 +3065,8 @@ def on_success(context): assert "ts" in context_received assert "params" in context_received - def test_handle_dag_callback_failure(self, dag_maker, session): - """Test handle_dag_callback executes failure callback with RuntimeTaskInstance context""" + def test_execute_dag_callbacks_failure(self, dag_maker, session): + """Test execute_dag_callbacks executes failure callback with RuntimeTaskInstance context""" called = False context_received = None @@ -3250,7 +3083,9 @@ def on_failure(context): dag.on_failure_callback = on_failure dag.has_on_failure_callback = True - dr.handle_dag_callback(dag, success=False, reason="test_failure") + dr.execute_dag_callbacks( + dag, success=False, relevant_ti=dr.get_task_instance("test_task"), reason="test_failure" + ) assert called is True assert context_received is not None @@ -3262,8 +3097,8 @@ def on_failure(context): assert "ts" in context_received assert "params" in context_received - def test_handle_dag_callback_multiple_callbacks(self, dag_maker, session): - """Test handle_dag_callback executes multiple callbacks""" + def test_execute_dag_callbacks_multiple_callbacks(self, dag_maker, session): + """Test execute_dag_callbacks executes multiple callbacks""" call_count = 0 def on_failure_1(context): @@ -3282,12 +3117,17 @@ def on_failure_2(context): dag.on_failure_callback = [on_failure_1, on_failure_2] dag.has_on_failure_callback = True - dr.handle_dag_callback(dag, success=False, reason="test_failure") + dr.execute_dag_callbacks( + dag, + success=False, + relevant_ti=dr.get_task_instance("test_task"), + reason="test_failure", + ) assert call_count == 2 - def test_handle_dag_callback_context_has_correct_ti_info(self, dag_maker, session): - """Test handle_dag_callback context contains correct task instance information""" + def test_execute_dag_callbacks_context_has_correct_ti_info(self, dag_maker, session): + """Test execute_dag_callbacks context contains correct task instance information""" context_received = None def on_failure(context): @@ -3302,10 +3142,171 @@ def on_failure(context): dag.on_failure_callback = on_failure dag.has_on_failure_callback = True - dr.handle_dag_callback(dag, success=False, reason="test_failure") + dr.execute_dag_callbacks( + dag, + success=False, + relevant_ti=dr.get_task_instance("test_task"), + reason="test_failure", + ) assert context_received is not None # Check that context contains correct task info assert context_received["ti"].task_id == "test_task" assert context_received["ti"].dag_id == "test_dag" assert context_received["ti"].run_id == dr.run_id + + +class TestDagRunTracing: + """Tests for DagRun OpenTelemetry span behavior.""" + + @pytest.fixture(autouse=True) + def sdk_tracer_provider(self): + """Patch the module-level tracer with one backed by a real SDK provider so spans have valid IDs.""" + provider = TracerProvider() + real_tracer = provider.get_tracer("airflow.models.dagrun") + with mock.patch("airflow.models.dagrun.tracer", real_tracer): + yield + + def test_context_carrier_set_on_init(self, dag_maker): + """DagRun.__init__ should populate context_carrier with a W3C traceparent.""" + with dag_maker("test_tracing_init"): + EmptyOperator(task_id="t1") + dr = dag_maker.create_dagrun() + + assert dr.context_carrier is not None + assert isinstance(dr.context_carrier, dict) + assert "traceparent" in dr.context_carrier + + def test_context_carrier_unique_per_dagrun(self, dag_maker): + """Each DagRun should get a distinct trace context.""" + with dag_maker("test_tracing_unique1"): + EmptyOperator(task_id="t1") + dr1 = dag_maker.create_dagrun() + + with dag_maker("test_tracing_unique2"): + EmptyOperator(task_id="t1") + dr2 = dag_maker.create_dagrun() + + assert dr1.context_carrier["traceparent"] != dr2.context_carrier["traceparent"] + + @pytest.mark.parametrize("final_state", [DagRunState.SUCCESS, DagRunState.FAILED]) + def test_emit_dagrun_span_called_on_completion(self, dag_maker, session, final_state): + """_emit_dagrun_span should be called exactly once when a dag run finishes.""" + with dag_maker("test_tracing_emit", session=session) as dag: + EmptyOperator(task_id="t1") + + dr = dag_maker.create_dagrun(state=DagRunState.RUNNING) + ti = dr.get_task_instance("t1", session=session) + ti.state = ( + TaskInstanceState.SUCCESS if final_state == DagRunState.SUCCESS else TaskInstanceState.FAILED + ) + session.flush() + + dr.dag = dag + + with mock.patch.object(dr, "_emit_dagrun_span") as mock_emit: + dr.update_state(session=session) + + mock_emit.assert_called_once_with(state=final_state) + + def test_emit_dagrun_span_not_called_while_running(self, dag_maker, session): + """_emit_dagrun_span should not be called while the dag run is still running.""" + with dag_maker("test_tracing_no_emit_running", session=session) as dag: + EmptyOperator(task_id="t1") + EmptyOperator(task_id="t2") + + dr = dag_maker.create_dagrun(state=DagRunState.RUNNING) + tis = dr.get_task_instances(session=session) + for ti in tis: + if ti.task_id == "t1": + ti.state = TaskInstanceState.SUCCESS + else: + ti.state = TaskInstanceState.RUNNING + session.flush() + + dr.dag = dag + + with mock.patch.object(dr, "_emit_dagrun_span") as mock_emit: + dr.update_state(session=session) + + mock_emit.assert_not_called() + + def test_emit_dagrun_span_uses_context_carrier_ids(self, dag_maker, session): + """The emitted span should inherit trace_id/span_id from the context_carrier.""" + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + + from airflow.observability.traces import OverrideableRandomIdGenerator + + in_mem_exporter = InMemorySpanExporter() + provider = TracerProvider(id_generator=OverrideableRandomIdGenerator()) + provider.add_span_processor(SimpleSpanProcessor(in_mem_exporter)) + test_tracer = provider.get_tracer("test") + + with dag_maker("test_tracing_ids", session=session) as dag: + EmptyOperator(task_id="t1") + + dr = dag_maker.create_dagrun(state=DagRunState.RUNNING) + ti = dr.get_task_instance("t1", session=session) + ti.state = TaskInstanceState.SUCCESS + session.flush() + dr.dag = dag + + with mock.patch("airflow.models.dagrun.tracer", test_tracer): + dr.update_state(session=session) + + spans = in_mem_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + # Decode the expected trace_id/span_id from the stored context_carrier + ctx = TraceContextTextMapPropagator().extract(dr.context_carrier) + from opentelemetry import trace as otel_trace + + stored_span = otel_trace.get_current_span(context=ctx) + stored_ctx = stored_span.get_span_context() + + assert span.context.trace_id == stored_ctx.trace_id + assert span.context.span_id == stored_ctx.span_id + + @pytest.mark.parametrize("final_state", [DagRunState.SUCCESS, DagRunState.FAILED]) + def test_emit_dagrun_span_attributes_and_status(self, dag_maker, session, final_state): + """The emitted span should have the correct name, attributes, and status code.""" + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + from opentelemetry.trace import StatusCode + + from airflow.observability.traces import OverrideableRandomIdGenerator + + in_mem_exporter = InMemorySpanExporter() + provider = TracerProvider(id_generator=OverrideableRandomIdGenerator()) + provider.add_span_processor(SimpleSpanProcessor(in_mem_exporter)) + test_tracer = provider.get_tracer("test") + + with dag_maker("test_tracing_attrs", session=session) as dag: + EmptyOperator(task_id="t1") + + dr = dag_maker.create_dagrun(state=DagRunState.RUNNING) + ti = dr.get_task_instance("t1", session=session) + ti.state = ( + TaskInstanceState.SUCCESS if final_state == DagRunState.SUCCESS else TaskInstanceState.FAILED + ) + session.flush() + dr.dag = dag + + with mock.patch("airflow.models.dagrun.tracer", test_tracer): + dr.update_state(session=session) + + spans = in_mem_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == f"dag_run.{dr.dag_id}" + assert span.attributes["airflow.dag_id"] == dr.dag_id + assert span.attributes["airflow.dag_run.run_id"] == dr.run_id + + expected_status = StatusCode.OK if final_state == DagRunState.SUCCESS else StatusCode.ERROR + assert span.status.status_code == expected_status diff --git a/airflow-core/tests/unit/models/test_deadline.py b/airflow-core/tests/unit/models/test_deadline.py index 379998efbb88c..94c6977ae0c16 100644 --- a/airflow-core/tests/unit/models/test_deadline.py +++ b/airflow-core/tests/unit/models/test_deadline.py @@ -28,11 +28,20 @@ from airflow.api_fastapi.core_api.datamodels.dag_run import DAGRunResponse from airflow.models import DagRun -from airflow.models.deadline import Deadline, ReferenceModels, _fetch_from_db +from airflow.models.deadline import Deadline, _fetch_from_db from airflow.providers.standard.operators.empty import EmptyOperator from airflow.sdk import timezone from airflow.sdk.definitions.callback import AsyncCallback, SyncCallback -from airflow.sdk.definitions.deadline import DeadlineReference, deadline_reference +from airflow.sdk.definitions.deadline import ( + AverageRuntimeDeadline, + BaseDeadlineReference, + DagRunLogicalDateDeadline, + DagRunQueuedAtDeadline, + DeadlineReference, + FixedDatetimeDeadline, + deadline_reference, +) +from airflow.serialization.definitions.deadline import SerializedReferenceModels from airflow.utils.state import DagRunState from tests_common.test_utils import db @@ -46,10 +55,12 @@ INVALID_RUN_ID = -1 REFERENCE_TYPES = [ - pytest.param(DeadlineReference.DAGRUN_LOGICAL_DATE, id="logical_date"), - pytest.param(DeadlineReference.DAGRUN_QUEUED_AT, id="queued_at"), - pytest.param(DeadlineReference.FIXED_DATETIME(DEFAULT_DATE), id="fixed_deadline"), - pytest.param(DeadlineReference.AVERAGE_RUNTIME(), id="average_runtime"), + pytest.param(SerializedReferenceModels.DagRunLogicalDateDeadline(), id="logical_date"), + pytest.param(SerializedReferenceModels.DagRunQueuedAtDeadline(), id="queued_at"), + pytest.param(SerializedReferenceModels.FixedDatetimeDeadline(DEFAULT_DATE), id="fixed_deadline"), + pytest.param( + SerializedReferenceModels.AverageRuntimeDeadline(max_runs=10, min_runs=10), id="average_runtime" + ), ] @@ -166,7 +177,7 @@ def test_prune_deadlines(self, mock_session, conditions, dagrun): mock_session.execute.return_value.all.assert_called_once() mock_session.delete.assert_called_once_with(mock_deadline) else: - mock_session.query.assert_not_called() + mock_session.execute.assert_not_called() def test_repr_with_callback_kwargs(self, deadline_orm, dagrun): repr_str = repr(deadline_orm) @@ -320,10 +331,20 @@ def test_fetch_from_db_error_cases( @pytest.mark.parametrize( ("reference", "expected_column"), [ - pytest.param(DeadlineReference.DAGRUN_LOGICAL_DATE, DagRun.logical_date, id="logical_date"), - pytest.param(DeadlineReference.DAGRUN_QUEUED_AT, DagRun.queued_at, id="queued_at"), - pytest.param(DeadlineReference.FIXED_DATETIME(DEFAULT_DATE), None, id="fixed_deadline"), - pytest.param(DeadlineReference.AVERAGE_RUNTIME(), None, id="average_runtime"), + pytest.param( + SerializedReferenceModels.DagRunLogicalDateDeadline(), DagRun.logical_date, id="logical_date" + ), + pytest.param( + SerializedReferenceModels.DagRunQueuedAtDeadline(), DagRun.queued_at, id="queued_at" + ), + pytest.param( + SerializedReferenceModels.FixedDatetimeDeadline(DEFAULT_DATE), None, id="fixed_deadline" + ), + pytest.param( + SerializedReferenceModels.AverageRuntimeDeadline(max_runs=10, min_runs=10), + None, + id="average_runtime", + ), ], ) def test_deadline_database_integration(self, reference, expected_column, session): @@ -337,13 +358,13 @@ def test_deadline_database_integration(self, reference, expected_column, session """ conditions = {"dag_id": DAG_ID, "run_id": "dagrun_1"} interval = timedelta(hours=1) - with mock.patch("airflow.models.deadline._fetch_from_db") as mock_fetch: + with mock.patch("airflow.serialization.definitions.deadline._fetch_from_db") as mock_fetch: mock_fetch.return_value = DEFAULT_DATE if expected_column is not None: result = reference.evaluate_with(session=session, interval=interval, **conditions) mock_fetch.assert_called_once_with(expected_column, session=session, **conditions) - elif reference == DeadlineReference.AVERAGE_RUNTIME(): + elif isinstance(reference, SerializedReferenceModels.AverageRuntimeDeadline): with mock.patch("airflow._shared.timezones.timezone.utcnow") as mock_utcnow: mock_utcnow.return_value = DEFAULT_DATE # No DAG runs exist, so it should use 24-hour default @@ -380,7 +401,7 @@ def test_average_runtime_with_sufficient_history(self, session, dag_maker): session.commit() # Test with default max_runs (10) - reference = DeadlineReference.AVERAGE_RUNTIME() + reference = SerializedReferenceModels.AverageRuntimeDeadline(max_runs=10, min_runs=10) interval = timedelta(hours=1) with mock.patch("airflow._shared.timezones.timezone.utcnow") as mock_utcnow: @@ -417,7 +438,7 @@ def test_average_runtime_with_insufficient_history(self, session, dag_maker): session.commit() - reference = DeadlineReference.AVERAGE_RUNTIME() + reference = SerializedReferenceModels.AverageRuntimeDeadline(max_runs=10, min_runs=10) interval = timedelta(hours=1) with mock.patch("airflow._shared.timezones.timezone.utcnow") as mock_utcnow: @@ -451,7 +472,7 @@ def test_average_runtime_with_min_runs(self, session, dag_maker): session.commit() # Test with min_runs=2, should work with 3 runs - reference = DeadlineReference.AVERAGE_RUNTIME(max_runs=10, min_runs=2) + reference = SerializedReferenceModels.AverageRuntimeDeadline(max_runs=10, min_runs=2) interval = timedelta(hours=1) with mock.patch("airflow._shared.timezones.timezone.utcnow") as mock_utcnow: @@ -465,7 +486,7 @@ def test_average_runtime_with_min_runs(self, session, dag_maker): assert result.replace(second=0, microsecond=0) == expected.replace(second=0, microsecond=0) # Test with min_runs=5, should return None with only 3 runs - reference = DeadlineReference.AVERAGE_RUNTIME(max_runs=10, min_runs=5) + reference = SerializedReferenceModels.AverageRuntimeDeadline(max_runs=10, min_runs=5) with mock.patch("airflow._shared.timezones.timezone.utcnow") as mock_utcnow: mock_utcnow.return_value = DEFAULT_DATE @@ -535,17 +556,17 @@ def test_deadline_missing_required_kwargs(self, reference, session): def test_deadline_reference_creation(self): """Test that DeadlineReference provides consistent interface and types.""" fixed_reference = DeadlineReference.FIXED_DATETIME(DEFAULT_DATE) - assert isinstance(fixed_reference, ReferenceModels.FixedDatetimeDeadline) + assert isinstance(fixed_reference, FixedDatetimeDeadline) assert fixed_reference._datetime == DEFAULT_DATE logical_date_reference = DeadlineReference.DAGRUN_LOGICAL_DATE - assert isinstance(logical_date_reference, ReferenceModels.DagRunLogicalDateDeadline) + assert isinstance(logical_date_reference, DagRunLogicalDateDeadline) queued_reference = DeadlineReference.DAGRUN_QUEUED_AT - assert isinstance(queued_reference, ReferenceModels.DagRunQueuedAtDeadline) + assert isinstance(queued_reference, DagRunQueuedAtDeadline) average_runtime_reference = DeadlineReference.AVERAGE_RUNTIME() - assert isinstance(average_runtime_reference, ReferenceModels.AverageRuntimeDeadline) + assert isinstance(average_runtime_reference, AverageRuntimeDeadline) assert average_runtime_reference.max_runs == 10 assert average_runtime_reference.min_runs == 10 @@ -556,14 +577,14 @@ def test_deadline_reference_creation(self): class TestCustomDeadlineReference: - class MyCustomRef(ReferenceModels.BaseDeadlineReference): + class MyCustomRef(BaseDeadlineReference): def _evaluate_with(self, *, session: Session, **kwargs) -> datetime: return timezone.datetime(DEFAULT_DATE) class MyInvalidCustomRef: pass - class MyCustomRefWithKwargs(ReferenceModels.BaseDeadlineReference): + class MyCustomRefWithKwargs(BaseDeadlineReference): required_kwargs = {"custom_id"} def _evaluate_with(self, *, session: Session, **kwargs) -> datetime: @@ -573,7 +594,6 @@ def setup_method(self): self.original_dagrun_created = DeadlineReference.TYPES.DAGRUN_CREATED self.original_dagrun_queued = DeadlineReference.TYPES.DAGRUN_QUEUED self.original_dagrun = DeadlineReference.TYPES.DAGRUN - self.original_attrs = set(dir(ReferenceModels)) self.original_deadline_attrs = set(dir(DeadlineReference)) def teardown_method(self): @@ -581,10 +601,6 @@ def teardown_method(self): DeadlineReference.TYPES.DAGRUN_QUEUED = self.original_dagrun_queued DeadlineReference.TYPES.DAGRUN = self.original_dagrun - for attr in set(dir(ReferenceModels)): - if attr not in self.original_attrs: - delattr(ReferenceModels, attr) - for attr in set(dir(DeadlineReference)): if attr not in self.original_deadline_attrs: delattr(DeadlineReference, attr) @@ -613,7 +629,7 @@ def test_register_custom_reference(self, timing, reference): expected_timing = timing assert result is reference - assert getattr(ReferenceModels, reference.__name__) is reference + assert hasattr(DeadlineReference, reference.__name__) assert getattr(DeadlineReference, reference.__name__).__class__ is reference assert_correct_timing(reference, expected_timing) @@ -637,12 +653,15 @@ def test_register_custom_reference_invalid_timing(self): ): DeadlineReference.register_custom_reference(self.MyCustomRef, invalid_timing) - def test_custom_reference_discoverable_by_get_reference_class(self): + def test_custom_reference_discoverable_on_deadline_reference(self): + # Custom references are only registered on DeadlineReference, not on ReferenceModels. + # During deserialization, custom refs are discovered via __class_path in the + # serialized data (using import_string), not through ReferenceModels lookup. DeadlineReference.register_custom_reference(self.MyCustomRef) - found_class = ReferenceModels.get_reference_class(self.MyCustomRef.__name__) - - assert found_class is self.MyCustomRef + assert hasattr(DeadlineReference, self.MyCustomRef.__name__) + found_instance = getattr(DeadlineReference, self.MyCustomRef.__name__) + assert isinstance(found_instance, self.MyCustomRef) class TestDeadlineReferenceDecorator: @@ -650,21 +669,21 @@ def setup_method(self): self.original_dagrun_created = DeadlineReference.TYPES.DAGRUN_CREATED self.original_dagrun_queued = DeadlineReference.TYPES.DAGRUN_QUEUED self.original_dagrun = DeadlineReference.TYPES.DAGRUN - self.original_attrs = set(dir(ReferenceModels)) + self.original_deadline_attrs = set(dir(DeadlineReference)) def teardown_method(self): DeadlineReference.TYPES.DAGRUN_CREATED = self.original_dagrun_created DeadlineReference.TYPES.DAGRUN_QUEUED = self.original_dagrun_queued DeadlineReference.TYPES.DAGRUN = self.original_dagrun - for attr in set(dir(ReferenceModels)): - if attr not in self.original_attrs: - delattr(ReferenceModels, attr) + for attr in set(dir(DeadlineReference)): + if attr not in self.original_deadline_attrs: + delattr(DeadlineReference, attr) @staticmethod def create_decorated_custom_ref(): @deadline_reference() - class DecoratedCustomRef(ReferenceModels.BaseDeadlineReference): + class DecoratedCustomRef(BaseDeadlineReference): def _evaluate_with(self, *, session: Session, **kwargs) -> datetime: return timezone.datetime(DEFAULT_DATE) @@ -673,7 +692,7 @@ def _evaluate_with(self, *, session: Session, **kwargs) -> datetime: @staticmethod def create_decorated_custom_ref_with_kwargs(): @deadline_reference() - class DecoratedCustomRefWithKwargs(ReferenceModels.BaseDeadlineReference): + class DecoratedCustomRefWithKwargs(BaseDeadlineReference): required_kwargs = {"custom_id"} def _evaluate_with(self, *, session: Session, **kwargs) -> datetime: @@ -684,7 +703,7 @@ def _evaluate_with(self, *, session: Session, **kwargs) -> datetime: @staticmethod def create_decorated_custom_ref_queued(): @deadline_reference(DeadlineReference.TYPES.DAGRUN_QUEUED) - class DecoratedCustomRefQueued(ReferenceModels.BaseDeadlineReference): + class DecoratedCustomRefQueued(BaseDeadlineReference): def _evaluate_with(self, *, session: Session, **kwargs) -> datetime: return timezone.datetime(DEFAULT_DATE) @@ -713,7 +732,7 @@ def _evaluate_with(self, *, session: Session, **kwargs) -> datetime: def test_deadline_reference_decorator(self, reference_factory, expected_timing): reference = reference_factory() - assert getattr(ReferenceModels, reference.__name__) is reference + assert hasattr(DeadlineReference, reference.__name__) assert getattr(DeadlineReference, reference.__name__).__class__ is reference assert_correct_timing(reference, expected_timing) @@ -741,7 +760,7 @@ def test_deadline_reference_decorator_with_invalid_timing(self): ): @deadline_reference(invalid_timing) - class DecoratedCustomRef(ReferenceModels.BaseDeadlineReference): + class DecoratedCustomRef(BaseDeadlineReference): def _evaluate_with(self, *, session: Session, **kwargs) -> datetime: return timezone.datetime(DEFAULT_DATE) @@ -750,7 +769,7 @@ def test_deadline_reference_decorator_calls_register_method(self, mock_register) timing = DeadlineReference.TYPES.DAGRUN_QUEUED @deadline_reference(timing) - class DecoratedCustomRef(ReferenceModels.BaseDeadlineReference): + class DecoratedCustomRef(BaseDeadlineReference): def _evaluate_with(self, *, session: Session, **kwargs) -> datetime: return timezone.datetime(DEFAULT_DATE) diff --git a/airflow-core/tests/unit/models/test_deadline_alert.py b/airflow-core/tests/unit/models/test_deadline_alert.py index 879203814b3ba..9d69577a6d6ec 100644 --- a/airflow-core/tests/unit/models/test_deadline_alert.py +++ b/airflow-core/tests/unit/models/test_deadline_alert.py @@ -16,13 +16,17 @@ # under the License. from __future__ import annotations +from datetime import timedelta +from unittest.mock import Mock + import pytest import time_machine from sqlalchemy import select +from airflow._shared.timezones import timezone from airflow.models.deadline_alert import DeadlineAlert from airflow.models.serialized_dag import SerializedDagModel -from airflow.sdk.definitions.deadline import DeadlineReference +from airflow.sdk.definitions.deadline import BaseDeadlineReference, DeadlineReference from airflow.serialization.definitions.deadline import SerializedReferenceModels from tests_common.test_utils import db @@ -172,3 +176,55 @@ def test_deadline_alert_get_by_id_not_found(self, session): nonexistent_uuid = "00000000-0000-7000-8000-000000000000" with pytest.raises(NoResultFound, match="No DeadlineAlert found"): DeadlineAlert.get_by_id(nonexistent_uuid, session=session) + + def test_serialized_custom_reference_kwargs_handling(self): + """Test that SerializedCustomReference properly filters and validates kwargs.""" + + class StrictCustomRef(BaseDeadlineReference): + reference_name = "StrictCustomRef" + required_kwargs = {"dag_id", "run_id"} + + def _evaluate_with(self, *, session, dag_id, run_id): + return timezone.utcnow() + + inner_ref = StrictCustomRef() + inner_ref._evaluate_with = Mock(return_value=timezone.utcnow()) + + wrapper = SerializedReferenceModels.SerializedCustomReference(inner_ref) + + wrapper.evaluate_with( + session=None, + interval=timedelta(hours=1), + dag_id="test_dag", + run_id="test_run", + extra_param="should_be_filtered", + ) + + inner_ref._evaluate_with.assert_called_once_with(session=None, dag_id="test_dag", run_id="test_run") + + # try calling with missing required parameters + with pytest.raises(ValueError, match="missing required parameters: run_id"): + wrapper.evaluate_with( + session=None, + interval=timedelta(hours=1), + dag_id="test_dag", + ) + + def test_core_deadline_reference_treated_as_builtins(self): + """Test that refs from airflow.models.deadline are still treated as builtins.""" + from airflow.models.deadline import ReferenceModels + from airflow.serialization.encoders import encode_deadline_reference + + ref = ReferenceModels.DagRunLogicalDateDeadline() + serialized = encode_deadline_reference(ref) + + assert "__class_path" not in serialized + assert serialized["reference_type"] == "DagRunLogicalDateDeadline" + + def test_is_builtin_reference(self): + """Test that is_builtin_reference correctly identifies built-in vs custom references.""" + assert SerializedReferenceModels.is_builtin_reference("DagRunLogicalDateDeadline") is True + assert SerializedReferenceModels.is_builtin_reference("DagRunQueuedAtDeadline") is True + assert SerializedReferenceModels.is_builtin_reference("AverageRuntimeDeadline") is True + + assert SerializedReferenceModels.is_builtin_reference("MyCustomRef") is False diff --git a/airflow-core/tests/unit/models/test_taskinstance.py b/airflow-core/tests/unit/models/test_taskinstance.py index a013b09cdb0c8..82b9adc3162ae 100644 --- a/airflow-core/tests/unit/models/test_taskinstance.py +++ b/airflow-core/tests/unit/models/test_taskinstance.py @@ -2885,6 +2885,71 @@ def cmds(): out_lines = [line.strip() for line in f] assert out_lines == ["hello FOO", "goodbye FOO", "hello BAR", "goodbye BAR"] + def test_xcom_pull_unmapped_task(self, dag_maker, session): + """ + Test that xcom_pull from unmapped task returns single deserialized value. + + For unmapped tasks with map_index < 0, xcom_pull should return the single value, + not a LazyXComSelectSequence. + """ + + with dag_maker(dag_id="test_xcom_unmapped", session=session): + upstream = PythonOperator( + task_id="unmapped_task", + python_callable=lambda: {"key": "value"}, + ) + downstream = PythonOperator( + task_id="downstream", + python_callable=lambda: None, + ) + upstream >> downstream + + dag_run = dag_maker.create_dagrun(logical_date=timezone.utcnow()) + + # Run upstream task to push xcom + dag_maker.run_ti("unmapped_task", dag_run=dag_run, session=session) + + # Get downstream task instance + ti_downstream = dag_run.get_task_instance("downstream", session=session) + ti_downstream.task = dag_maker.dag.task_dict["downstream"] + + # Pull xcom - should return single dict value, not LazyXComSelectSequence + result = ti_downstream.xcom_pull(task_ids="unmapped_task", session=session) + assert isinstance(result, dict), f"Expected dict for unmapped task, got {type(result)}" + assert result == {"key": "value"} + + def test_xcom_pull_returns_lazy_sequence_for_mapped_xcom(self, dag_maker, session): + """ + Test that xcom_pull returns LazyXComSelectSequence when XComs are mapped (map_index >= 0) + and map_indexes is not specified. + """ + from airflow.models.xcom import LazyXComSelectSequence + + with dag_maker(dag_id="test_xcom_mapped_values", session=session): + + @task + def push_values(val): + return val + + upstream = push_values.expand(val=[2, 4]) + downstream = PythonOperator( + task_id="downstream", + python_callable=lambda: None, + ) + upstream >> downstream + + dag_run = dag_maker.create_dagrun(logical_date=timezone.utcnow()) + dag_maker.run_ti(upstream.operator.task_id, map_index=0, dag_run=dag_run, session=session) + dag_maker.run_ti(upstream.operator.task_id, map_index=1, dag_run=dag_run, session=session) + + ti_downstream = dag_run.get_task_instance("downstream", session=session) + ti_downstream.task = dag_maker.dag.task_dict["downstream"] + + result = ti_downstream.xcom_pull(task_ids=upstream.operator.task_id, session=session) + assert isinstance(result, LazyXComSelectSequence), ( + f"Expected LazyXComSelectSequence for mapped XComs, got {type(result)}" + ) + def _get_lazy_xcom_access_expected_sql_lines() -> list[str]: backend = os.environ.get("BACKEND") diff --git a/airflow-core/tests/unit/observability/traces/test_otel_tracer.py b/airflow-core/tests/unit/observability/traces/test_otel_tracer.py deleted file mode 100644 index 3ca8e07f5d55f..0000000000000 --- a/airflow-core/tests/unit/observability/traces/test_otel_tracer.py +++ /dev/null @@ -1,259 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from __future__ import annotations - -import json -import logging -from datetime import datetime -from unittest.mock import patch - -import pytest -from opentelemetry.sdk import util -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter - -from airflow._shared.observability.traces.base_tracer import EmptyTrace -from airflow._shared.observability.traces.otel_tracer import OtelTrace -from airflow._shared.observability.traces.utils import datetime_to_nano -from airflow.observability.trace import Trace -from airflow.observability.traces import otel_tracer - -from tests_common.test_utils.config import env_vars - - -@pytest.fixture -def name(): - return "test_traces_run" - - -class TestOtelTrace: - @env_vars( - { - "AIRFLOW__TRACES__OTEL_ON": "True", - "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318", - "OTEL_TRACES_EXPORTER": "console", - } - ) - def test_get_otel_tracer_from_trace_metaclass(self): - """Test that `Trace.some_method()`, uses an `OtelTrace` instance when otel is configured.""" - tracer = otel_tracer.get_otel_tracer(Trace) - assert tracer.use_simple_processor is False - - assert isinstance(Trace.factory(), EmptyTrace) - - Trace.configure_factory() - assert isinstance(Trace.factory(), OtelTrace) - - task_tracer = otel_tracer.get_otel_tracer_for_task(Trace) - assert task_tracer.use_simple_processor is True - - task_tracer.get_otel_tracer_provider() - assert task_tracer.use_simple_processor is True - - @patch("opentelemetry.sdk.trace.export.ConsoleSpanExporter") - @patch("airflow._shared.observability.otel_env_config.OtelEnvConfig") - @env_vars( - { - "OTEL_SERVICE_NAME": "my_test_service", - # necessary to speed up the span to be emitted - "OTEL_BSP_SCHEDULE_DELAY": "1", - } - ) - def test_tracer(self, otel_env_conf, exporter): - log = logging.getLogger("TestOtelTrace.test_tracer") - log.setLevel(logging.DEBUG) - - # mocking console exporter with in mem exporter for better assertion - in_mem_exporter = InMemorySpanExporter() - exporter.return_value = in_mem_exporter - - tracer = otel_tracer.get_otel_tracer(Trace) - assert otel_env_conf.called - otel_env_conf.assert_called_once() - with tracer.start_span(span_name="span1") as s1: - with tracer.start_span(span_name="span2") as s2: - s2.set_attribute("attr2", "val2") - span2 = json.loads(s2.to_json()) - span1 = json.loads(s1.to_json()) - # assert the two span data - assert span1["name"] == "span1" - assert span2["name"] == "span2" - trace_id = span1["context"]["trace_id"] - s1_span_id = span1["context"]["span_id"] - assert span2["context"]["trace_id"] == trace_id - assert span2["parent_id"] == s1_span_id - assert span2["attributes"]["attr2"] == "val2" - assert span2["resource"]["attributes"]["service.name"] == "my_test_service" - - @patch("opentelemetry.sdk.trace.export.ConsoleSpanExporter") - @env_vars( - { - "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318", - # necessary to speed up the span to be emitted - "OTEL_BSP_SCHEDULE_DELAY": "1", - } - ) - def test_dag_tracer(self, exporter): - log = logging.getLogger("TestOtelTrace.test_dag_tracer") - log.setLevel(logging.DEBUG) - - # mocking console exporter with in mem exporter for better assertion - in_mem_exporter = InMemorySpanExporter() - exporter.return_value = in_mem_exporter - - now = datetime.now() - - tracer = otel_tracer.get_otel_tracer(Trace) - with tracer.start_root_span(span_name="span1", start_time=now) as s1: - with tracer.start_span(span_name="span2") as s2: - s2.set_attribute("attr2", "val2") - span2 = json.loads(s2.to_json()) - span1 = json.loads(s1.to_json()) - - # The otel sdk, accepts an int for the start_time, and converts it to an iso string, - # using `util.ns_to_iso_str()`. - nano_time = datetime_to_nano(now) - assert span1["start_time"] == util.ns_to_iso_str(nano_time) - # Same trace_id - assert span1["context"]["trace_id"] == span2["context"]["trace_id"] - assert span1["context"]["span_id"] == span2["parent_id"] - - @patch("opentelemetry.sdk.trace.export.ConsoleSpanExporter") - @env_vars( - { - "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318", - # necessary to speed up the span to be emitted - "OTEL_BSP_SCHEDULE_DELAY": "1", - } - ) - def test_context_propagation(self, exporter): - log = logging.getLogger("TestOtelTrace.test_context_propagation") - log.setLevel(logging.DEBUG) - - # mocking console exporter with in mem exporter for better assertion - in_mem_exporter = InMemorySpanExporter() - exporter.return_value = in_mem_exporter - - # Method that represents another service which is - # - getting the carrier - # - extracting the context - # - using the context to create a new span - # The new span should be associated with the span from the injected context carrier. - def _task_func(otel_tr, carrier): - parent_context = otel_tr.extract(carrier) - - with otel_tr.start_child_span(span_name="sub_span", parent_context=parent_context) as span: - span.set_attribute("attr2", "val2") - json_span = json.loads(span.to_json()) - return json_span - - tracer = otel_tracer.get_otel_tracer(Trace) - - root_span = tracer.start_root_span(span_name="root_span", start_as_current=False) - # The context is available, it can be injected into the carrier. - context_carrier = tracer.inject() - - # Some function that uses the carrier to create a new span. - json_span2 = _task_func(otel_tr=tracer, carrier=context_carrier) - - json_span1 = json.loads(root_span.to_json()) - # Manually end the span. - root_span.end() - - # Verify that span1 is a root span. - assert json_span1["parent_id"] is None - # Check span2 parent_id to verify that it's a child of span1. - assert json_span2["parent_id"] == json_span1["context"]["span_id"] - # The trace_id and the span_id are randomly generated by the otel sdk. - # Both spans should belong to the same trace. - assert json_span1["context"]["trace_id"] == json_span2["context"]["trace_id"] - - @pytest.mark.parametrize( - ("provided_env_vars", "expected_endpoint", "expected_exporter_module"), - [ - pytest.param( - { - "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:1234", - "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", - "AIRFLOW__TRACES__OTEL_HOST": "breeze-otel-collector", - "AIRFLOW__TRACES__OTEL_PORT": "4318", - }, - "localhost:1234", - "grpc", - id="env_vars_with_grpc", - ), - pytest.param( - { - "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", - "AIRFLOW__TRACES__OTEL_HOST": "breeze-otel-collector", - "AIRFLOW__TRACES__OTEL_PORT": "4318", - }, - "http://breeze-otel-collector:4318/v1/traces", - "http", - id="protocol_is_ignored_if_no_env_endpoint", - ), - pytest.param( - { - "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:1234", - "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", - "AIRFLOW__TRACES__OTEL_HOST": "breeze-otel-collector", - "AIRFLOW__TRACES__OTEL_PORT": "4318", - }, - "http://localhost:1234/v1/traces", - "http", - id="for_http_with_env_vars_otel_builds_full_url", - ), - pytest.param( - { - "AIRFLOW__TRACES__OTEL_HOST": "breeze-otel-collector", - "AIRFLOW__TRACES__OTEL_PORT": "4318", - }, - "http://breeze-otel-collector:4318/v1/traces", - "http", - id="use_airflow_config", - ), - pytest.param( - { - "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:1234", - "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", - }, - "http://localhost:1234/v1/traces", - "http", - id="only_env_vars", - ), - pytest.param( - { - "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:1234", - "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://localhost:2222", - "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", - "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL": "grpc", - }, - "localhost:2222", - "grpc", - id="type_specific_vars_take_precedence", - ), - ], - ) - def test_config_priorities(self, provided_env_vars, expected_endpoint, expected_exporter_module): - with env_vars(provided_env_vars): - tracer = otel_tracer.get_otel_tracer(Trace) - - assert tracer.span_exporter._endpoint == expected_endpoint - - assert ( - tracer.span_exporter.__class__.__module__ - == f"opentelemetry.exporter.otlp.proto.{expected_exporter_module}.trace_exporter" - ) diff --git a/airflow-core/tests/unit/partition_mappers/test_allowed_key.py b/airflow-core/tests/unit/partition_mappers/test_allowed_key.py new file mode 100644 index 0000000000000..a04b22e48e9b4 --- /dev/null +++ b/airflow-core/tests/unit/partition_mappers/test_allowed_key.py @@ -0,0 +1,48 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import pytest + +from airflow.partition_mappers.allowed_key import AllowedKeyMapper + + +class TestAllowedKeyMapper: + def test_to_downstream(self): + pm = AllowedKeyMapper(["us", "eu", "apac"]) + assert pm.to_downstream("us") == "us" + assert pm.to_downstream("eu") == "eu" + + def test_to_downstream_invalid_key(self): + pm = AllowedKeyMapper(["us", "eu"]) + with pytest.raises(ValueError, match="not in allowed keys"): + pm.to_downstream("apac") + + def test_serialize(self): + pm = AllowedKeyMapper(["a", "b", "c"]) + assert pm.serialize() == {"allowed_keys": ["a", "b", "c"]} + + def test_deserialize(self): + pm = AllowedKeyMapper.deserialize({"allowed_keys": ["x", "y"]}) + assert isinstance(pm, AllowedKeyMapper) + assert pm.allowed_keys == ["x", "y"] + + def test_empty_allowed_keys(self): + pm = AllowedKeyMapper([]) + assert pm.serialize() == {"allowed_keys": []} + with pytest.raises(ValueError, match="not in allowed keys"): + pm.to_downstream("any") diff --git a/airflow-core/tests/unit/plugins/test_plugins_manager.py b/airflow-core/tests/unit/plugins/test_plugins_manager.py index 10377fbb4ed18..7ffa84060f9a8 100644 --- a/airflow-core/tests/unit/plugins/test_plugins_manager.py +++ b/airflow-core/tests/unit/plugins/test_plugins_manager.py @@ -84,7 +84,7 @@ def test_no_log_when_no_plugins(self, caplog): plugins_manager.ensure_plugins_loaded() - assert caplog.record_tuples == [] + assert [r for r in caplog.record_tuples if not r[0].startswith("opentelemetry.")] == [] def test_loads_filesystem_plugins(self, caplog): from airflow import plugins_manager @@ -104,7 +104,7 @@ def test_loads_filesystem_plugins(self, caplog): else: pytest.fail("Wasn't able to find a registered `AirflowTestOnLoadPlugin`") - assert caplog.record_tuples == [] + assert [r for r in caplog.record_tuples if not r[0].startswith("opentelemetry.")] == [] def test_loads_filesystem_plugins_exception(self, caplog, tmp_path): from airflow import plugins_manager @@ -396,4 +396,4 @@ def test_does_not_double_import_entrypoint_provider_plugins(self): # Mock/skip loading from plugin dir with mock.patch("airflow.plugins_manager._load_plugins_from_plugin_directory", return_value=([], [])): plugins = plugins_manager._get_plugins()[0] - assert len(plugins) == 5 + assert len(plugins) == 6 diff --git a/airflow-core/tests/unit/serialization/test_serialized_objects.py b/airflow-core/tests/unit/serialization/test_serialized_objects.py index e001def29a9e3..a814de07d2e05 100644 --- a/airflow-core/tests/unit/serialization/test_serialized_objects.py +++ b/airflow-core/tests/unit/serialization/test_serialized_objects.py @@ -877,6 +877,31 @@ def test_decode_product_mapper(): assert core_pm.to_downstream("2024-06-15T10:30:00|2024-06-15T10:30:00") == "2024-06-15T10|2024-06-15" +def test_encode_allowed_key_mapper(): + from airflow.sdk import AllowedKeyMapper + from airflow.serialization.encoders import encode_partition_mapper + + partition_mapper = AllowedKeyMapper(["us", "eu", "apac"]) + assert encode_partition_mapper(partition_mapper) == { + Encoding.TYPE: "airflow.partition_mappers.allowed_key.AllowedKeyMapper", + Encoding.VAR: {"allowed_keys": ["us", "eu", "apac"]}, + } + + +def test_decode_allowed_key_mapper(): + from airflow.partition_mappers.allowed_key import AllowedKeyMapper as CoreAllowedKeyMapper + from airflow.sdk import AllowedKeyMapper + from airflow.serialization.decoders import decode_partition_mapper + from airflow.serialization.encoders import encode_partition_mapper + + partition_mapper = AllowedKeyMapper(["us", "eu", "apac"]) + encoded_pm = encode_partition_mapper(partition_mapper) + core_pm = decode_partition_mapper(encoded_pm) + + assert isinstance(core_pm, CoreAllowedKeyMapper) + assert core_pm.allowed_keys == ["us", "eu", "apac"] + + class TestSerializedBaseOperator: # ensure the default logging config is used for this test, no matter what ran before @pytest.mark.usefixtures("reset_logging_config") diff --git a/airflow-core/tests/unit/ti_deps/deps/test_not_previously_skipped_dep.py b/airflow-core/tests/unit/ti_deps/deps/test_not_previously_skipped_dep.py index 0227ca2325e9c..da74cd21c4af9 100644 --- a/airflow-core/tests/unit/ti_deps/deps/test_not_previously_skipped_dep.py +++ b/airflow-core/tests/unit/ti_deps/deps/test_not_previously_skipped_dep.py @@ -22,10 +22,15 @@ from sqlalchemy import delete from airflow.models import DagRun, TaskInstance +from airflow.models.xcom import XComModel from airflow.providers.standard.operators.empty import EmptyOperator from airflow.providers.standard.operators.python import BranchPythonOperator from airflow.ti_deps.dep_context import DepContext -from airflow.ti_deps.deps.not_previously_skipped_dep import NotPreviouslySkippedDep +from airflow.ti_deps.deps.not_previously_skipped_dep import ( + XCOM_SKIPMIXIN_FOLLOWED, + XCOM_SKIPMIXIN_KEY, + NotPreviouslySkippedDep, +) from airflow.utils.state import State from airflow.utils.types import DagRunType @@ -161,3 +166,51 @@ def test_parent_not_executed(session, dag_maker): assert len(list(dep.get_dep_statuses(ti2, session, DepContext()))) == 0 assert dep.is_met(ti2, session) assert ti2.state == State.NONE + + +def test_unmapped_parent_skip_mapped_downstream(session, dag_maker): + """ + When an unmapped SkipMixin parent writes XCom with map_index=-1, + mapped downstream TIs (map_index >= 0) should still be skipped + by NotPreviouslySkippedDep. + + Regression test for https://github.com/apache/airflow/issues/62118 + """ + start_date = pendulum.datetime(2020, 1, 1) + with dag_maker( + "test_unmapped_skip_mapped_dag", + schedule=None, + start_date=start_date, + session=session, + ): + op1 = BranchPythonOperator(task_id="op1", python_callable=lambda: "op3") + op2 = EmptyOperator(task_id="op2") + op3 = EmptyOperator(task_id="op3") + op1 >> [op2, op3] + + dr = dag_maker.create_dagrun(run_type=DagRunType.MANUAL, state=State.RUNNING) + tis = {ti.task_id: ti for ti in dr.task_instances} + + # Simulate the unmapped branch operator having run: set it to SUCCESS + # and store XCom with map_index=-1 (as SkipMixin does for unmapped tasks). + tis["op1"].state = State.SUCCESS + session.merge(tis["op1"]) + XComModel.set( + key=XCOM_SKIPMIXIN_KEY, + value={XCOM_SKIPMIXIN_FOLLOWED: ["op3"]}, + dag_id=dr.dag_id, + task_id="op1", + run_id=dr.run_id, + map_index=-1, + session=session, + ) + + # Simulate a mapped downstream TI by changing map_index to 0. + tis["op2"].map_index = 0 + session.merge(tis["op2"]) + session.flush() + + dep = NotPreviouslySkippedDep() + assert len(list(dep.get_dep_statuses(tis["op2"], session, DepContext()))) == 1 + assert not dep.is_met(tis["op2"], session) + assert tis["op2"].state == State.SKIPPED diff --git a/airflow-core/tests/unit/timetables/test_continuous_timetable.py b/airflow-core/tests/unit/timetables/test_continuous_timetable.py index 73a4be5ea48a2..03babcea63839 100644 --- a/airflow-core/tests/unit/timetables/test_continuous_timetable.py +++ b/airflow-core/tests/unit/timetables/test_continuous_timetable.py @@ -41,12 +41,28 @@ def timetable(): return ContinuousTimetable() -def test_no_runs_without_start_date(timetable): +@time_machine.travel(DURING_DATE) +def test_runs_without_start_date(timetable): next_info = timetable.next_dagrun_info( last_automated_data_interval=None, restriction=TimeRestriction(earliest=None, latest=None, catchup=False), ) - assert next_info is None + assert next_info is not None + assert next_info.run_after == DURING_DATE + assert next_info.data_interval.start == DURING_DATE + assert next_info.data_interval.end == DURING_DATE + + +@time_machine.travel(AFTER_DATE) +def test_subsequent_runs_without_start_date(timetable): + next_info = timetable.next_dagrun_info( + last_automated_data_interval=DataInterval(DURING_DATE, DURING_DATE), + restriction=TimeRestriction(earliest=None, latest=None, catchup=False), + ) + assert next_info is not None + assert next_info.run_after == AFTER_DATE + assert next_info.data_interval.start == DURING_DATE + assert next_info.data_interval.end == AFTER_DATE @time_machine.travel(DURING_DATE) diff --git a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py index 3f89a7c969818..73a2b28c3f0b4 100644 --- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py +++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py @@ -44,10 +44,13 @@ def date_param(): # Passing password via command line is insecure but acceptable for testing purposes # Please do not do this in production, it enables possibility of exposing your credentials -LOGIN_COMMAND = "auth login --username airflow --password airflow" +CREDENTIAL_SUFFIX = "--username airflow --password airflow" +LOGIN_COMMAND = f"auth login {CREDENTIAL_SUFFIX}" LOGIN_COMMAND_SKIP_KEYRING = "auth login --skip-keyring" LOGIN_OUTPUT = "Login successful! Welcome to airflowctl!" TEST_COMMANDS = [ + # Auth commands + f"auth token {CREDENTIAL_SUFFIX}", # Assets commands "assets list", "assets get --asset-id=1", @@ -131,7 +134,6 @@ def date_param(): ] -@pytest.mark.flaky(reruns=3, reruns_delay=1) @pytest.mark.parametrize( "command", TEST_COMMANDS_DEBUG_MODE, @@ -144,7 +146,6 @@ def test_airflowctl_commands(command: str, run_command): run_command(command, env_vars, skip_login=True) -@pytest.mark.flaky(reruns=3, reruns_delay=1) @pytest.mark.parametrize( "command", TEST_COMMANDS_SKIP_KEYRING, diff --git a/airflow-ctl/.pre-commit-config.yaml b/airflow-ctl/.pre-commit-config.yaml index 1f029eb314a83..e63268b077ef8 100644 --- a/airflow-ctl/.pre-commit-config.yaml +++ b/airflow-ctl/.pre-commit-config.yaml @@ -60,4 +60,5 @@ repos: pass_filenames: false files: (?x) - ^src/airflowctl/api/operations\.py$ + ^src/airflowctl/api/operations\.py$| + ^docs/images/command_hashes.txt$ diff --git a/airflow-ctl/RELEASE_NOTES.rst b/airflow-ctl/RELEASE_NOTES.rst index 164d9bef98070..3e4ac4427de28 100644 --- a/airflow-ctl/RELEASE_NOTES.rst +++ b/airflow-ctl/RELEASE_NOTES.rst @@ -15,6 +15,42 @@ specific language governing permissions and limitations under the License. +airflowctl 0.1.3 (2026-03-09) +----------------------------- + +Significant Changes +^^^^^^^^^^^^^^^^^^^ + +- Add airflowctl auth token command to print JWT access tokens (#62843) +- Add ``--action-on-existing-key`` to ``pools import`` and ``connections import`` (#62702) +- Add retry mechanism to airflowctl and remove flaky integration mark (#63016) +- airflowctl auth login: prompt for credentials interactively when none are provided (#62549) +- feat(airflowctl): support on headless environments (#62217) + +Bug Fixes +^^^^^^^^^ + +- Fix ``airflowctl pools export`` ignoring ``--output`` table/yaml/plain (#62665) +- Fix ``airflowctl connections import`` failure when JSON omits ``extra`` field (#62662) +- Amend compatibility issues for airflowctl (#63388) + +Improvements +^^^^^^^^^^^^ + +- Send ``limit`` parameter in ``execute_list`` server requests (#63048) +- Run test coverage when airflowctl command has any change (#63216) +- airflow-ctl: add coverage tests for console formatting output (#62627) +- Clean up stale Python 3.9 workaround in airflow-ctl CLI config parser (#62206) +- Expose ``timetable_partitioned`` in UI API (#62777) + +Miscellaneous +^^^^^^^^^^^^^ + +- CI: upgrade important CI environment (#62610) +- Fix all build-system requirements including transitive dependencies (#62570) +- Add DagRunType for asset materializations (#62276) + + airflowctl 0.1.2 (2026-02-20) ----------------------------- @@ -34,13 +70,13 @@ Bug Fixes - Fix airflowctl auth login reporting success when keyring backend is unavailable (#61296) - Fix airflowctl crash when incorrect keyring password is entered (#61042) - Strip api-url for airflowctl auth login which fails with trailing slash (#61245) -- Fix airflow-ctl-tests files not triggering pre-commit integration tests (#61023) +- Fix ``airflow-ctl-tests`` files not triggering pre-commit integration tests (#61023) Improvements ^^^^^^^^^^^^ - Print debug mode warning to stderr to avoid polluting stdout JSON output (#61302) -- Refactor datamodel defaulting logic into dedicated method (#61236) +- Refactor ``datamodel`` defaulting logic into dedicated method (#61236) - Alias run_after for XComResponse (#61443) - Add test for sensitive config masking in airflowctl (#60361) diff --git a/airflow-ctl/docs/cli-and-env-variables-ref.rst b/airflow-ctl/docs/cli-and-env-variables-ref.rst index 2d63d47d5be53..66e3638a4a7b6 100644 --- a/airflow-ctl/docs/cli-and-env-variables-ref.rst +++ b/airflow-ctl/docs/cli-and-env-variables-ref.rst @@ -60,3 +60,21 @@ Environment Variables It disables some features such as keyring integration and save credentials to file. It is only meant to use if either you are developing airflowctl or running API integration tests. Please do not use this variable unless you know what you are doing. + +.. envvar:: AIRFLOW_CLI_API_RETRIES + + The number of times to retry an API call if it fails. This is + only used if you are using the Airflow API and have not set up + authentication using a different method. The default value is 3. + +.. envvar:: AIRFLOW_CLI_API_RETRY_WAIT_MIN + + The minimum amount of time to wait between API retries in seconds. + This is only used if you are using the Airflow API and have not set up + authentication using a different method. The default value is 1 second. + +.. envvar:: AIRFLOW_CLI_API_RETRY_WAIT_MAX + + The maximum amount of time to wait between API retries in seconds. + This is only used if you are using the Airflow API and have not set up + authentication using a different method. The default value is 10 seconds. diff --git a/airflow-ctl/docs/images/command_hashes.txt b/airflow-ctl/docs/images/command_hashes.txt index 5922aa473cf1e..b0089d41d1f91 100644 --- a/airflow-ctl/docs/images/command_hashes.txt +++ b/airflow-ctl/docs/images/command_hashes.txt @@ -1,6 +1,6 @@ main:65249416abad6ad24c276fb44326ae15 assets:b3ae2b933e54528bf486ff28e887804d -auth:82bc73405e153df5112f05c4811ab92b +auth:d79e9c7d00c432bdbcbc2a86e2e32053 backfill:bbce9859a2d1ce054ad22db92dea8c05 config:cb175bedf29e8a2c2c6a2ebd13d770a7 connections:e34b6b93f64714986139958c1f370428 diff --git a/airflow-ctl/docs/images/output_auth.svg b/airflow-ctl/docs/images/output_auth.svg index 9f0f482e18623..7e697a2bb1a0c 100644 --- a/airflow-ctl/docs/images/output_auth.svg +++ b/airflow-ctl/docs/images/output_auth.svg @@ -1,4 +1,4 @@ - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + - + - + - - Usage:airflowctl auth [-hCOMMAND... - -Manage authentication for CLI. Either pass token from environment  -variable/parameter or pass username and password. - -Positional Arguments: -COMMAND -list-envs -List all CLI environments that the user has logged into -loginLogin to the metadata database for personal usage. JWT Token  -must be provided via parameter. - -Options: --h--helpshow this help message and exit + + Usage:airflowctl auth [-hCOMMAND... + +Manage authentication for CLI. Either pass token from environment  +variable/parameter or pass username and password. + +Positional Arguments: +COMMAND +list-envs +List all CLI environments that the user has logged into +loginLogin to the metadata database for personal usage. JWT Token  +must be provided via parameter. +tokenGenerate and print a JWT token for the given credentials + +Options: +-h--helpshow this help message and exit diff --git a/airflow-ctl/pyproject.toml b/airflow-ctl/pyproject.toml index 45e42a0c5b337..fec7206a23092 100644 --- a/airflow-ctl/pyproject.toml +++ b/airflow-ctl/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "structlog>=25.4.0", "uuid6>=2024.7.10", "tabulate>=0.9.0", + "tenacity>=9.1.4", ] classifiers = [ diff --git a/airflow-ctl/src/airflowctl/api/client.py b/airflow-ctl/src/airflowctl/api/client.py index 7c03dae30554a..0ef5d7cb16441 100644 --- a/airflow-ctl/src/airflowctl/api/client.py +++ b/airflow-ctl/src/airflowctl/api/client.py @@ -21,6 +21,7 @@ import enum import getpass import json +import logging import os import sys from collections.abc import Callable @@ -32,6 +33,13 @@ import structlog from httpx import URL from keyring.errors import NoKeyringError +from tenacity import ( + before_log, + retry, + retry_if_exception, + stop_after_attempt, + wait_random_exponential, +) from uuid6 import uuid7 from airflowctl import __version__ as version @@ -261,6 +269,20 @@ def auth_flow(self, request: httpx.Request): yield request +def _should_retry_api_request(exception: BaseException) -> bool: + """Determine if an API request should be retried based on the exception type.""" + if isinstance(exception, httpx.HTTPStatusError): + return exception.response.status_code >= 500 + + return isinstance(exception, httpx.RequestError) + + +# API Client Retry Configuration +API_RETRIES = int(os.getenv("AIRFLOW_CLI_API_RETRIES", "3")) +API_RETRY_WAIT_MIN = int(os.getenv("AIRFLOW_CLI_API_RETRY_WAIT_MIN", "1")) +API_RETRY_WAIT_MAX = int(os.getenv("AIRFLOW_CLI_API_RETRY_WAIT_MAX", "10")) + + class Client(httpx.Client): """Client for the Airflow REST API.""" @@ -298,6 +320,23 @@ def _get_base_url( return f"{base_url}/auth" return f"{base_url}/api/v2" + @retry( + retry=retry_if_exception(_should_retry_api_request), + stop=stop_after_attempt(API_RETRIES), + wait=wait_random_exponential(min=API_RETRY_WAIT_MIN, max=API_RETRY_WAIT_MAX), + before_sleep=before_log(log, logging.WARNING), + reraise=True, + ) + def request(self, *args, **kwargs): + """Implement a convenience for httpx.Client.request with a retry layer.""" + # Set content type as convenience if not already set + if kwargs.get("content", None) is not None and "content-type" not in ( + kwargs.get("headers", {}) or {} + ): + kwargs["headers"] = {"content-type": "application/json"} + + return super().request(*args, **kwargs) + @lru_cache() # type: ignore[prop-decorator] @property def login(self): diff --git a/airflow-ctl/src/airflowctl/api/operations.py b/airflow-ctl/src/airflowctl/api/operations.py index e4a046ed77421..64424eff9c557 100644 --- a/airflow-ctl/src/airflowctl/api/operations.py +++ b/airflow-ctl/src/airflowctl/api/operations.py @@ -19,11 +19,11 @@ import datetime import json -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar, get_args import httpx import structlog -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError from airflowctl.api.datamodels.auth_generated import LoginBody, LoginResponse from airflowctl.api.datamodels.generated import ( @@ -133,6 +133,43 @@ def wrapped(self, *args, **kwargs): return wrapped +TYPE_DEFAULTS = { + bool: False, + int: 0, + float: 0.0, + str: "", + list: [], + dict: {}, +} + + +def get_field_default(annotation) -> Any: + args = get_args(annotation) + if args: + non_none = [a for a in args if a is not type(None)] + if non_none: + return get_field_default(non_none[0]) + return TYPE_DEFAULTS.get(annotation, None) + + +def fill_missing_fields(data: dict, model: type[BaseModel]) -> dict: + for field_name, field_info in model.model_fields.items(): + annotation = field_info.annotation + args = get_args(annotation) + if field_name not in data and field_info.is_required(): + data[field_name] = get_field_default(annotation) + elif field_name in data and isinstance(data[field_name], dict): + if isinstance(annotation, type) and issubclass(annotation, BaseModel): + data[field_name] = fill_missing_fields(data[field_name], annotation) + elif field_name in data and isinstance(data[field_name], list) and args: + if isinstance(args[0], type) and issubclass(args[0], BaseModel): + data[field_name] = [ + fill_missing_fields(item, args[0]) if isinstance(item, dict) else item + for item in data[field_name] + ] + return data + + class BaseOperations: """ Base class for operations. @@ -155,33 +192,35 @@ def __init_subclass__(cls, **kwargs): if callable(value): setattr(cls, attr, _check_flag_and_exit_if_server_response_error(value)) - def execute_list( - self, - *, - path: str, - data_model: type[T], - offset: int = 0, - limit: int = 50, - params: dict | None = None, - ) -> T | ServerResponseError: - shared_params = {**(params or {})} + def execute_list(self, *, path, data_model, offset=0, limit=50, params=None): + shared_params = {"limit": limit, **(params or {})} + + def safe_validate(content: bytes) -> BaseModel: + try: + return data_model.model_validate_json(content) # type: ignore[union-attr] + except ValidationError: + raw = fill_missing_fields(json.loads(content), data_model) + return data_model.model_validate(raw) # type: ignore[union-attr] + self.response = self.client.get(path, params=shared_params) - first_pass = data_model.model_validate_json(self.response.content) + first_pass = safe_validate(self.response.content) total_entries = first_pass.total_entries # type: ignore[attr-defined] if total_entries < limit: return first_pass + found_key = None for key, value in first_pass.model_dump().items(): if key != "total_entries" and isinstance(value, list): + found_key = key break - entry_list = getattr(first_pass, key) + entry_list = getattr(first_pass, found_key) offset = offset + limit while offset < total_entries: self.response = self.client.get(path, params={**shared_params, "offset": offset}) - entry = data_model.model_validate_json(self.response.content) + entry = safe_validate(self.response.content) offset = offset + limit - entry_list.extend(getattr(entry, key)) - obj = data_model(**{key: entry_list, "total_entries": total_entries}) - return data_model.model_validate(obj.model_dump()) + entry_list.extend(getattr(entry, found_key)) + obj = data_model(**{found_key: entry_list, "total_entries": total_entries}) + return data_model.model_validate(obj.model_dump()) # type: ignore[union-attr] # Login operations @@ -237,7 +276,9 @@ def create_event( # Ensure extra is initialised before sent to API if asset_event_body.extra is None: asset_event_body.extra = {} - self.response = self.client.post("assets/events", json=asset_event_body.model_dump(mode="json")) + self.response = self.client.post( + "assets/events", json=asset_event_body.model_dump(mode="json", exclude_none=True) + ) return AssetEventResponse.model_validate_json(self.response.content) except ServerResponseError as e: raise e @@ -307,7 +348,9 @@ class BackfillOperations(BaseOperations): def create(self, backfill: BackfillPostBody) -> BackfillResponse | ServerResponseError: """Create a backfill.""" try: - self.response = self.client.post("backfills", data=backfill.model_dump(mode="json")) + self.response = self.client.post( + "backfills", data=backfill.model_dump(mode="json", exclude_none=True) + ) return BackfillResponse.model_validate_json(self.response.content) except ServerResponseError as e: raise e @@ -315,7 +358,9 @@ def create(self, backfill: BackfillPostBody) -> BackfillResponse | ServerRespons def create_dry_run(self, backfill: BackfillPostBody) -> BackfillResponse | ServerResponseError: """Create a dry run backfill.""" try: - self.response = self.client.post("backfills/dry_run", data=backfill.model_dump(mode="json")) + self.response = self.client.post( + "backfills/dry_run", data=backfill.model_dump(mode="json", exclude_none=True) + ) return BackfillResponse.model_validate_json(self.response.content) except ServerResponseError as e: raise e @@ -399,7 +444,9 @@ def create( ) -> ConnectionResponse | ServerResponseError: """Create a connection.""" try: - self.response = self.client.post("connections", json=connection.model_dump(mode="json")) + self.response = self.client.post( + "connections", json=connection.model_dump(mode="json", exclude_none=True) + ) return ConnectionResponse.model_validate_json(self.response.content) except ServerResponseError as e: raise e @@ -618,7 +665,7 @@ def list(self) -> PoolCollectionResponse | ServerResponseError: def create(self, pool: PoolBody) -> PoolResponse | ServerResponseError: """Create a pool.""" try: - self.response = self.client.post("pools", json=pool.model_dump(mode="json")) + self.response = self.client.post("pools", json=pool.model_dump(mode="json", exclude_none=True)) return PoolResponse.model_validate_json(self.response.content) except ServerResponseError as e: raise e @@ -676,7 +723,9 @@ def list(self) -> VariableCollectionResponse | ServerResponseError: def create(self, variable: VariableBody) -> VariableResponse | ServerResponseError: """Create a variable.""" try: - self.response = self.client.post("variables", json=variable.model_dump(mode="json")) + self.response = self.client.post( + "variables", json=variable.model_dump(mode="json", exclude_none=True) + ) return VariableResponse.model_validate_json(self.response.content) except ServerResponseError as e: raise e @@ -786,7 +835,7 @@ def add( try: self.response = self.client.post( f"dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries", - json=body.model_dump(mode="json", exclude_unset=True), + json=body.model_dump(mode="json", exclude_unset=True, exclude_none=True), ) return XComResponseNative.model_validate_json(self.response.content) except ServerResponseError as e: @@ -814,7 +863,7 @@ def edit( try: self.response = self.client.patch( f"dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries/{key}", - json=body.model_dump(mode="json", exclude_unset=True), + json=body.model_dump(mode="json", exclude_unset=True, exclude_none=True), ) return XComResponseNative.model_validate_json(self.response.content) except ServerResponseError as e: diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py index 28dce22805dd5..bdcd8b2fb9060 100755 --- a/airflow-ctl/src/airflowctl/ctl/cli_config.py +++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py @@ -254,12 +254,11 @@ def string_lower_type(val): help="The DAG ID of the DAG to pause or unpause", ) -# Variable Commands Args -ARG_VARIABLE_ACTION_ON_EXISTING_KEY = Arg( +ARG_ACTION_ON_EXISTING_KEY = Arg( flags=("-a", "--action-on-existing-key"), type=str, default="overwrite", - help="Action to take if we encounter a variable key that already exists.", + help="Action to take if the entity already exists.", choices=("overwrite", "fail", "skip"), ) @@ -842,6 +841,20 @@ def merge_commands( func=lazy_load_command("airflowctl.ctl.commands.auth_command.list_envs"), args=(ARG_OUTPUT,), ), + ActionCommand( + name="token", + help="Generate and print a JWT token for the given credentials", + description=( + "Authenticate with username and password and print the access token to stdout. " + "Username and password are prompted interactively if not provided." + ), + func=lazy_load_command("airflowctl.ctl.commands.auth_command.get_token"), + args=( + ARG_AUTH_URL, + ARG_AUTH_USERNAME, + ARG_AUTH_PASSWORD, + ), + ), ) CONFIG_COMMANDS = ( @@ -865,7 +878,10 @@ def merge_commands( name="import", help="Import connections from a file exported with local CLI.", func=lazy_load_command("airflowctl.ctl.commands.connection_command.import_"), - args=(Arg(flags=("file",), metavar="FILEPATH", help="Connections JSON file"),), + args=( + Arg(flags=("file",), metavar="FILEPATH", help="Connections JSON file"), + ARG_ACTION_ON_EXISTING_KEY, + ), ), ) @@ -895,7 +911,7 @@ def merge_commands( name="import", help="Import pools", func=lazy_load_command("airflowctl.ctl.commands.pool_command.import_"), - args=(ARG_FILE,), + args=(ARG_FILE, ARG_ACTION_ON_EXISTING_KEY), ), ActionCommand( name="export", @@ -913,7 +929,7 @@ def merge_commands( name="import", help="Import variables from a file exported with local CLI.", func=lazy_load_command("airflowctl.ctl.commands.variable_command.import_"), - args=(ARG_FILE, ARG_VARIABLE_ACTION_ON_EXISTING_KEY), + args=(ARG_FILE, ARG_ACTION_ON_EXISTING_KEY), ), ) diff --git a/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py b/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py index 809cde294e8ca..236b8d5c6b8de 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py @@ -104,6 +104,23 @@ def login(args, api_client=NEW_API_CLIENT) -> None: rich.print(success_message) +@provide_api_client(kind=ClientKind.AUTH) +def get_token(args, api_client=NEW_API_CLIENT) -> None: + """Generate and print a JWT token for the given credentials to stdout.""" + username = args.username or input("Username: ") + password = args.password or getpass.getpass("Password: ") + + try: + api_client.refresh_base_url(base_url=args.api_url, kind=ClientKind.AUTH) + login_response = api_client.login.login_with_username_and_password( + LoginBody(username=username, password=password) + ) + print(login_response.access_token) + except Exception as e: + rich.print(f"[red]Token generation failed: {e}[/red]") + sys.exit(1) + + def list_envs(args) -> None: """List all CLI environments that the user has logged into.""" # Get AIRFLOW_HOME diff --git a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py index b689083faa28e..b1a8a820998ac 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py @@ -62,7 +62,7 @@ def import_(args, api_client=NEW_API_CLIENT) -> None: connection_create_action = BulkCreateActionConnectionBody( action="create", entities=list(connections_data.values()), - action_on_existence=BulkActionOnExistence("fail"), + action_on_existence=BulkActionOnExistence(args.action_on_existing_key), ) response = api_client.connections.bulk(BulkBodyConnectionBody(actions=[connection_create_action])) if response.create.errors: diff --git a/airflow-ctl/src/airflowctl/ctl/commands/pool_command.py b/airflow-ctl/src/airflowctl/ctl/commands/pool_command.py index 1437b4f78de7c..08e56eed87b0b 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/pool_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/pool_command.py @@ -41,7 +41,7 @@ def import_(args, api_client: Client = NEW_API_CLIENT) -> None: if not filepath.exists(): raise SystemExit(f"Missing pools file {args.file}") - success, errors = _import_helper(api_client, filepath) + success, errors = _import_helper(api_client, filepath, BulkActionOnExistence(args.action_on_existing_key)) if errors: raise SystemExit(f"Failed to update pool(s): {errors}") rich.print(success) @@ -83,7 +83,7 @@ def export(args, api_client: Client = NEW_API_CLIENT) -> None: raise SystemExit(f"Failed to export pools: {e}") -def _import_helper(api_client: Client, filepath: Path): +def _import_helper(api_client: Client, filepath: Path, action_on_existence: BulkActionOnExistence): """Help import pools from the json file.""" try: with open(filepath) as f: @@ -113,7 +113,7 @@ def _import_helper(api_client: Client, filepath: Path): BulkCreateActionPoolBody( action="create", entities=pools_to_update, - action_on_existence=BulkActionOnExistence.FAIL, + action_on_existence=action_on_existence, ) ] ) diff --git a/airflow-ctl/tests/airflow_ctl/api/test_client.py b/airflow-ctl/tests/airflow_ctl/api/test_client.py index f79322d16fb74..0617d62276a1c 100644 --- a/airflow-ctl/tests/airflow_ctl/api/test_client.py +++ b/airflow-ctl/tests/airflow_ctl/api/test_client.py @@ -25,6 +25,7 @@ import httpx import pytest +import time_machine from httpx import URL from airflowctl.api.client import Client, ClientKind, Credentials, _bounded_get_new_password @@ -32,6 +33,15 @@ from airflowctl.exceptions import AirflowCtlCredentialNotFoundException, AirflowCtlKeyringException +def make_client_w_responses(responses: list[httpx.Response]) -> Client: + """Get a client with custom responses.""" + + def handle_request(request: httpx.Request) -> httpx.Response: + return responses.pop(0) + + return Client(base_url="", token="", mounts={"'http://": httpx.MockTransport(handle_request)}) + + @pytest.fixture(autouse=True) def unique_config_dir(): temp_dir = tempfile.mkdtemp() @@ -314,3 +324,55 @@ def test_save_skips_patch_for_non_encrypted_backend(self, mock_keyring): assert not hasattr(mock_backend, "_get_new_password") mock_keyring.set_password.assert_called_once_with("airflowctl", "api_token_production", "token") + + def test_retry_handling_unrecoverable_error(self): + with time_machine.travel("2023-01-01T00:00:00Z", tick=False): + responses: list[httpx.Response] = [ + *[httpx.Response(500, text="Internal Server Error")] * 6, + httpx.Response(200, json={"detail": "Recovered from error - but will fail before"}), + httpx.Response(400, json={"detail": "Should not get here"}), + ] + client = make_client_w_responses(responses) + + with pytest.raises(httpx.HTTPStatusError) as err: + client.get("http://error") + assert not isinstance(err.value, ServerResponseError) + assert len(responses) == 5 + + def test_retry_handling_recovered(self): + with time_machine.travel("2023-01-01T00:00:00Z", tick=False): + responses: list[httpx.Response] = [ + *[httpx.Response(500, text="Internal Server Error")] * 2, + httpx.Response(200, json={"detail": "Recovered from error"}), + httpx.Response(400, json={"detail": "Should not get here"}), + ] + client = make_client_w_responses(responses) + + response = client.get("http://error") + assert response.status_code == 200 + assert len(responses) == 1 + + def test_retry_handling_non_retry_error(self): + with time_machine.travel("2023-01-01T00:00:00Z", tick=False): + responses: list[httpx.Response] = [ + httpx.Response(422, json={"detail": "Somehow this is a bad request"}), + httpx.Response(400, json={"detail": "Should not get here"}), + ] + client = make_client_w_responses(responses) + + with pytest.raises(ServerResponseError) as err: + client.get("http://error") + assert len(responses) == 1 + assert err.value.args == ("Client error message: {'detail': 'Somehow this is a bad request'}",) + + def test_retry_handling_ok(self): + with time_machine.travel("2023-01-01T00:00:00Z", tick=False): + responses: list[httpx.Response] = [ + httpx.Response(200, json={"detail": "Recovered from error"}), + httpx.Response(400, json={"detail": "Should not get here"}), + ] + client = make_client_w_responses(responses) + + response = client.get("http://error") + assert response.status_code == 200 + assert len(responses) == 1 diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py b/airflow-ctl/tests/airflow_ctl/api/test_operations.py index 65f058d6656af..f75a3c9678fd2 100644 --- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py +++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py @@ -194,6 +194,38 @@ def test_execute_list(self, total_entries, limit, expected_response): assert expected_response == response + def test_execute_list_sends_limit_to_server(self): + """``limit`` must be included in request params so the server returns + the expected page size. Without it the server uses its own default + (e.g. 100) which causes duplicate entries when ``limit`` differs.""" + mock_client = Mock() + mock_client.get.return_value = Mock( + content=json.dumps({"hellos": [{"name": "hello"}] * 3, "total_entries": 3}) + ) + base_operation = BaseOperations(client=mock_client) + + base_operation.execute_list(path="hello", data_model=HelloCollectionResponse, limit=50) + + call_params = mock_client.get.call_args_list[0] + assert call_params.kwargs["params"]["limit"] == 50 + + def test_execute_list_sends_limit_on_subsequent_pages(self): + """Every paginated request must include ``limit`` so that offset + arithmetic stays consistent with the actual page size returned.""" + mock_client = Mock() + mock_client.get.side_effect = [ + Mock(content=json.dumps({"hellos": [{"name": "a"}, {"name": "b"}], "total_entries": 3})), + Mock(content=json.dumps({"hellos": [{"name": "c"}], "total_entries": 3})), + ] + base_operation = BaseOperations(client=mock_client) + + response = base_operation.execute_list(path="hello", data_model=HelloCollectionResponse, limit=2) + + assert len(response.hellos) == 3 + # Verify limit is sent on both the first and second request + for call in mock_client.get.call_args_list: + assert call.kwargs["params"]["limit"] == 2 + class TestAssetsOperations: asset_id: int = 1 diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py index 02b56eda99b0e..f944e66ab1f24 100644 --- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py +++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py @@ -17,12 +17,14 @@ from __future__ import annotations import json +from unittest import mock from unittest.mock import patch import pytest -from airflowctl.api.client import ClientKind +from airflowctl.api.client import Client, ClientKind from airflowctl.api.datamodels.generated import ( + BulkActionOnExistence, BulkActionResponse, BulkResponse, ConnectionBody, @@ -176,3 +178,47 @@ def test_import_without_extra_field(self, api_client_maker, tmp_path, monkeypatc extra=None, description="", ) + + @pytest.mark.parametrize( + ("action_on_existing_key", "expected_enum"), + [ + ("overwrite", BulkActionOnExistence.OVERWRITE), + ("skip", BulkActionOnExistence.SKIP), + ("fail", BulkActionOnExistence.FAIL), + ], + ) + def test_import_action_on_existing_key(self, tmp_path, action_on_existing_key, expected_enum): + expected_json_path = tmp_path / self.export_file_name + connection_file = { + self.connection_id: { + "conn_type": "test_type", + "host": "test_host", + "extra": "{}", + "connection_id": self.connection_id, + } + } + expected_json_path.write_text(json.dumps(connection_file)) + + mock_client = mock.MagicMock(spec=Client) + mock_response = mock.MagicMock() + mock_response.create.success = [self.connection_id] + mock_response.create.errors = [] + mock_client.connections.bulk.return_value = mock_response + + connection_command.import_( + self.parser.parse_args( + [ + "connections", + "import", + expected_json_path.as_posix(), + "--action-on-existing-key", + action_on_existing_key, + ] + ), + api_client=mock_client, + ) + + mock_client.connections.bulk.assert_called_once() + bulk_body = mock_client.connections.bulk.call_args[0][0] + action = bulk_body.actions[0] + assert action.action_on_existence == expected_enum diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py index 84152d59c813e..0bc2438929454 100644 --- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py +++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py @@ -48,21 +48,21 @@ def test_import_missing_file(self, mock_client, tmp_path): """Test import with missing file.""" non_existent = tmp_path / "non_existent.json" with pytest.raises(SystemExit, match=f"Missing pools file {non_existent}"): - pool_command.import_(mock.MagicMock(file=non_existent)) + pool_command.import_(mock.MagicMock(file=non_existent, action_on_existing_key="fail")) def test_import_invalid_json(self, mock_client, tmp_path): """Test import with invalid JSON file.""" invalid_json = tmp_path / "invalid.json" invalid_json.write_text("invalid json") with pytest.raises(SystemExit, match="Invalid json file"): - pool_command.import_(mock.MagicMock(file=invalid_json)) + pool_command.import_(mock.MagicMock(file=invalid_json, action_on_existing_key="fail")) def test_import_invalid_pool_config(self, mock_client, tmp_path): """Test import with invalid pool configuration.""" invalid_pool = tmp_path / "invalid_pool.json" invalid_pool.write_text(json.dumps([{"invalid": "config"}])) with pytest.raises(SystemExit, match="Invalid pool configuration: {'invalid': 'config'}"): - pool_command.import_(mock.MagicMock(file=invalid_pool)) + pool_command.import_(mock.MagicMock(file=invalid_pool, action_on_existing_key="fail")) def test_import_success(self, mock_client, tmp_path, capsys): """Test successful pool import.""" @@ -87,7 +87,7 @@ def test_import_success(self, mock_client, tmp_path, capsys): mock_client.pools.bulk.return_value = mock_bulk_builder - pool_command.import_(mock.MagicMock(file=pools_file)) + pool_command.import_(mock.MagicMock(file=pools_file, action_on_existing_key="fail")) # Verify bulk operation was called with correct parameters mock_client.pools.bulk.assert_called_once() @@ -108,6 +108,34 @@ def test_import_success(self, mock_client, tmp_path, capsys): captured = capsys.readouterr() assert str(["test_pool"]) in captured.out + @pytest.mark.parametrize( + ("action_on_existing_key", "expected_enum"), + [ + ("overwrite", BulkActionOnExistence.OVERWRITE), + ("skip", BulkActionOnExistence.SKIP), + ("fail", BulkActionOnExistence.FAIL), + ], + ) + def test_import_action_on_existing_key( + self, mock_client, tmp_path, action_on_existing_key, expected_enum + ): + """Test that --action-on-existing-key is passed through to the bulk API.""" + pools_file = tmp_path / "pools.json" + pools_file.write_text(json.dumps([{"name": "test_pool", "slots": 1}])) + + mock_response = mock.MagicMock() + mock_response.success = ["test_pool"] + mock_response.errors = [] + mock_bulk_builder = mock.MagicMock() + mock_bulk_builder.create = mock_response + mock_client.pools.bulk.return_value = mock_bulk_builder + + pool_command.import_(mock.MagicMock(file=pools_file, action_on_existing_key=action_on_existing_key)) + + call_args = mock_client.pools.bulk.call_args[1] + action = call_args["pools"].actions[0] + assert action.action_on_existence == expected_enum + class TestPoolExportCommand: """Test cases for pool export command.""" diff --git a/airflow-e2e-tests/scripts/init-aws.sh b/airflow-e2e-tests/scripts/init-aws.sh index ca5a1cfe0783e..4c78d873570d4 100755 --- a/airflow-e2e-tests/scripts/init-aws.sh +++ b/airflow-e2e-tests/scripts/init-aws.sh @@ -17,4 +17,5 @@ # under the License. aws --endpoint-url=http://localstack:4566 s3 mb s3://test-airflow-logs +aws --endpoint-url=http://localstack:4566 s3 mb s3://test-xcom-objectstorage-backend aws --endpoint-url=http://localstack:4566 s3 ls diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py index 24caa32a21317..ad071d4e02b8d 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py @@ -36,6 +36,7 @@ LOCALSTACK_PATH, LOGS_FOLDER, TEST_REPORT_FILE, + XCOM_BUCKET, ) from tests_common.test_utils.fernet import generate_fernet_key_string @@ -48,13 +49,18 @@ class _E2ETestState: airflow_logs_path: Path | None = None -def _setup_s3_integration(dot_env_file, tmp_dir): +def _copy_localstack_files(tmp_dir): + """Copy localstack compose file and init script into the temp directory.""" copyfile(LOCALSTACK_PATH, tmp_dir / "localstack.yml") copyfile(AWS_INIT_PATH, tmp_dir / "init-aws.sh") current_permissions = os.stat(tmp_dir / "init-aws.sh").st_mode os.chmod(tmp_dir / "init-aws.sh", current_permissions | 0o111) + +def _setup_s3_integration(dot_env_file, tmp_dir): + _copy_localstack_files(tmp_dir) + dot_env_file.write_text( f"AIRFLOW_UID={os.getuid()}\n" "AWS_DEFAULT_REGION=us-east-1\n" @@ -68,6 +74,27 @@ def _setup_s3_integration(dot_env_file, tmp_dir): os.environ["ENV_FILE_PATH"] = str(dot_env_file) +def _setup_xcom_object_storage_integration(dot_env_file, tmp_dir): + _copy_localstack_files(tmp_dir) + + dot_env_file.write_text( + f"AIRFLOW_UID={os.getuid()}\n" + # XComObjectStorageBackend requires AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as env vars + # because `universal-path` uses boto3's native S3 client, which relies on environment variables + # for authentication rather than parsing credentials from the connection URI + "AWS_ACCESS_KEY_ID=test\n" + "AWS_SECRET_ACCESS_KEY=test\n" + "AWS_DEFAULT_REGION=us-east-1\n" + "AWS_ENDPOINT_URL_S3=http://localstack:4566\n" + "AIRFLOW_CONN_AWS_DEFAULT=aws://test:test@\n" + "AIRFLOW__CORE__XCOM_BACKEND=airflow.providers.common.io.xcom.backend.XComObjectStorageBackend\n" + f"AIRFLOW__COMMON_IO__XCOM_OBJECTSTORAGE_PATH=s3://aws_default@{XCOM_BUCKET}/xcom\n" + "AIRFLOW__COMMON_IO__XCOM_OBJECTSTORAGE_THRESHOLD=0\n" + "_PIP_ADDITIONAL_REQUIREMENTS=apache-airflow-providers-amazon[s3fs]\n" + ) + os.environ["ENV_FILE_PATH"] = str(dot_env_file) + + def spin_up_airflow_environment(tmp_path_factory: pytest.TempPathFactory): tmp_dir = tmp_path_factory.mktemp("airflow-e2e-tests") @@ -97,6 +124,9 @@ def spin_up_airflow_environment(tmp_path_factory: pytest.TempPathFactory): if E2E_TEST_MODE == "remote_log": compose_file_names.append("localstack.yml") _setup_s3_integration(dot_env_file, tmp_dir) + elif E2E_TEST_MODE == "xcom_object_storage": + compose_file_names.append("localstack.yml") + _setup_xcom_object_storage_integration(dot_env_file, tmp_dir) # # Please Do not use this Fernet key in any deployments! Please generate your own key. diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py index a208487da8b5d..764b4e56dc583 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py @@ -42,3 +42,6 @@ LOCALSTACK_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "docker" / "localstack.yml" E2E_TEST_MODE = os.environ.get("E2E_TEST_MODE", "basic") AWS_INIT_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "scripts" / "init-aws.sh" + +# s3 bucket name for XComObjectStorageBackend tests. This bucket will be created in the `init-aws.sh` script that is run as part of the LocalStack container initialization. +XCOM_BUCKET = "test-xcom-objectstorage-backend" diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/e2e_test_utils/clients.py b/airflow-e2e-tests/tests/airflow_e2e_tests/e2e_test_utils/clients.py index 32abeb5331d78..3a11101d4acd4 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/e2e_test_utils/clients.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/e2e_test_utils/clients.py @@ -20,6 +20,7 @@ from datetime import datetime, timezone from functools import cached_property +import boto3 import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry @@ -31,19 +32,41 @@ ) +def get_s3_client(): + """Return a boto3 S3 client configured to use the local LocalStack endpoint.""" + return boto3.client( + "s3", + endpoint_url="http://localhost:4566", + aws_access_key_id="test", + aws_secret_access_key="test", + region_name="us-east-1", + ) + + +def create_request_session_with_retries(status_forcelist: list[int]): + """Create a requests Session with retry logic for handling transient errors.""" + Retry.DEFAULT_BACKOFF_MAX = 32 + retry_strategy = Retry( + total=10, + backoff_factor=1, + status_forcelist=status_forcelist, + ) + session = requests.Session() + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount("http://", adapter) + session.mount("https://", adapter) + return session + + class AirflowClient: """Client for interacting with the Airflow REST API.""" def __init__(self): - self.session = requests.Session() + self.session = create_request_session_with_retries(status_forcelist=[429]) @cached_property def token(self): - Retry.DEFAULT_BACKOFF_MAX = 32 - retry = Retry(total=10, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504]) - session = requests.Session() - session.mount("http://", HTTPAdapter(max_retries=retry)) - session.mount("https://", HTTPAdapter(max_retries=retry)) + session = create_request_session_with_retries(status_forcelist=[429, 500, 502, 503, 504]) api_server_url = DOCKER_COMPOSE_HOST_PORT if not api_server_url.startswith(("http://", "https://")): @@ -121,11 +144,23 @@ def trigger_dag_and_wait(self, dag_id: str, json=None): run_id=resp["dag_run_id"], ) - def get_task_logs(self, dag_id: str, run_id: str, task_id: str, try_number: int = 1): + def get_task_instances(self, dag_id: str, run_id: str): + """Get task instances for a given DAG run.""" + return self._make_request( + method="GET", + endpoint=f"dags/{dag_id}/dagRuns/{run_id}/taskInstances", + ) + + def get_task_logs( + self, dag_id: str, run_id: str, task_id: str, try_number: int = 1, map_index: int | None = None + ): """Get task logs via API.""" + endpoint = f"dags/{dag_id}/dagRuns/{run_id}/taskInstances/{task_id}/logs/{try_number}" + if map_index is not None: + endpoint += f"?map_index={map_index}" return self._make_request( method="GET", - endpoint=f"dags/{dag_id}/dagRuns/{run_id}/taskInstances/{task_id}/logs/{try_number}", + endpoint=endpoint, ) diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/remote_log_tests/test_remote_logging.py b/airflow-e2e-tests/tests/airflow_e2e_tests/remote_log_tests/test_remote_logging.py index 52f19c5a1f5ff..9260f0abe0370 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/remote_log_tests/test_remote_logging.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/remote_log_tests/test_remote_logging.py @@ -19,10 +19,9 @@ import time from datetime import datetime, timezone -import boto3 import pytest -from airflow_e2e_tests.e2e_test_utils.clients import AirflowClient +from airflow_e2e_tests.e2e_test_utils.clients import AirflowClient, get_s3_client class TestRemoteLogging: @@ -56,15 +55,10 @@ def test_remote_logging_s3(self): # This bucket will be created part of the docker-compose setup in bucket_name = "test-airflow-logs" - s3_client = boto3.client( - "s3", - endpoint_url="http://localhost:4566", - aws_access_key_id="test", - aws_secret_access_key="test", - region_name="us-east-1", - ) + s3_client = get_s3_client() # Wait for logs to be available in S3 before we call `get_task_logs` + contents = [] for _ in range(self.max_retries): response = s3_client.list_objects_v2(Bucket=bucket_name) contents = response.get("Contents", []) diff --git a/task-sdk/src/airflow/sdk/observability/traces/__init__.py b/airflow-e2e-tests/tests/airflow_e2e_tests/xcom_object_storage_tests/__init__.py similarity index 99% rename from task-sdk/src/airflow/sdk/observability/traces/__init__.py rename to airflow-e2e-tests/tests/airflow_e2e_tests/xcom_object_storage_tests/__init__.py index 217e5db960782..13a83393a9124 100644 --- a/task-sdk/src/airflow/sdk/observability/traces/__init__.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/xcom_object_storage_tests/__init__.py @@ -1,4 +1,3 @@ -# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/xcom_object_storage_tests/test_xcom_object_storage_backend.py b/airflow-e2e-tests/tests/airflow_e2e_tests/xcom_object_storage_tests/test_xcom_object_storage_backend.py new file mode 100644 index 0000000000000..8ddb9738b6b29 --- /dev/null +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/xcom_object_storage_tests/test_xcom_object_storage_backend.py @@ -0,0 +1,89 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import time +from datetime import datetime, timezone +from pprint import pprint +from uuid import uuid4 + +import pytest + +from airflow_e2e_tests.constants import XCOM_BUCKET +from airflow_e2e_tests.e2e_test_utils.clients import AirflowClient, get_s3_client + + +class TestXComObjectStorageBackend: + airflow_client = AirflowClient() + dag_id = "example_xcom_test" + retry_interval_in_seconds = 5 + max_retries = 12 + + def test_dag_succeeds_and_xcom_values_stored_in_s3(self): + """Test that a DAG using XComObjectStorageBackend completes successfully and persists XCom values to S3.""" + self.airflow_client.un_pause_dag(self.dag_id) + + trigger_resp = self.airflow_client.trigger_dag( + self.dag_id, + json={ + "dag_run_id": f"test_xcom_object_storage_backend_{uuid4()}", + "logical_date": datetime.now(timezone.utc).isoformat(), + }, + ) + dag_run_id = trigger_resp["dag_run_id"] + state = self.airflow_client.wait_for_dag_run( + dag_id=self.dag_id, + run_id=dag_run_id, + ) + + # try to get all the logs to help debugging + if state != "success": + task_instances_resp = self.airflow_client.get_task_instances(self.dag_id, dag_run_id) + for task_instance in task_instances_resp["task_instances"]: + task_id = task_instance["task_id"] + try_number = task_instance["try_number"] + try: + print(f"\nLogs for task {task_id} (try {try_number}):") + task_logs_resp = self.airflow_client.get_task_logs( + dag_id=self.dag_id, task_id=task_id, run_id=dag_run_id, try_number=try_number + ) + pprint(task_logs_resp) + except Exception as e: + print(f"Could not get logs for task {task_id} (try {try_number}): {e}") + + assert state == "success", f"DAG {self.dag_id} did not complete successfully. Final state: {state}" + + s3_client = get_s3_client() + + contents = [] + for _ in range(self.max_retries): + response = s3_client.list_objects_v2(Bucket=XCOM_BUCKET) + contents = response.get("Contents", []) + if contents: + break + + print(f"No XCom objects found in S3 bucket {XCOM_BUCKET!r} yet. Retrying...") + time.sleep(self.retry_interval_in_seconds) + + if not contents: + pytest.fail( + f"Expected XCom objects in S3 bucket {XCOM_BUCKET!r}, but bucket is empty.\n" + f"List Objects Response: {response}" + ) + + keys = [obj["Key"] for obj in contents] + print(f"Found {len(keys)} XCom object(s) in S3: {keys}") diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 303d23610c697..5e020b658ac5c 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -20,7 +20,7 @@ apiVersion: v2 name: airflow version: 1.20.0-dev -appVersion: 3.1.7 +appVersion: 3.1.8 description: The official Helm chart to deploy Apache Airflow, a platform to programmatically author, schedule, and monitor workflows home: https://airflow.apache.org/ @@ -47,21 +47,21 @@ annotations: url: https://airflow.apache.org/docs/helm-chart/1.20.0/ artifacthub.io/screenshots: | - title: Home Page - url: https://airflow.apache.org/docs/apache-airflow/3.1.7/_images/home_dark.png + url: https://airflow.apache.org/docs/apache-airflow/3.1.8/_images/home_dark.png - title: DAG Overview Dashboard - url: https://airflow.apache.org/docs/apache-airflow/3.1.7/_images/dag_overview_dashboard.png + url: https://airflow.apache.org/docs/apache-airflow/3.1.8/_images/dag_overview_dashboard.png - title: DAGs View - url: https://airflow.apache.org/docs/apache-airflow/3.1.7/_images/dags.png + url: https://airflow.apache.org/docs/apache-airflow/3.1.8/_images/dags.png - title: Assets View - url: https://airflow.apache.org/docs/apache-airflow/3.1.7/_images/asset_view.png + url: https://airflow.apache.org/docs/apache-airflow/3.1.8/_images/asset_view.png - title: Grid View - url: https://airflow.apache.org/docs/apache-airflow/3.1.7/_images/dag_overview_grid.png + url: https://airflow.apache.org/docs/apache-airflow/3.1.8/_images/dag_overview_grid.png - title: Graph View - url: https://airflow.apache.org/docs/apache-airflow/3.1.7/_images/dag_overview_graph.png + url: https://airflow.apache.org/docs/apache-airflow/3.1.8/_images/dag_overview_graph.png - title: Variable View - url: https://airflow.apache.org/docs/apache-airflow/3.1.7/_images/variable_hidden.png + url: https://airflow.apache.org/docs/apache-airflow/3.1.8/_images/variable_hidden.png - title: Code View - url: https://airflow.apache.org/docs/apache-airflow/3.1.7/_images/dag_overview_code.png + url: https://airflow.apache.org/docs/apache-airflow/3.1.8/_images/dag_overview_code.png artifacthub.io/changes: | - description: Add ``PodDisruptionBudget`` for Dag Processor kind: added diff --git a/chart/docs/customizing-labels.rst b/chart/docs/customizing-labels.rst index 0fcabeee5021f..6cdf2a3ab6f18 100644 --- a/chart/docs/customizing-labels.rst +++ b/chart/docs/customizing-labels.rst @@ -15,13 +15,16 @@ specific language governing permissions and limitations under the License. -Customizing Labels for Pods -=========================== +Customizing Labels and Annotations for Pods +=========================================== + +Customizing Pod Labels +---------------------- The Helm Chart allows you to customize labels for your Airflow objects. You can set global labels that apply to all objects and pods defined in the chart, as well as component-specific labels for individual Airflow components. Global Labels -------------- +~~~~~~~~~~~~~ Global labels can be set using the ``labels`` parameter in your values file. These labels will be applied to all Airflow objects and pods defined in the chart: @@ -32,7 +35,7 @@ Global labels can be set using the ``labels`` parameter in your values file. The environment: production Component-Specific Labels -------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~ You can also set specific labels for individual Airflow components, which will be merged with the global labels. Component-specific labels take precedence over global labels, allowing you to override them as needed. @@ -59,3 +62,66 @@ For example, to add specific labels to different components: apiServer: labels: role: ui + +Customizing Pod Annotations +--------------------------- + +Pod annotations can be customized similarly to labels using ``podAnnotations`` and ``airflowPodAnnotations``. + +Global Pod Annotations +~~~~~~~~~~~~~~~~~~~~~~ + +Global pod annotations can be set using ``airflowPodAnnotations``. These are applied to all Airflow component pods (scheduler, api-server/webserver, triggerer, dag-processor and workers): + +.. code-block:: yaml + :caption: values.yaml + + airflowPodAnnotations: + example.com/team: data-platform + +Component-Specific Pod Annotations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each component also supports its own ``podAnnotations``. Component-specific annotations take precedence over global ones: + +.. code-block:: yaml + :caption: values.yaml + + scheduler: + podAnnotations: + example.com/component: scheduler + +Templated Pod Annotations +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Both ``airflowPodAnnotations`` and ``podAnnotations`` support Helm template expressions. This allows annotations to reference release metadata or compute checksums of chart-managed resources, so that pods automatically restart when those resources change. + +For example, to restart scheduler pods whenever the chart's extra ConfigMaps change: + +.. code-block:: yaml + :caption: values.yaml + + extraConfigMaps: + my-listener-config: + data: | + listener.py: ... + + scheduler: + podAnnotations: + checksum/extra-configmaps: '{{ include (print $.Template.BasePath "/configmaps/extra-configmaps.yaml") . | sha256sum }}' + +You can also reference release metadata: + +.. code-block:: yaml + :caption: values.yaml + + airflowPodAnnotations: + release: '{{ .Release.Name }}' + +.. note:: + + The ``include``/``sha256sum`` pattern only works for resources managed by this chart + (e.g., those created via ``extraConfigMaps`` or ``extraSecrets``). + For ConfigMaps or Secrets created outside the chart, consider using a tool like + `Stakater Reloader `__ to trigger pod restarts + automatically. diff --git a/chart/files/pod-template-file.kubernetes-helm-yaml b/chart/files/pod-template-file.kubernetes-helm-yaml index 4fa1413037ed9..8a93d8f7f10bf 100644 --- a/chart/files/pod-template-file.kubernetes-helm-yaml +++ b/chart/files/pod-template-file.kubernetes-helm-yaml @@ -17,7 +17,7 @@ under the License. */}} --- -{{- $nodeSelector := or .Values.workers.nodeSelector .Values.nodeSelector }} +{{- $nodeSelector := or .Values.workers.kubernetes.nodeSelector .Values.workers.nodeSelector .Values.nodeSelector }} {{- $affinity := or .Values.workers.affinity .Values.affinity }} {{- $tolerations := or .Values.workers.tolerations .Values.tolerations }} {{- $topologySpreadConstraints := or .Values.workers.topologySpreadConstraints .Values.topologySpreadConstraints }} @@ -43,7 +43,7 @@ metadata: {{- mustMerge .Values.workers.labels .Values.labels | toYaml | nindent 4 }} {{- end }} annotations: - {{- toYaml $podAnnotations | nindent 4 }} + {{- tpl (toYaml $podAnnotations) . | nindent 4 }} {{- if or .Values.workers.kubernetes.kerberosInitContainer.enabled .Values.workers.kerberosInitContainer.enabled }} checksum/kerberos-keytab: {{ include (print $.Template.BasePath "/secrets/kerberos-keytab-secret.yaml") . | sha256sum }} {{- end }} @@ -131,7 +131,7 @@ spec: {{- if or .Values.workers.kubernetes.command .Values.workers.command }} command: {{ tpl (toYaml (.Values.workers.kubernetes.command | default .Values.workers.command)) . | nindent 8 }} {{- end }} - resources: {{- toYaml .Values.workers.resources | nindent 8 }} + resources: {{- toYaml (.Values.workers.kubernetes.resources | default .Values.workers.resources) | nindent 8 }} volumeMounts: - mountPath: {{ template "airflow_logs" . }} name: logs @@ -233,7 +233,7 @@ spec: {{- if $schedulerName }} schedulerName: {{ $schedulerName }} {{- end }} - terminationGracePeriodSeconds: {{ .Values.workers.terminationGracePeriodSeconds }} + terminationGracePeriodSeconds: {{ .Values.workers.kubernetes.terminationGracePeriodSeconds | default .Values.workers.terminationGracePeriodSeconds }} tolerations: {{- toYaml $tolerations | nindent 4 }} topologySpreadConstraints: {{- toYaml $topologySpreadConstraints | nindent 4 }} serviceAccountName: {{ include "worker.serviceAccountName" . }} diff --git a/chart/newsfragments/61890.significant.rst b/chart/newsfragments/61890.significant.rst new file mode 100644 index 0000000000000..e8208570c2af8 --- /dev/null +++ b/chart/newsfragments/61890.significant.rst @@ -0,0 +1 @@ +``workers.resources`` section is now deprecated in favor of ``workers.celery.resources`` and ``workers.kubernetes.resources``. Please update your configuration accordingly. diff --git a/chart/newsfragments/62334.significant.rst b/chart/newsfragments/62334.significant.rst new file mode 100644 index 0000000000000..924e77869e43b --- /dev/null +++ b/chart/newsfragments/62334.significant.rst @@ -0,0 +1,5 @@ +As Git-Sync is not service-type object, the readiness probe will be removed. To enable feature behaviour set ``dags.gitSync.recommendedProbeSetting`` to ``true``. Section itself will be removed in future release as to not break setups during upgrades. + +As Git-Sync has dedicated liveness service, the liveness probe behaviour will be changed. To enable feature behaviour set ``dags.gitSync.recommendedProbeSetting`` to ``true``. + +Please update your configuration accordingly. diff --git a/chart/newsfragments/63019.feature.rst b/chart/newsfragments/63019.feature.rst new file mode 100644 index 0000000000000..f91881be4bff6 --- /dev/null +++ b/chart/newsfragments/63019.feature.rst @@ -0,0 +1 @@ +Support Helm template expressions in ``podAnnotations`` and ``airflowPodAnnotations`` values. diff --git a/chart/newsfragments/63392.significant.rst b/chart/newsfragments/63392.significant.rst new file mode 100644 index 0000000000000..51692b1568dca --- /dev/null +++ b/chart/newsfragments/63392.significant.rst @@ -0,0 +1,3 @@ +Default Airflow image is updated to ``3.1.8`` + +The default Airflow image that is used with the Chart is now ``3.1.8``, previously it was ``3.1.7``. diff --git a/chart/templates/NOTES.txt b/chart/templates/NOTES.txt index 79d967919dd9b..5e61762a1a673 100644 --- a/chart/templates/NOTES.txt +++ b/chart/templates/NOTES.txt @@ -196,6 +196,14 @@ https://airflow.apache.org/docs/helm-chart/stable/production-guide.html#knownhos {{- end }} +{{- if and .Values.dags.gitSync.enabled .Values.dags.gitSync.readinessProbe }} + + DEPRECATION WARNING: + `dags.gitSync.readinessProbe` section has been removed as Git-Sync is not service-type object. + Please remove overwrite values of section, if defined, as support it will be removed in a future release. + +{{- end }} + {{- if .Values.flower.extraNetworkPolicies }} DEPRECATION WARNING: @@ -493,6 +501,14 @@ DEPRECATION WARNING: {{- end }} +{{- if not (empty .Values.workers.resources) }} + + DEPRECATION WARNING: + `workers.resources` has been renamed to `workers.celery.resources`/`workers.kubernetes.resources`. + Please change your values as support for the old name will be dropped in a future release. + +{{- end }} + {{- if not (empty .Values.webserver.defaultUser) }} DEPRECATION WARNING: @@ -501,6 +517,14 @@ DEPRECATION WARNING: {{- end }} +{{- if not .Values.dags.gitSync.recommendedProbeSetting }} + + DEPRECATION WARNING: + Dags Git-Sync bevaiour with `dags.gitSync.recommendedProbeSetting` equal `false` is deprecated and will be removed in future. + Please change your values as support for the old name will be dropped in a future release. + +{{- end }} + {{- if not (or .Values.webserverSecretKey .Values.webserverSecretKeySecretName .Values.apiSecretKey .Values.apiSecretKeySecretName) }} {{ if (semverCompare ">=3.0.0" .Values.airflowVersion) }} ##################################################### diff --git a/chart/templates/_helpers.yaml b/chart/templates/_helpers.yaml index f3f92a87c42c2..42848a9e22e14 100644 --- a/chart/templates/_helpers.yaml +++ b/chart/templates/_helpers.yaml @@ -299,17 +299,43 @@ If release name contains chart name it will be used as a full name. value: "true" - name: GITSYNC_ONE_TIME value: "true" + {{- else }} + - name: GIT_SYNC_HTTP_BIND + value: ":{{ .Values.dags.gitSync.httpPort }}" + - name: GITSYNC_HTTP_BIND + value: ":{{ .Values.dags.gitSync.httpPort }}" {{- end }} {{- with .Values.dags.gitSync.env }} {{- toYaml . | nindent 4 }} {{- end }} resources: {{ toYaml .Values.dags.gitSync.resources | nindent 4 }} - {{- if and .Values.dags.gitSync.livenessProbe (not .is_init) }} + {{- if not .is_init }} + {{- if .Values.dags.gitSync.startupProbe.enabled }} + startupProbe: + httpGet: + path: / + port: {{ .Values.dags.gitSync.httpPort }} + timeoutSeconds: {{ .Values.dags.gitSync.startupProbe.timeoutSeconds }} + initialDelaySeconds: {{ .Values.dags.gitSync.startupProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.dags.gitSync.startupProbe.periodSeconds }} + failureThreshold: {{ .Values.dags.gitSync.startupProbe.failureThreshold }} + {{- end }} + {{- if and .Values.dags.gitSync.recommendedProbeSetting (hasKey .Values.dags.gitSync.livenessProbe "enabled") .Values.dags.gitSync.livenessProbe.enabled }} + livenessProbe: + httpGet: + path: / + port: {{ .Values.dags.gitSync.httpPort }} + timeoutSeconds: {{ .Values.dags.gitSync.livenessProbe.timeoutSeconds | default 1 }} + initialDelaySeconds: {{ .Values.dags.gitSync.livenessProbe.initialDelaySeconds | default 0 }} + periodSeconds: {{ .Values.dags.gitSync.livenessProbe.periodSeconds | default 5 }} + failureThreshold: {{ .Values.dags.gitSync.livenessProbe.failureThreshold | default 10 }} + {{- else if .Values.dags.gitSync.livenessProbe }} livenessProbe: {{ tpl (toYaml .Values.dags.gitSync.livenessProbe) . | nindent 4 }} {{- end }} - {{- if and .Values.dags.gitSync.readinessProbe (not .is_init) }} + {{- if and .Values.dags.gitSync.readinessProbe (not .Values.dags.gitSync.recommendedProbeSetting) }} readinessProbe: {{ tpl (toYaml .Values.dags.gitSync.readinessProbe) . | nindent 4 }} {{- end }} + {{- end }} volumeMounts: - name: dags mountPath: /git diff --git a/chart/templates/api-server/api-server-deployment.yaml b/chart/templates/api-server/api-server-deployment.yaml index 0d3d8c0cdd38c..29a84c74ed5ae 100644 --- a/chart/templates/api-server/api-server-deployment.yaml +++ b/chart/templates/api-server/api-server-deployment.yaml @@ -47,7 +47,9 @@ metadata: annotations: {{- toYaml .Values.apiServer.annotations | nindent 4 }} {{- end }} spec: + {{- if not .Values.apiServer.hpa.enabled }} replicas: {{ .Values.apiServer.replicas }} + {{- end }} {{- if ne $revisionHistoryLimit "" }} revisionHistoryLimit: {{ $revisionHistoryLimit }} {{- end }} @@ -91,10 +93,10 @@ spec: checksum/jwt-secret: {{ include (print $.Template.BasePath "/secrets/jwt-secret.yaml") . | sha256sum }} {{- end }} {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.apiServer.podAnnotations }} - {{- toYaml .Values.apiServer.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.apiServer.podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.apiServer.hostAliases }} diff --git a/chart/templates/api-server/api-server-hpa.yaml b/chart/templates/api-server/api-server-hpa.yaml index b8830d4b9b1e1..ee714efd9180d 100644 --- a/chart/templates/api-server/api-server-hpa.yaml +++ b/chart/templates/api-server/api-server-hpa.yaml @@ -20,7 +20,7 @@ ################################ ## Airflow Api-Server HPA ################################# -{{- if .Values.apiServer.hpa.enabled }} +{{- if and .Values.apiServer.enabled .Values.apiServer.hpa.enabled (semverCompare ">=3.0.0" .Values.airflowVersion) }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: diff --git a/chart/templates/cleanup/cleanup-cronjob.yaml b/chart/templates/cleanup/cleanup-cronjob.yaml index fa52af1da7a19..88ca64e8c429f 100644 --- a/chart/templates/cleanup/cleanup-cronjob.yaml +++ b/chart/templates/cleanup/cleanup-cronjob.yaml @@ -67,10 +67,10 @@ spec: {{- end }} annotations: {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 12 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 12 }} {{- end }} {{- if .Values.cleanup.podAnnotations }} - {{- toYaml .Values.cleanup.podAnnotations | nindent 12 }} + {{- tpl (toYaml .Values.cleanup.podAnnotations) . | nindent 12 }} {{- end }} spec: restartPolicy: Never diff --git a/chart/templates/dag-processor/dag-processor-deployment.yaml b/chart/templates/dag-processor/dag-processor-deployment.yaml index 448682224a003..88311ad48c3aa 100644 --- a/chart/templates/dag-processor/dag-processor-deployment.yaml +++ b/chart/templates/dag-processor/dag-processor-deployment.yaml @@ -83,10 +83,10 @@ spec: cluster-autoscaler.kubernetes.io/safe-to-evict: "true" {{- end }} {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.dagProcessor.podAnnotations }} - {{- toYaml .Values.dagProcessor.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.dagProcessor.podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.dagProcessor.priorityClassName }} @@ -214,6 +214,10 @@ spec: - name: AIRFLOW__LOG_RETENTION_DAYS value: "{{ .Values.dagProcessor.logGroomerSidecar.retentionDays }}" {{- end }} + {{- if .Values.dagProcessor.logGroomerSidecar.retentionMinutes }} + - name: AIRFLOW__LOG_RETENTION_MINUTES + value: "{{ .Values.dagProcessor.logGroomerSidecar.retentionMinutes }}" + {{- end }} {{- if .Values.dagProcessor.logGroomerSidecar.frequencyMinutes }} - name: AIRFLOW__LOG_CLEANUP_FREQUENCY_MINUTES value: "{{ .Values.dagProcessor.logGroomerSidecar.frequencyMinutes }}" diff --git a/chart/templates/database-cleanup/database-cleanup-cronjob.yaml b/chart/templates/database-cleanup/database-cleanup-cronjob.yaml index f0b25058d50e7..03e0ce08d4b14 100644 --- a/chart/templates/database-cleanup/database-cleanup-cronjob.yaml +++ b/chart/templates/database-cleanup/database-cleanup-cronjob.yaml @@ -67,10 +67,10 @@ spec: {{- end }} annotations: {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 12 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 12 }} {{- end }} {{- if .Values.databaseCleanup.podAnnotations }} - {{- toYaml .Values.databaseCleanup.podAnnotations | nindent 12 }} + {{- tpl (toYaml .Values.databaseCleanup.podAnnotations) . | nindent 12 }} {{- end }} spec: restartPolicy: Never diff --git a/chart/templates/flower/flower-deployment.yaml b/chart/templates/flower/flower-deployment.yaml index d79f55173f539..a68c8400c3ef0 100644 --- a/chart/templates/flower/flower-deployment.yaml +++ b/chart/templates/flower/flower-deployment.yaml @@ -69,7 +69,7 @@ spec: checksum/airflow-config: {{ include (print $.Template.BasePath "/configmaps/configmap.yaml") . | sha256sum }} checksum/flower-secret: {{ include (print $.Template.BasePath "/secrets/flower-secret.yaml") . | sha256sum }} {{- if or (.Values.airflowPodAnnotations) (.Values.flower.podAnnotations) }} - {{- mustMerge .Values.flower.podAnnotations .Values.airflowPodAnnotations | toYaml | nindent 8 }} + {{- tpl (mustMerge .Values.flower.podAnnotations .Values.airflowPodAnnotations | toYaml) . | nindent 8 }} {{- end }} spec: nodeSelector: {{- toYaml $nodeSelector | nindent 8 }} diff --git a/chart/templates/jobs/create-user-job.yaml b/chart/templates/jobs/create-user-job.yaml index 6626fb7ff5ba5..1d89502ae714f 100644 --- a/chart/templates/jobs/create-user-job.yaml +++ b/chart/templates/jobs/create-user-job.yaml @@ -66,10 +66,10 @@ spec: {{- if or .Values.airflowPodAnnotations .Values.createUserJob.annotations }} annotations: {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.createUserJob.annotations }} - {{- toYaml .Values.createUserJob.annotations | nindent 8 }} + {{- tpl (toYaml .Values.createUserJob.annotations) . | nindent 8 }} {{- end }} {{- end }} spec: diff --git a/chart/templates/jobs/migrate-database-job.yaml b/chart/templates/jobs/migrate-database-job.yaml index 84915ed039ec3..fe28f6bb0cb8e 100644 --- a/chart/templates/jobs/migrate-database-job.yaml +++ b/chart/templates/jobs/migrate-database-job.yaml @@ -66,10 +66,10 @@ spec: {{- if or .Values.airflowPodAnnotations .Values.migrateDatabaseJob.annotations }} annotations: {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.migrateDatabaseJob.annotations }} - {{- toYaml .Values.migrateDatabaseJob.annotations | nindent 8 }} + {{- tpl (toYaml .Values.migrateDatabaseJob.annotations) . | nindent 8 }} {{- end }} {{- end }} spec: diff --git a/chart/templates/pgbouncer/pgbouncer-deployment.yaml b/chart/templates/pgbouncer/pgbouncer-deployment.yaml index b568c259b1234..0ecbc1e208ff7 100644 --- a/chart/templates/pgbouncer/pgbouncer-deployment.yaml +++ b/chart/templates/pgbouncer/pgbouncer-deployment.yaml @@ -74,7 +74,7 @@ spec: checksum/pgbouncer-config-secret: {{ include (print $.Template.BasePath "/secrets/pgbouncer-config-secret.yaml") . | sha256sum }} checksum/pgbouncer-certificates-secret: {{ include (print $.Template.BasePath "/secrets/pgbouncer-certificates-secret.yaml") . | sha256sum }} {{- if .Values.pgbouncer.podAnnotations }} - {{- toYaml .Values.pgbouncer.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.pgbouncer.podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.pgbouncer.priorityClassName }} diff --git a/chart/templates/pgbouncer/pgbouncer-networkpolicy.yaml b/chart/templates/pgbouncer/pgbouncer-networkpolicy.yaml index f80f3e1264d25..8fd2a616f36ad 100644 --- a/chart/templates/pgbouncer/pgbouncer-networkpolicy.yaml +++ b/chart/templates/pgbouncer/pgbouncer-networkpolicy.yaml @@ -24,7 +24,7 @@ {{- if hasKey .Values.workers "celery" }} {{- $kedaEnabled = or .Values.workers.celery.keda.enabled (and (not (has .Values.workers.celery.keda.enabled (list true false))) .Values.workers.keda.enabled) }} {{- end }} -{{- $workersKedaEnabled := and $kedaEnabled (has .Values.executor (list "CeleryExecutor" "CeleryKubernetesExecutor")) }} +{{- $workersKedaEnabled := and $kedaEnabled (or (contains "CeleryExecutor" .Values.executor) (contains "CeleryKubernetesExecutor" .Values.executor)) }} {{- $triggererEnabled := .Values.triggerer.enabled }} {{- $triggererKedaEnabled := and $triggererEnabled .Values.triggerer.keda.enabled }} {{- if and .Values.pgbouncer.enabled .Values.networkPolicies.enabled }} diff --git a/chart/templates/rbac/security-context-constraint-rolebinding.yaml b/chart/templates/rbac/security-context-constraint-rolebinding.yaml index 40055e8606c35..45f95480cd717 100644 --- a/chart/templates/rbac/security-context-constraint-rolebinding.yaml +++ b/chart/templates/rbac/security-context-constraint-rolebinding.yaml @@ -21,7 +21,6 @@ ## Airflow SCC Role Binding ########################### {{- if and .Values.rbac.create .Values.rbac.createSCCRoleBinding }} -{{- $hasWorkers := has .Values.executor (list "CeleryExecutor" "LocalKubernetesExecutor" "KubernetesExecutor" "CeleryKubernetesExecutor") }} apiVersion: rbac.authorization.k8s.io/v1 {{- if .Values.multiNamespaceMode }} kind: ClusterRoleBinding @@ -51,20 +50,26 @@ roleRef: kind: ClusterRole name: system:openshift:scc:anyuid subjects: + {{- if and .Values.webserver.enabled (semverCompare "<3.0.0" .Values.airflowVersion) }} - kind: ServiceAccount name: {{ include "webserver.serviceAccountName" . }} namespace: "{{ .Release.Namespace }}" - {{- if $hasWorkers }} + {{- end }} + {{- if or (contains "CeleryExecutor" .Values.executor) (contains "LocalKubernetesExecutor" .Values.executor) (contains "KubernetesExecutor" .Values.executor) (contains "CeleryKubernetesExecutor" .Values.executor) }} - kind: ServiceAccount name: {{ include "worker.serviceAccountName" . }} namespace: "{{ .Release.Namespace }}" {{- end }} + {{- if .Values.scheduler.enabled }} - kind: ServiceAccount name: {{ include "scheduler.serviceAccountName" . }} namespace: "{{ .Release.Namespace }}" + {{- end }} + {{- if and .Values.apiServer.enabled (semverCompare ">=3.0.0" .Values.airflowVersion) }} - kind: ServiceAccount name: {{ include "apiServer.serviceAccountName" . }} namespace: "{{ .Release.Namespace }}" + {{- end }} {{- if and .Values.statsd.enabled }} - kind: ServiceAccount name: {{ include "statsd.serviceAccountName" . }} @@ -80,12 +85,16 @@ subjects: name: {{ include "redis.serviceAccountName" . }} namespace: "{{ .Release.Namespace }}" {{- end }} + {{- if .Values.triggerer.enabled }} - kind: ServiceAccount name: {{ include "triggerer.serviceAccountName" . }} namespace: "{{ .Release.Namespace }}" + {{- end }} + {{- if .Values.migrateDatabaseJob.enabled }} - kind: ServiceAccount name: {{ include "migrateDatabaseJob.serviceAccountName" . }} namespace: "{{ .Release.Namespace }}" + {{- end }} {{- if eq (include "createUserJob.isEnabled" .) "true" }} - kind: ServiceAccount name: {{ include "createUserJob.serviceAccountName" . }} diff --git a/chart/templates/redis/redis-statefulset.yaml b/chart/templates/redis/redis-statefulset.yaml index f3e5474369c83..77f0c7e458c97 100644 --- a/chart/templates/redis/redis-statefulset.yaml +++ b/chart/templates/redis/redis-statefulset.yaml @@ -67,7 +67,7 @@ spec: {{- if or .Values.redis.safeToEvict .Values.redis.podAnnotations }} annotations: {{- if .Values.redis.podAnnotations }} - {{- toYaml .Values.redis.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.redis.podAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.redis.safeToEvict }} cluster-autoscaler.kubernetes.io/safe-to-evict: "true" diff --git a/chart/templates/scheduler/scheduler-deployment.yaml b/chart/templates/scheduler/scheduler-deployment.yaml index 7f508a8078e10..2a476740088d3 100644 --- a/chart/templates/scheduler/scheduler-deployment.yaml +++ b/chart/templates/scheduler/scheduler-deployment.yaml @@ -109,10 +109,10 @@ spec: cluster-autoscaler.kubernetes.io/safe-to-evict: "true" {{- end }} {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.scheduler.podAnnotations }} - {{- toYaml .Values.scheduler.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.scheduler.podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.scheduler.priorityClassName }} @@ -288,6 +288,10 @@ spec: - name: AIRFLOW__LOG_RETENTION_DAYS value: "{{ .Values.scheduler.logGroomerSidecar.retentionDays }}" {{- end }} + {{- if .Values.scheduler.logGroomerSidecar.retentionMinutes }} + - name: AIRFLOW__LOG_RETENTION_MINUTES + value: "{{ .Values.scheduler.logGroomerSidecar.retentionMinutes }}" + {{- end }} {{- if .Values.scheduler.logGroomerSidecar.frequencyMinutes }} - name: AIRFLOW__LOG_CLEANUP_FREQUENCY_MINUTES value: "{{ .Values.scheduler.logGroomerSidecar.frequencyMinutes }}" diff --git a/chart/templates/secrets/flower-secret.yaml b/chart/templates/secrets/flower-secret.yaml index e402f27dc3b47..66395881d19ea 100644 --- a/chart/templates/secrets/flower-secret.yaml +++ b/chart/templates/secrets/flower-secret.yaml @@ -20,7 +20,7 @@ ################################ ## Flower Secret ################################# -{{- if (and (not .Values.flower.secretName) .Values.flower.username .Values.flower.password) }} +{{- if and .Values.flower.enabled (not .Values.flower.secretName) .Values.flower.username .Values.flower.password }} apiVersion: v1 kind: Secret metadata: diff --git a/chart/templates/secrets/kerberos-keytab-secret.yaml b/chart/templates/secrets/kerberos-keytab-secret.yaml index 6cb90d544b94a..cf1bc3ca23f88 100644 --- a/chart/templates/secrets/kerberos-keytab-secret.yaml +++ b/chart/templates/secrets/kerberos-keytab-secret.yaml @@ -20,7 +20,7 @@ ################################ ## Kerberos Secret ################################# -{{- if .Values.kerberos.keytabBase64Content }} +{{- if and .Values.kerberos.enabled .Values.kerberos.keytabBase64Content }} apiVersion: v1 kind: Secret metadata: diff --git a/chart/templates/secrets/pgbouncer-certificates-secret.yaml b/chart/templates/secrets/pgbouncer-certificates-secret.yaml index bd09f704e0f96..e826d16a97c76 100644 --- a/chart/templates/secrets/pgbouncer-certificates-secret.yaml +++ b/chart/templates/secrets/pgbouncer-certificates-secret.yaml @@ -20,7 +20,7 @@ ################################ ## Pgbouncer Certificate Secret ################################# -{{- if or .Values.pgbouncer.ssl.ca .Values.pgbouncer.ssl.cert .Values.pgbouncer.ssl.key }} +{{- if and .Values.pgbouncer.enabled (or .Values.pgbouncer.ssl.ca .Values.pgbouncer.ssl.cert .Values.pgbouncer.ssl.key) }} apiVersion: v1 kind: Secret metadata: diff --git a/chart/templates/statsd/statsd-deployment.yaml b/chart/templates/statsd/statsd-deployment.yaml index b7e624b149eba..0b21999453c27 100644 --- a/chart/templates/statsd/statsd-deployment.yaml +++ b/chart/templates/statsd/statsd-deployment.yaml @@ -68,7 +68,7 @@ spec: annotations: checksum/statsd-config: {{ include (print $.Template.BasePath "/configmaps/statsd-configmap.yaml") . | sha256sum }} {{- if .Values.statsd.podAnnotations }} - {{- toYaml .Values.statsd.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.statsd.podAnnotations) . | nindent 8 }} {{- end }} {{- end }} spec: diff --git a/chart/templates/triggerer/triggerer-deployment.yaml b/chart/templates/triggerer/triggerer-deployment.yaml index dcfa1d1f428ef..e4a394b3ad169 100644 --- a/chart/templates/triggerer/triggerer-deployment.yaml +++ b/chart/templates/triggerer/triggerer-deployment.yaml @@ -93,10 +93,10 @@ spec: cluster-autoscaler.kubernetes.io/safe-to-evict: "true" {{- end }} {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.triggerer.podAnnotations }} - {{- toYaml .Values.triggerer.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.triggerer.podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.triggerer.priorityClassName }} @@ -245,6 +245,10 @@ spec: - name: AIRFLOW__LOG_RETENTION_DAYS value: "{{ .Values.triggerer.logGroomerSidecar.retentionDays }}" {{- end }} + {{- if .Values.triggerer.logGroomerSidecar.retentionMinutes }} + - name: AIRFLOW__LOG_RETENTION_MINUTES + value: "{{ .Values.triggerer.logGroomerSidecar.retentionMinutes }}" + {{- end }} {{- if .Values.triggerer.logGroomerSidecar.frequencyMinutes }} - name: AIRFLOW__LOG_CLEANUP_FREQUENCY_MINUTES value: "{{ .Values.triggerer.logGroomerSidecar.frequencyMinutes }}" diff --git a/chart/templates/webserver/webserver-deployment.yaml b/chart/templates/webserver/webserver-deployment.yaml index 08f6b30a4d819..7b958f581acbc 100644 --- a/chart/templates/webserver/webserver-deployment.yaml +++ b/chart/templates/webserver/webserver-deployment.yaml @@ -92,10 +92,10 @@ spec: checksum/extra-configmaps: {{ include (print $.Template.BasePath "/configmaps/extra-configmaps.yaml") . | sha256sum }} checksum/extra-secrets: {{ include (print $.Template.BasePath "/secrets/extra-secrets.yaml") . | sha256sum }} {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.webserver.podAnnotations }} - {{- toYaml .Values.webserver.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.webserver.podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.webserver.hostAliases }} diff --git a/chart/templates/workers/worker-deployment.yaml b/chart/templates/workers/worker-deployment.yaml index 4d44c4fa824ec..96069aa1ff5b7 100644 --- a/chart/templates/workers/worker-deployment.yaml +++ b/chart/templates/workers/worker-deployment.yaml @@ -124,7 +124,7 @@ spec: checksum/extra-configmaps: {{ include (print $.Template.BasePath "/configmaps/extra-configmaps.yaml") . | sha256sum }} checksum/extra-secrets: {{ include (print $.Template.BasePath "/secrets/extra-secrets.yaml") . | sha256sum }} {{- if $podAnnotations }} - {{- toYaml $podAnnotations | nindent 8 }} + {{- tpl (toYaml $podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.workers.runtimeClassName }} @@ -367,6 +367,10 @@ spec: - name: AIRFLOW__LOG_RETENTION_DAYS value: "{{ .Values.workers.logGroomerSidecar.retentionDays }}" {{- end }} + {{- if .Values.workers.logGroomerSidecar.retentionMinutes }} + - name: AIRFLOW__LOG_RETENTION_MINUTES + value: "{{ .Values.workers.logGroomerSidecar.retentionMinutes }}" + {{- end }} {{- if .Values.workers.logGroomerSidecar.frequencyMinutes }} - name: AIRFLOW__LOG_CLEANUP_FREQUENCY_MINUTES value: "{{ .Values.workers.logGroomerSidecar.frequencyMinutes }}" diff --git a/chart/values.schema.json b/chart/values.schema.json index 74d931666da40..31e6333e88538 100644 --- a/chart/values.schema.json +++ b/chart/values.schema.json @@ -78,7 +78,7 @@ "defaultAirflowTag": { "description": "Default airflow tag to deploy.", "type": "string", - "default": "3.1.7", + "default": "3.1.8", "x-docsSection": "Common" }, "defaultAirflowDigest": { @@ -93,7 +93,7 @@ "airflowVersion": { "description": "Airflow version (Used to make some decisions based on Airflow Version being deployed). Version 2.11.0 and above is supported.", "type": "string", - "default": "3.1.7", + "default": "3.1.8", "x-docsSection": "Common" }, "securityContext": { @@ -768,7 +768,7 @@ } }, "airflowPodAnnotations": { - "description": "Extra annotations to apply to all Airflow pods.", + "description": "Extra annotations to apply to all Airflow pods (templated).", "type": "object", "default": {}, "x-docsSection": "Kubernetes", @@ -2264,7 +2264,7 @@ } }, "resources": { - "description": "Resource configuration for Airflow Celery workers and pods created with pod-template-file.", + "description": "Resource configuration for Airflow Celery workers and pods created with pod-template-file (deprecated, use `workers.celery.resources` or/and `workers.kubernetes.resources` instead).", "type": "object", "default": {}, "examples": [ @@ -2282,7 +2282,7 @@ "$ref": "#/definitions/io.k8s.api.core.v1.ResourceRequirements" }, "terminationGracePeriodSeconds": { - "description": "Grace period for tasks to finish after SIGTERM is sent from Kubernetes. It is used by Airflow Celery workers and pod-template-file.", + "description": "Grace period for tasks to finish after SIGTERM is sent from Kubernetes. It is used by Airflow Celery workers and pod-template-file. Use ``workers.celery.terminationGracePeriodSeconds`` and/or ``workers.kubernetes.terminationGracePeriodSeconds`` to separate value between Celery workers and pod-template-file", "type": "integer", "default": 600 }, @@ -2332,7 +2332,7 @@ } }, "nodeSelector": { - "description": "Select certain nodes for Airflow Celery worker pods and pods created with pod-template-file.", + "description": "Select certain nodes for Airflow Celery worker pods and pods created with pod-template-file. Use ``workers.celery.nodeSelector`` and/or ``workers.kubernetes.nodeSelector`` to separate value between Celery workers and pod-template-file.", "type": "object", "default": {}, "additionalProperties": { @@ -2410,7 +2410,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the Airflow Celery workers and pods created with pod-template-file.", + "description": "Annotations to add to the Airflow Celery workers and pods created with pod-template-file (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -3257,6 +3257,40 @@ } } } + }, + "resources": { + "description": "Resource configuration for Airflow Celery workers.", + "type": "object", + "default": {}, + "examples": [ + { + "limits": { + "cpu": "100m", + "memory": "128Mi" + }, + "requests": { + "cpu": "100m", + "memory": "128Mi" + } + } + ], + "$ref": "#/definitions/io.k8s.api.core.v1.ResourceRequirements" + }, + "terminationGracePeriodSeconds": { + "description": "Grace period for tasks to finish after SIGTERM is sent from Kubernetes.", + "type": [ + "integer", + "null" + ], + "default": null + }, + "nodeSelector": { + "description": "Select certain nodes for Airflow Celery worker pods.", + "type": "object", + "default": {}, + "additionalProperties": { + "type": "string" + } } } }, @@ -3512,6 +3546,40 @@ } } } + }, + "resources": { + "description": "Resource configuration for pods created with pod-template-file.", + "type": "object", + "default": {}, + "examples": [ + { + "limits": { + "cpu": "100m", + "memory": "128Mi" + }, + "requests": { + "cpu": "100m", + "memory": "128Mi" + } + } + ], + "$ref": "#/definitions/io.k8s.api.core.v1.ResourceRequirements" + }, + "terminationGracePeriodSeconds": { + "description": "Grace period for tasks to finish after SIGTERM is sent from Kubernetes.", + "type": [ + "integer", + "null" + ], + "default": null + }, + "nodeSelector": { + "description": "Select certain nodes for pods created with pod-template-file.", + "type": "object", + "default": {}, + "additionalProperties": { + "type": "string" + } } } } @@ -3899,7 +3967,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the scheduler pods.", + "description": "Annotations to add to the scheduler pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -4467,7 +4535,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the triggerer pods.", + "description": "Annotations to add to the triggerer pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -5079,7 +5147,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the dag processor pods.", + "description": "Annotations to add to the dag processor pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -5388,7 +5456,7 @@ ] }, "annotations": { - "description": "Annotations to add to the create user job pod.", + "description": "Annotations to add to the create user job pod (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -5737,7 +5805,7 @@ ] }, "annotations": { - "description": "Annotations to add to the migrate database job pod.", + "description": "Annotations to add to the migrate database job pod (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -6143,8 +6211,11 @@ } }, "replicas": { - "description": "How many Airflow API server replicas should run. This setting is ignored when HPA (Horizontal Pod Autoscaler) is enabled", - "type": "integer", + "description": "How many Airflow API server replicas should run. Omitted from the Deployment, when HPA is enabled.", + "type": [ + "integer", + "null" + ], "default": 1 }, "revisionHistoryLimit": { @@ -6632,7 +6703,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the API server pods.", + "description": "Annotations to add to the API server pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -7459,7 +7530,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the webserver pods.", + "description": "Annotations to add to the webserver pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -8000,7 +8071,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the Flower pods.", + "description": "Annotations to add to the Flower pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -8419,7 +8490,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the StatsD pods.", + "description": "Annotations to add to the StatsD pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -8623,7 +8694,7 @@ } }, "podAnnotations": { - "description": "Add annotations for the PgBouncer Pod.", + "description": "Add annotations for the PgBouncer Pod (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -9462,7 +9533,7 @@ "default": 0 }, "podAnnotations": { - "description": "Annotations to add to the redis pods.", + "description": "Annotations to add to the redis pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -9838,7 +9909,7 @@ "default": null }, "podAnnotations": { - "description": "Annotations to add to cleanup pods.", + "description": "Annotations to add to cleanup pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -10191,7 +10262,7 @@ "default": null }, "podAnnotations": { - "description": "Annotations to add to database cleanup pods.", + "description": "Annotations to add to database cleanup pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -10649,13 +10720,82 @@ } ] }, + "httpPort": { + "description": "Git-Sync liveness service http bind port.", + "type": "integer", + "default": 1234 + }, + "recommendedProbeSetting": { + "description": "Setting this to true, will remove readiness probe usage and configure liveness probe to use a dedicated Git-Sync liveness service.", + "type": "boolean", + "default": false + }, + "startupProbe": { + "description": "Startup probe configuration for git sync container.", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "description": "Enable GitSync Kubernetes Startup Probe.", + "type": "boolean", + "default": true + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Minimum value is 1 seconds.", + "type": "integer", + "default": 1 + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before startup probe is initiated.", + "type": "integer", + "default": 0 + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Minimum value is 1.", + "type": "integer", + "default": 5 + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Minimum value is 1.", + "type": "integer", + "default": 10 + } + } + }, "livenessProbe": { "description": "Liveness probe configuration for git sync container.", "type": "object", - "$ref": "#/definitions/io.k8s.api.core.v1.Probe" + "additionalProperties": true, + "properties": { + "enabled": { + "description": "Enable GitSync Kubernetes Liveness Probe.", + "type": "boolean", + "default": true + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Minimum value is 1 seconds.", + "type": "integer", + "default": 1 + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before startup probe is initiated.", + "type": "integer", + "default": 0 + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Minimum value is 1.", + "type": "integer", + "default": 5 + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Minimum value is 1.", + "type": "integer", + "default": 10 + } + } }, "readinessProbe": { - "description": "Readiness probe configuration for git sync container.", + "description": "Readiness probe configuration for git sync container. As Git-Sync is not service-type object, the usage of the section was removed. Section itself will be removed in future release as to not break setups during upgrades.", "type": "object", "$ref": "#/definitions/io.k8s.api.core.v1.Probe" }, @@ -14070,10 +14210,15 @@ ] }, "retentionDays": { - "description": "Number of days to retain the logs when running the Airflow log groomer sidecar.", + "description": "Number of days to retain the logs when running the Airflow log groomer sidecar. Total retention time is retentionDays + retentionMinutes.", "type": "integer", "default": 15 }, + "retentionMinutes": { + "description": "Number of minutes to retain the logs when running the Airflow log groomer sidecar. Total retention time is retentionDays + retentionMinutes.", + "type": "integer", + "default": 0 + }, "frequencyMinutes": { "description": "Number of minutes between attempts to groom the Airflow logs in log groomer sidecar.", "type": "integer", diff --git a/chart/values.yaml b/chart/values.yaml index 255db1700bc83..a5159c84faa52 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -68,14 +68,14 @@ airflowHome: /opt/airflow defaultAirflowRepository: apache/airflow # Default airflow tag to deploy -defaultAirflowTag: "3.1.7" +defaultAirflowTag: "3.1.8" # Default airflow digest. If specified, it takes precedence over tag defaultAirflowDigest: ~ # Airflow version (Used to make some decisions based on Airflow Version being deployed) # Version 2.11.0 and above is supported. -airflowVersion: "3.1.7" +airflowVersion: "3.1.8" # Images images: @@ -338,7 +338,7 @@ networkPolicies: enabled: false # Extra annotations to apply to all -# Airflow pods +# Airflow pods (templated) airflowPodAnnotations: {} # Extra annotations to apply to @@ -898,6 +898,7 @@ workers: containerLifecycleHooks: {} # Resource configuration for Airflow Celery workers and pods created with pod-template-file + # (deprecated, use `workers.celery.resources` or/and `workers.kubernetes.resources` instead) resources: {} # limits: # cpu: 100m @@ -908,6 +909,8 @@ workers: # Grace period for tasks to finish after SIGTERM is sent from kubernetes. # It is used by Airflow Celery workers and pod-template-file. + # Use workers.celery.terminationGracePeriodSeconds and/or workers.kubernetes.terminationGracePeriodSeconds + # to separate value between Celery workers and pod-template-file terminationGracePeriodSeconds: 600 # This setting tells kubernetes that its ok to evict when it wants to scale a node down. @@ -944,7 +947,10 @@ workers: extraPorts: [] # Select certain nodes for Airflow Celery worker pods and pods created with pod-template-file + # Use workers.celery.nodeSelector and/or workers.kubernetes.nodeSelector to separate value + # between Celery workers and pod-template-file nodeSelector: {} + runtimeClassName: ~ priorityClassName: ~ affinity: {} @@ -973,7 +979,7 @@ workers: # Annotations for the Airflow Celery worker resource annotations: {} - # Pod annotations for the Airflow Celery workers and pods created with pod-template-file + # Pod annotations for the Airflow Celery workers and pods created with pod-template-file (templated) podAnnotations: {} # Labels specific to Airflow Celery workers objects and pods created with pod-template-file @@ -993,6 +999,12 @@ workers: # Number of days to retain logs retentionDays: 15 + # Number of minutes to retain logs. + # This can be used for finer granularity than days. + # Total retention is retentionDays + retentionMinutes. + retentionMinutes: 0 + + # Frequency to attempt to groom logs (in minutes) frequencyMinutes: 15 @@ -1246,6 +1258,21 @@ workers: # Container level lifecycle hooks containerLifecycleHooks: {} + # Resource configuration for Airflow Celery workers + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + # Grace period for tasks to finish after SIGTERM is sent from kubernetes + terminationGracePeriodSeconds: ~ + + # Select certain nodes for Airflow Celery worker pods + nodeSelector: {} + kubernetes: # Command to use in pod-template-file (templated) command: ~ @@ -1301,6 +1328,21 @@ workers: # Container level lifecycle hooks containerLifecycleHooks: {} + # Resource configuration for pods created with pod-template-file + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + # Grace period for tasks to finish after SIGTERM is sent from kubernetes + terminationGracePeriodSeconds: ~ + + # Select certain nodes for pods created with pod-template-file + nodeSelector: {} + # Airflow scheduler settings scheduler: enabled: true @@ -1457,6 +1499,7 @@ scheduler: # annotations for scheduler deployment annotations: {} + # Pod annotations for scheduler pods (templated) podAnnotations: {} # Labels specific to scheduler objects and pods @@ -1471,6 +1514,12 @@ scheduler: args: ["bash", "/clean-logs"] # Number of days to retain logs retentionDays: 15 + + # Number of minutes to retain logs. + # This can be used for finer granularity than days. + # Total retention is retentionDays + retentionMinutes. + retentionMinutes: 0 + # frequency to attempt to groom logs, in minutes frequencyMinutes: 15 # Max size of logs in bytes. 0 = disabled @@ -1541,7 +1590,7 @@ createUserJob: - "{{ if .Values.webserver.defaultUser }}{{ .Values.webserver.defaultUser.lastName }}{{ else }}{{ .Values.createUserJob.defaultUser.lastName }}{{ end }}" - "-p" - "{{ if .Values.webserver.defaultUser }}{{ .Values.webserver.defaultUser.password }}{{ else }}{{ .Values.createUserJob.defaultUser.password }}{{ end }}" - # Annotations on the create user job pod + # Annotations on the create user job pod (templated) annotations: {} # jobAnnotations are annotations on the create user job jobAnnotations: {} @@ -1636,7 +1685,7 @@ migrateDatabaseJob: airflow db migrate - # Annotations on the database migration pod + # Annotations on the database migration pod (templated) annotations: {} # jobAnnotations are annotations on the database migration job jobAnnotations: {} @@ -1716,9 +1765,8 @@ migrateDatabaseJob: apiServer: enabled: true - # Number of Airflow API servers in the deployment - # This setting is ignored when HPA (Horizontal Pod Autoscaler) is enabled, - # as HPA will automatically manage the number of replicas based on the configured metrics. + # Number of Airflow API servers in the deployment. + # Omitted from the Deployment, when HPA is enabled. replicas: 1 # Max number of old replicasets to retain revisionHistoryLimit: ~ @@ -1852,6 +1900,7 @@ apiServer: # annotations for Airflow API server deployment annotations: {} + # Pod annotations for API server pods (templated) podAnnotations: {} networkPolicy: @@ -2136,6 +2185,7 @@ webserver: # annotations for webserver deployment annotations: {} + # Pod annotations for webserver pods (templated) podAnnotations: {} # Labels specific webserver app @@ -2299,6 +2349,7 @@ triggerer: # annotations for the triggerer deployment annotations: {} + # Pod annotations for triggerer pods (templated) podAnnotations: {} # Labels specific to triggerer objects and pods @@ -2313,6 +2364,12 @@ triggerer: args: ["bash", "/clean-logs"] # Number of days to retain logs retentionDays: 15 + + # Number of minutes to retain logs. + # This can be used for finer granularity than days. + # Total retention is retentionDays + retentionMinutes. + retentionMinutes: 0 + # frequency to attempt to groom logs, in minutes frequencyMinutes: 15 # Max size of logs in bytes. 0 = disabled @@ -2532,6 +2589,7 @@ dagProcessor: # annotations for the dag processor deployment annotations: {} + # Pod annotations for dag processor pods (templated) podAnnotations: {} logGroomerSidecar: @@ -2543,6 +2601,12 @@ dagProcessor: args: ["bash", "/clean-logs"] # Number of days to retain logs retentionDays: 15 + + # Number of minutes to retain logs. + # This can be used for finer granularity than days. + # Total retention is retentionDays + retentionMinutes. + retentionMinutes: 0 + # frequency to attempt to groom logs, in minutes frequencyMinutes: 15 # Max size of logs in bytes. 0 = disabled @@ -2716,6 +2780,7 @@ flower: # annotations for the flower deployment annotations: {} + # Pod annotations for flower pods (templated) podAnnotations: {} # Labels specific to flower objects and pods @@ -2833,6 +2898,7 @@ statsd: # So, If you use it, ensure all mapping item contains in it. overrideMappings: [] + # Pod annotations for StatsD pods (templated) podAnnotations: {} # Labels specific to statsd objects and pods @@ -2862,6 +2928,7 @@ pgbouncer: # annotations to be added to the PgBouncer deployment annotations: {} + # Pod annotations for PgBouncer pods (templated) podAnnotations: {} # Add custom annotations to the pgbouncer certificates secret @@ -3159,6 +3226,7 @@ redis: # Labels specific to redis objects and pods labels: {} + # Pod annotations for Redis pods (templated) podAnnotations: {} # Auth secret for a private registry (Deprecated - use `imagePullSecrets` instead) @@ -3257,6 +3325,7 @@ cleanup: topologySpreadConstraints: [] priorityClassName: ~ + # Pod annotations for cleanup pods (templated) podAnnotations: {} # Labels specific to cleanup objects and pods @@ -3346,6 +3415,7 @@ databaseCleanup: topologySpreadConstraints: [] priorityClassName: ~ + # Pod annotations for database cleanup pods (templated) podAnnotations: {} # Labels specific to database cleanup objects and pods @@ -3605,8 +3675,35 @@ dags: # container level lifecycle hooks containerLifecycleHooks: {} + # Git-Sync liveness service http bind port + httpPort: 1234 + + # Setting this to true, will remove readinessProbe usage and configure livenessProbe to + # use a dedicated Git-Sync liveness service. In future, behaviour with value true will be + # default one and old one will be removed + recommendedProbeSetting: false + + startupProbe: + enabled: true + timeoutSeconds: 1 + initialDelaySeconds: 0 + periodSeconds: 5 + failureThreshold: 10 + + # As Git-Sync is not service-type object, the usage of this section will be removed. + # By setting dags.gitSync.recommendedProbeSetting to true, you will enable future behaviour. readinessProbe: {} + + # The behaviour of the livenessProbe will change with the next release of Helm Chart. + # To enable future behaviour set dags.gitSync.recommendedProbeSetting to true. + # New behaviour uses the recommended liveness configuration by using Git-Sync built-in + # liveness service livenessProbe: {} + # enabled: true + # timeoutSeconds: 1 + # initialDelaySeconds: 0 + # periodSeconds: 5 + # failureThreshold: 10 # Mount additional volumes into git-sync. It can be templated like in the following example: # extraVolumeMounts: diff --git a/contributing-docs/01_roles_in_airflow_project.rst b/contributing-docs/01_roles_in_airflow_project.rst index 2f14904497ade..70db91979651d 100644 --- a/contributing-docs/01_roles_in_airflow_project.rst +++ b/contributing-docs/01_roles_in_airflow_project.rst @@ -22,7 +22,9 @@ There are several roles within the Airflow Open-Source community. For detailed information for each role, see: `Committers and PMC members <../COMMITTERS.rst>`__. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: PMC Member ---------- diff --git a/contributing-docs/02_how_to_communicate.rst b/contributing-docs/02_how_to_communicate.rst index f01da6ca53d23..04b3d47cd1e17 100644 --- a/contributing-docs/02_how_to_communicate.rst +++ b/contributing-docs/02_how_to_communicate.rst @@ -26,7 +26,9 @@ This means that communication plays a big role in it, and this chapter is all ab In our communication, everyone is expected to follow the `ASF Code of Conduct `_. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Various Communication channels ------------------------------ diff --git a/contributing-docs/03_contributors_quick_start.rst b/contributing-docs/03_contributors_quick_start.rst index 2288a4a8a4479..93ef708e6271a 100644 --- a/contributing-docs/03_contributors_quick_start.rst +++ b/contributing-docs/03_contributors_quick_start.rst @@ -19,7 +19,9 @@ Contributor's Quick Start ************************* -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Note to Starters ################ @@ -238,6 +240,9 @@ Forking and cloning Project Configuring prek ---------------- +.. note:: + Run the commands in this section in your **local terminal** (on your host machine), not inside a Breeze shell. Prek hooks run on your host to check and format code before you commit. + Before committing changes to github or raising a pull request, the code needs to be checked for certain quality standards such as spell check, code syntax, code formatting, compatibility with Apache License requirements etc. This set of tests are applied when you commit your code. @@ -261,6 +266,14 @@ To avoid burden on our CI infrastructure and to save time, prek hooks can be run Installing prek is best done with ``uv`` (recommended) or ``pipx``. +You can also update the tools installed with UV, including ``prek``. + + Run the following command to upgrade all UV-managed tools: + + .. code-block:: bash + + uv tool upgrade --all + 1. Installing required packages on Debian / Ubuntu, install via @@ -285,6 +298,7 @@ on macOS, install via uv tool install prek + or with pipx: .. code-block:: bash @@ -361,6 +375,7 @@ It will run prek hooks automatically before committing and stops the commit on f cd ~/Projects/airflow prek uninstall + - For more information on this visit |08_static_code_checks.rst| .. |08_static_code_checks.rst| raw:: html diff --git a/contributing-docs/03a_contributors_quick_start_beginners.rst b/contributing-docs/03a_contributors_quick_start_beginners.rst index 6a04887441e2e..f0ffb29cb34f4 100644 --- a/contributing-docs/03a_contributors_quick_start_beginners.rst +++ b/contributing-docs/03a_contributors_quick_start_beginners.rst @@ -87,7 +87,7 @@ Option A – Breeze on Your Laptop The command starts a shell and launches multiple terminals using tmux and launches all Airflow necessary components in those terminals. To know more about tmux commands, -check out this cheat sheet: https://tmuxcheatsheet.com/. Now You can also access Airflow UI on your local machine at |http://localhost:28080| with user name ``admin`` and password ``admin``. To exit breeze, type ``stop_airflow`` in any +check out this cheat sheet: https://tmuxcheatsheet.com/. Now You can also access Airflow UI on your local machine at `http://localhost:28080 `_ with user name ``admin`` and password ``admin``. To exit breeze, type ``stop_airflow`` in any of the tmux panes and hit Enter **Working with DAGs in Breeze:** diff --git a/contributing-docs/04_how_to_contribute.rst b/contributing-docs/04_how_to_contribute.rst index e3bd22318a086..830ecfce41879 100644 --- a/contributing-docs/04_how_to_contribute.rst +++ b/contributing-docs/04_how_to_contribute.rst @@ -21,7 +21,9 @@ How to contribute There are various ways how you can contribute to Apache Airflow. Here is a short overview of some of those ways that involve creating issues and pull requests on GitHub. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Report Bugs ----------- diff --git a/contributing-docs/05_pull_requests.rst b/contributing-docs/05_pull_requests.rst index f936210a8e2fb..ae8678710d95a 100644 --- a/contributing-docs/05_pull_requests.rst +++ b/contributing-docs/05_pull_requests.rst @@ -1,4 +1,4 @@ - +contributing-docs/05_pull_requests.rst .. Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information @@ -22,7 +22,9 @@ Pull Requests This document describes how you can create Pull Requests (PRs) and describes coding standards we use when implementing them. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Protect your commit identity ---------------------------- @@ -75,6 +77,15 @@ Pull Request guidelines Before you submit a Pull Request (PR) from your forked repo, check that it meets these guidelines: +- Start with **Draft**: Until you are sure that your PR passes all the quality checks and tests, keep it + in **Draft** status. This will signal to maintainers that the PR is not yet ready + for review and it will prevent maintainers from accidentally merging it before + it's ready. Once you are sure that your PR is ready for review, you can mark it as + "Ready for review" in the GitHub UI. Our regular check will convert all PRs from + non-collaborators that do not pass our quality gates to Draft status, so if you see + that your PR is in Draft status and you haven't set it to Draft. Check the + comments to see what needs to be fixed. + - Include tests, either as doctests, unit tests, or both, to your pull request. The Airflow repo uses `GitHub Actions `_ to @@ -147,6 +158,57 @@ these guidelines: - Adhere to guidelines for commit messages described in this `article `_. This makes the lives of those who come after you (and your future self) a lot easier. +.. _pull-request-quality-criteria: + +Pull Request quality criteria +----------------------------- + +Every open PR must meet the following minimum quality criteria before maintainers will review it. +PRs that do not meet these criteria may be automatically converted to **draft** status by project +tooling, with a comment explaining what needs to be fixed. + +1. **Descriptive title** — The PR title must clearly describe the change. + Generic titles such as "Fix bug", "Update code", "Changes", single-word titles, or titles + that only reference an issue number (e.g. "Fixes #12345") do not meet this bar. + +2. **Meaningful description** — The PR body must contain a meaningful description of *what* the + PR does and *why*. An empty body, a body consisting only of the PR template + checkboxes/headers with no added text, or a body that merely repeats the title is not + sufficient. + +3. **Passing static checks** — The PR's static checks (pre-commit / ruff / mypy) must pass. + You can run them locally with ``prek run --from-ref main`` before pushing. + +4. **Gen-AI disclosure** — If the PR was created with the assistance of generative AI tools, + the description must include a disclosure (see `Gen-AI Assisted contributions`_ below). + +5. **Coherent changes** — The PR should contain related changes only. Completely unrelated + changes bundled together will be flagged. + +**What happens when a PR is converted to draft?** + +- The comment informs you what you need to do. +- Fix each issue, then mark the PR as "Ready for review" in the GitHub UI - but only after making sure that all the issues are fixed. +- Maintainers will then proceed with a normal review. + +Converting a PR to draft is **not** a rejection — it is an invitation to bring the PR up to +the project's standards so that maintainer review time is spent productively. + +**What happens when a PR is closed for quality violations?** + +If a contributor has more than 3 open PRs that are flagged for quality issues, maintainers may +choose to **close** the PR instead of converting it to draft. Closed PRs receive the +``closed because of multiple quality violations`` label and a comment listing the violations. +Contributors are welcome to open a new PR that addresses the issues listed in the comment. + +**What happens when suspicious changes are detected?** + +When maintainers review a PR's diff before approving CI workflow runs and determine that it +contains suspicious changes (e.g. attempts to exfiltrate secrets, modify CI pipelines +maliciously, or inject harmful code), **all open PRs by the same author** will be closed +and labeled ``suspicious changes detected``. A comment is posted on each PR explaining that +the closure was triggered by suspicious changes found in the flagged PR. + Gen-AI Assisted contributions ----------------------------- diff --git a/contributing-docs/06_development_environments.rst b/contributing-docs/06_development_environments.rst index 67c7734a3d85d..ea03ffa5b346a 100644 --- a/contributing-docs/06_development_environments.rst +++ b/contributing-docs/06_development_environments.rst @@ -21,7 +21,9 @@ Development Environments There are two environments, available on Linux and macOS, that you can use to develop Apache Airflow. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Local virtualenv Development Environment ---------------------------------------- diff --git a/contributing-docs/07_local_virtualenv.rst b/contributing-docs/07_local_virtualenv.rst index 82e823140a6ba..771d644775c9f 100644 --- a/contributing-docs/07_local_virtualenv.rst +++ b/contributing-docs/07_local_virtualenv.rst @@ -26,7 +26,9 @@ harder to debug the tests and to use your IDE to run them. That's why we recommend using local virtualenv for development and testing. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Required Software Packages -------------------------- diff --git a/contributing-docs/08_static_code_checks.rst b/contributing-docs/08_static_code_checks.rst index 1ddb14c4f9383..2750cbafe5dc7 100644 --- a/contributing-docs/08_static_code_checks.rst +++ b/contributing-docs/08_static_code_checks.rst @@ -26,7 +26,9 @@ for the first time. You can also run the checks via `Breeze <../dev/breeze/doc/README.rst>`_ environment. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Prek hooks ---------- @@ -175,13 +177,13 @@ But you can run prek hooks manually as needed. .. code-block:: bash - prek mypy-airflow-core mypy-dev --hook-stage pre-push + prek mypy-airflow-core mypy-dev --stage pre-push - Run only mypy airflow checks on all "airflow-core" files by using: .. code-block:: bash - prek mypy-airflow-core --all-files --hook-stage pre-push + prek mypy-airflow-core --all-files --stage pre-push - Run all pre-commit stage hooks on all files by using: @@ -272,7 +274,7 @@ Manual prek hooks Most of the checks we run are configured to run automatically when you commit the code or push PR. However, there are some checks that are not run automatically and you need to run them manually. You can run -them manually by running ``prek --hook-stage manual ``. +them manually by running ``prek --stage manual ``. Special pin-versions prek ------------------------- @@ -287,7 +289,7 @@ However, you can run it manually by running: .. code-block:: bash export GITHUB_TOKEN=YOUR_GITHUB_TOKEN - prek --all-files --hook-stage manual --verbose pin-versions + prek --all-files --stage manual --verbose pin-versions Mypy checks @@ -309,19 +311,19 @@ command (example for ``airflow`` files): .. code-block:: bash - prek --hook-stage manual mypy- --all-files + prek --stage manual mypy- --all-files For example: .. code-block:: bash - prek --hook-stage manual mypy-airflow --all-files + prek --stage manual mypy-airflow --all-files To show unused mypy ignores for any providers/airflow etc, eg: run below command: .. code-block:: bash export SHOW_UNUSED_MYPY_WARNINGS=true - prek --hook-stage manual mypy-airflow --all-files + prek --stage manual mypy-airflow --all-files MyPy uses a separate docker-volume (called ``mypy-cache-volume``) that keeps the cache of last MyPy execution in order to speed MyPy checks up (sometimes by order of magnitude). While in most cases MyPy diff --git a/contributing-docs/10_working_with_git.rst b/contributing-docs/10_working_with_git.rst index 15372e2557f9d..5551b1ea513dd 100644 --- a/contributing-docs/10_working_with_git.rst +++ b/contributing-docs/10_working_with_git.rst @@ -22,7 +22,9 @@ Working with Git In this document you can learn basics of how you should use Git in Airflow project. It explains branching model and stresses that we are using rebase workflow. It also explains how to sync your fork with the main repository. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Airflow Git Branches ==================== diff --git a/contributing-docs/12_provider_distributions.rst b/contributing-docs/12_provider_distributions.rst index cb2bb42e55c71..523ad2a8fca40 100644 --- a/contributing-docs/12_provider_distributions.rst +++ b/contributing-docs/12_provider_distributions.rst @@ -25,7 +25,9 @@ Airflow is split into core and providers. They are delivered as separate distrib * ``apache-airflow-task-sdk`` - task-sdk distribution that are imported by the providers * ``apache-airflow-providers-*`` - More than 90 providers to communicate with external services -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Where providers are kept in our repository ------------------------------------------ diff --git a/contributing-docs/13_airflow_dependencies_and_extras.rst b/contributing-docs/13_airflow_dependencies_and_extras.rst index 8e835cba81892..cd43e486ffbdf 100644 --- a/contributing-docs/13_airflow_dependencies_and_extras.rst +++ b/contributing-docs/13_airflow_dependencies_and_extras.rst @@ -419,9 +419,6 @@ You can read more about those extras in the `extras reference `_. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** - - ----- You can now check how to update Airflow's `metadata database <14_metadata_database_updates.rst>`__ if you need diff --git a/contributing-docs/14_metadata_database_updates.rst b/contributing-docs/14_metadata_database_updates.rst index 12ac4cdeeec55..d2d696be3ffe1 100644 --- a/contributing-docs/14_metadata_database_updates.rst +++ b/contributing-docs/14_metadata_database_updates.rst @@ -63,8 +63,6 @@ When rebasing your branch onto the latest ``main``, you may encounter conflicts The affected files may include: -- ``docs/apache-airflow/img/airflow_erd.sha256`` -- ``docs/apache-airflow/img/airflow_erd.svg`` - ``docs/apache-airflow/migrations-ref.rst`` - ``airflow/migrations/versions/1234_A_B_C_.py`` @@ -73,15 +71,19 @@ The affected files may include: To resolve these conflicts: 1. First, resolve all conflicts **except** those in the files listed above. This includes conflicts in other ``.py`` files within the ``airflow/`` or ``tests/`` directories. -2. Then, run the following commands to automatically update the affected files: +2. Then, run the following command to automatically update the affected files: .. code-block:: bash prek update-migration-references --all-files - prek update-er-diagram --all-files 3. Add the updated files to the staging area and continue with the rebase. +.. note:: + + The ERD diagram (``airflow_erd.svg``) is no longer committed to the repository. It is + automatically generated during the documentation build by the ``generate_erd`` Sphinx extension. + Running migration CI tests locally ---------------------------------- diff --git a/contributing-docs/15_node_environment_setup.rst b/contributing-docs/15_node_environment_setup.rst index cdbb8840c8d02..34285e9df7da0 100644 --- a/contributing-docs/15_node_environment_setup.rst +++ b/contributing-docs/15_node_environment_setup.rst @@ -107,8 +107,6 @@ Project Structure - ``/src/theme.ts`` the theme for the UI, update this to change the colors, fonts, etc. - ``/src/queryClient.ts`` the query client for the UI, update this to change the default options for the API requests -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** - React, JSX and Chakra --------------------- diff --git a/contributing-docs/16_adding_api_endpoints.rst b/contributing-docs/16_adding_api_endpoints.rst index 85ccf1f94e72a..b4485024a5e19 100644 --- a/contributing-docs/16_adding_api_endpoints.rst +++ b/contributing-docs/16_adding_api_endpoints.rst @@ -20,7 +20,9 @@ Adding a New API Endpoint in Apache Airflow This documentation outlines the steps required to add a new API endpoint in Apache Airflow. It includes implementing the logic, running prek hooks, and documenting the changes. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Introduction diff --git a/contributing-docs/18_contribution_workflow.rst b/contributing-docs/18_contribution_workflow.rst index 8cbc37c98cd12..4dec1a0740e96 100644 --- a/contributing-docs/18_contribution_workflow.rst +++ b/contributing-docs/18_contribution_workflow.rst @@ -18,7 +18,9 @@ Contribution Workflow ===================== -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Typically, you start your first contribution by reviewing open tickets at `GitHub issues `__. diff --git a/contributing-docs/23_provider_hook_migration_to_yaml.rst b/contributing-docs/23_provider_hook_migration_to_yaml.rst index ce9a3fd8dd5d2..f751f9675a901 100644 --- a/contributing-docs/23_provider_hook_migration_to_yaml.rst +++ b/contributing-docs/23_provider_hook_migration_to_yaml.rst @@ -21,7 +21,9 @@ Provider hook to YAML Migration We can now, redefine connection form metadata declaratively in ``provider.yaml`` of a provider instead of Python hook code, reducing dependencies and improving API server startup performance. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Background ---------- diff --git a/contributing-docs/quick-start-ide/contributors_quick_start_gitpod.rst b/contributing-docs/quick-start-ide/contributors_quick_start_gitpod.rst index 580844fb17f06..08ae88c1f5c2a 100644 --- a/contributing-docs/quick-start-ide/contributors_quick_start_gitpod.rst +++ b/contributing-docs/quick-start-ide/contributors_quick_start_gitpod.rst @@ -15,7 +15,9 @@ specific language governing permissions and limitations under the License. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Connect your project to Gitpod ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/contributing-docs/quick-start-ide/contributors_quick_start_pycharm.rst b/contributing-docs/quick-start-ide/contributors_quick_start_pycharm.rst index cdb691a7e5ba7..09e7599f7dea4 100644 --- a/contributing-docs/quick-start-ide/contributors_quick_start_pycharm.rst +++ b/contributing-docs/quick-start-ide/contributors_quick_start_pycharm.rst @@ -15,7 +15,9 @@ specific language governing permissions and limitations under the License. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Setup your project ################## diff --git a/contributing-docs/quick-start-ide/contributors_quick_start_vscode.rst b/contributing-docs/quick-start-ide/contributors_quick_start_vscode.rst index 3291b3c93d740..414acaeea4e98 100644 --- a/contributing-docs/quick-start-ide/contributors_quick_start_vscode.rst +++ b/contributing-docs/quick-start-ide/contributors_quick_start_vscode.rst @@ -15,7 +15,9 @@ specific language governing permissions and limitations under the License. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Setup your project ################## diff --git a/contributing-docs/testing/docker_compose_tests.rst b/contributing-docs/testing/docker_compose_tests.rst index 68ee83895e69b..8c8dcb8306cfb 100644 --- a/contributing-docs/testing/docker_compose_tests.rst +++ b/contributing-docs/testing/docker_compose_tests.rst @@ -20,7 +20,9 @@ Airflow Docker Compose Tests This document describes how to run tests for Airflow Docker Compose deployment. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Running Docker Compose Tests with Breeze ---------------------------------------- diff --git a/contributing-docs/testing/integration_tests.rst b/contributing-docs/testing/integration_tests.rst index 1158c59bc9770..36dadb6a529c0 100644 --- a/contributing-docs/testing/integration_tests.rst +++ b/contributing-docs/testing/integration_tests.rst @@ -24,7 +24,9 @@ These tests require ``airflow`` Docker image and extra images with integrations The integration tests are all stored in the ``tests/integration`` folder, and similarly to the unit tests they all run using `pytest `_, but they are skipped by default unless ``--integration`` flag is passed to pytest. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Enabling Integrations --------------------- @@ -80,6 +82,8 @@ core or provider type of test. +---------------+-------------------------------------------------------+ | openlineage | Integration required for Openlineage hooks. | +---------------+-------------------------------------------------------+ +| opensearch | Integration required for OpenSearch hooks. | ++---------------+-------------------------------------------------------+ | otel | Integration required for OTEL/opentelemetry hooks. | +---------------+-------------------------------------------------------+ | pinot | Integration required for Apache Pinot hooks. | diff --git a/contributing-docs/testing/k8s_tests.rst b/contributing-docs/testing/k8s_tests.rst index bfb823cf8288b..084bb5e092342 100644 --- a/contributing-docs/testing/k8s_tests.rst +++ b/contributing-docs/testing/k8s_tests.rst @@ -25,7 +25,9 @@ deploy and run the cluster tests in our repository and into Breeze development e KinD has a really nice ``kind`` tool that you can use to interact with the cluster. Run ``kind --help`` to learn more. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: K8S test environment -------------------- diff --git a/contributing-docs/testing/system_tests.rst b/contributing-docs/testing/system_tests.rst index ffe0b4c7c90e1..19341c7ba5e67 100644 --- a/contributing-docs/testing/system_tests.rst +++ b/contributing-docs/testing/system_tests.rst @@ -25,7 +25,9 @@ external services. A system test tries to look as close to a regular Dag as poss System tests need to communicate with external services/systems that are available if you have appropriate credentials configured for your tests. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Purpose of System Tests ----------------------- diff --git a/contributing-docs/testing/testing_packages.rst b/contributing-docs/testing/testing_packages.rst index c9024b07896a8..324eebca9b472 100644 --- a/contributing-docs/testing/testing_packages.rst +++ b/contributing-docs/testing/testing_packages.rst @@ -22,7 +22,9 @@ Breeze can be used to test new release candidates of distributions - both Airflo configure the CI image of Breeze to install and start Airflow for both Airflow and providers, whether they are built from sources or downloaded from PyPI as release candidates. -**The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Prerequisites ------------- diff --git a/contributing-docs/testing/unit_tests.rst b/contributing-docs/testing/unit_tests.rst index f8c08f9c1763c..272824cec522d 100644 --- a/contributing-docs/testing/unit_tests.rst +++ b/contributing-docs/testing/unit_tests.rst @@ -20,7 +20,9 @@ Airflow Unit Tests All unit tests for Apache Airflow are run using `pytest `_. -**The outline for this document in GitHub is available via the button in the top-right corner (icon with 3 dots and 3 lines).** +.. contents:: Table of Contents + :depth: 2 + :local: Writing Unit Tests ------------------ diff --git a/dev/README_RELEASE_PROVIDERS.md b/dev/README_RELEASE_PROVIDERS.md index 2d6e2fda5ceb1..da841c6ae34aa 100644 --- a/dev/README_RELEASE_PROVIDERS.md +++ b/dev/README_RELEASE_PROVIDERS.md @@ -259,7 +259,12 @@ changelogs. If there are, you need to add them to PR and classify the changes ma * if needed adjust version of provider - in changelog and provider.yaml, in case the new change changes classification of the upgrade (patchlevel/minor/major) -Commit the changes and merge the PR, be careful to do it quickly so that no new PRs are merged for +Commit the changes and create the PR. You need to apply the following labels to the PR: + +* `skip common compat check` +* `allow provider dependency bump` + +Once approved, merge it, be careful to do it quickly so that no new PRs are merged for providers in the meantime - if they are, you will miss them in the changelog. In case you want to also release a pre-installed provider that is in ``not-ready`` state (i.e. when diff --git a/dev/breeze/doc/10_ui_tasks.rst b/dev/breeze/doc/10_ui_tasks.rst index 974a21dfccf2f..ee98f3770e4da 100644 --- a/dev/breeze/doc/10_ui_tasks.rst +++ b/dev/breeze/doc/10_ui_tasks.rst @@ -80,11 +80,11 @@ Example usage: # Add missing translations with TODO markers breeze ui check-translation-completeness --add-missing - # Remove extra translations not present in English - breeze ui check-translation-completeness --remove-extra + # Remove unused translations (keys not required) + breeze ui check-translation-completeness --remove-unused # Fix translations for a specific language - breeze ui check-translation-completeness --language de --add-missing --remove-extra + breeze ui check-translation-completeness --language de --add-missing --remove-unused ----- diff --git a/dev/breeze/doc/11_registry_tasks.rst b/dev/breeze/doc/11_registry_tasks.rst index acbd90ffeacd6..9be90b576b3ae 100644 --- a/dev/breeze/doc/11_registry_tasks.rst +++ b/dev/breeze/doc/11_registry_tasks.rst @@ -50,6 +50,58 @@ Example usage: # Extract with a specific Python version breeze registry extract-data --python 3.12 +Backfilling older versions +.......................... + +The ``breeze registry backfill`` command extracts runtime parameters and connection +types for older provider versions without Docker. It uses ``uv run --with`` to +install the specific provider version in a temporary environment and runs +``extract_parameters.py`` and ``extract_connections.py``. + +This is useful when you need to add pages for previously released versions that +were not included in the initial registry build. + +.. image:: ./images/output_registry_backfill.svg + :target: https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/doc/images/output_registry_backfill.svg + :width: 100% + :alt: Breeze registry backfill + +Example usage: + +.. code-block:: bash + + # Backfill a single version + breeze registry backfill --provider amazon --version 9.15.0 + + # Backfill multiple versions at once + breeze registry backfill --provider amazon --version 9.15.0 --version 9.14.0 --version 9.13.0 + + # Backfill a hyphenated provider + breeze registry backfill --provider microsoft-azure --version 11.0.0 + +Each run uses an isolated temporary ``providers.json``, so different providers +can be backfilled in parallel from separate terminal sessions: + +.. code-block:: bash + + # Terminal 1 + breeze registry backfill --provider amazon --version 9.15.0 --version 9.14.0 + + # Terminal 2 (safe to run simultaneously) + breeze registry backfill --provider google --version 14.0.0 --version 13.0.0 + +Output is written to ``registry/src/_data/versions/{provider}/{version}/``: + +- ``parameters.json`` — operator/sensor/hook parameters +- ``connections.json`` — connection type definitions + +After backfilling, you still need to: + +1. Extract metadata from git tags: ``uv run python dev/registry/extract_versions.py --provider {id} --version {version}`` +2. Build the Eleventy site: ``cd registry && pnpm build`` +3. Sync new version pages to S3 +4. Run ``breeze registry publish-versions`` to update version dropdowns + Publishing version metadata .......................... @@ -81,5 +133,4 @@ Example usage: ----- -Next step: Follow the `Advanced Breeze topics <13_advanced_breeze_topics.rst>`__ instructions to learn more -about advanced Breeze topics and internals. +Next step: Follow the `Issue tasks <12_issue_tasks.rst>`__ instructions to learn more about issue tasks. diff --git a/dev/breeze/doc/12_issues_tasks.rst b/dev/breeze/doc/12_issues_tasks.rst index de4ab617d4894..8f88e032c8518 100644 --- a/dev/breeze/doc/12_issues_tasks.rst +++ b/dev/breeze/doc/12_issues_tasks.rst @@ -54,5 +54,5 @@ Example usage: ----- -Next step: Follow the `Advanced Breeze topics <12_advanced_breeze_topics.rst>`__ instructions to learn more -about advanced Breeze topics and internals. +Next step: Follow the `Pull request tasks <13_pr_tasks.rst>`__ instructions to learn how to manage GitHub +pull requests with Breeze. diff --git a/dev/breeze/doc/13_pr_tasks.rst b/dev/breeze/doc/13_pr_tasks.rst new file mode 100644 index 0000000000000..b4f0d1a5890aa --- /dev/null +++ b/dev/breeze/doc/13_pr_tasks.rst @@ -0,0 +1,117 @@ + .. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + .. http://www.apache.org/licenses/LICENSE-2.0 + + .. Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +Pull request tasks +------------------ + +There are Breeze commands that help maintainers manage GitHub pull requests for the Apache Airflow project. + +Those are all of the available PR commands: + +.. image:: ./images/output_pr.svg + :target: https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/doc/images/output_pr.svg + :width: 100% + :alt: Breeze PR commands + +Auto-triaging PRs +""""""""""""""""" + +The ``breeze pr auto-triage`` command finds open PRs from non-collaborators that don't meet +minimum quality criteria and lets maintainers take action on them interactively. + +.. image:: ./images/output_pr_auto-triage.svg + :target: https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/doc/images/output_pr_auto-triage.svg + :width: 100% + :alt: Breeze PR auto-triage + +The command works in several phases: + +1. **Fetch** — Fetches open, non-draft PRs via the GitHub GraphQL API. +2. **Filter** — Skips collaborators, bot accounts (dependabot, renovate, github-actions), + and PRs already labeled ``ready for maintainer review``. +3. **Assess** — Runs deterministic checks (CI failures, merge conflicts, missing test + workflows) and optionally LLM-based quality assessment (via ``claude`` or ``codex`` CLI). +4. **Triage** — Presents flagged PRs grouped by author. For each PR the maintainer chooses + an action: + + * **[D]raft** — Convert to draft and post a comment listing the violations (default for + most PRs). + * **[C]lose** — Close the PR, add the ``closed because of multiple quality violations`` + label, and post a comment. This is the suggested default when the author has more than + 3 flagged PRs. + * **[R]eady** — Add the ``ready for maintainer review`` label so the PR is skipped in + future runs. + * **[S]kip** — Take no action on this PR. + * **[Q]uit** — Stop processing. + + The command computes a smart default action based on CI failures, merge conflicts, LLM + assessment, and the number of flagged PRs by the same author. In ``--dry-run`` mode the + default action is displayed without prompting. + +5. **Workflow approval** — PRs with no test workflows run are presented for workflow + approval. Before approving, the maintainer reviews the full PR diff to check for + suspicious changes (e.g. attempts to exfiltrate secrets or modify CI pipelines). If + suspicious changes are confirmed, **all open PRs by that author** are closed, labeled + ``suspicious changes detected``, and commented. + +Labels used by auto-triage +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The command uses the following GitHub labels to track triage state: + +``ready for maintainer review`` + Applied when a maintainer chooses the **[R]eady** action on a flagged PR. PRs with this + label are automatically skipped in future triage runs, indicating the maintainer has + reviewed the flags and considers the PR acceptable for review. + +``closed because of multiple quality violations`` + Applied when a maintainer chooses the **[C]lose** action. This label marks PRs that were + closed because they did not meet quality criteria and the author had more than 3 flagged + PRs open at the time. A comment listing the violations is posted on the PR. + +``suspicious changes detected`` + Applied when a maintainer identifies suspicious changes (e.g. secret exfiltration attempts, + malicious CI modifications) while reviewing a PR diff during the workflow approval phase. + When this label is applied, **all open PRs by the same author** are closed and labeled, + with a comment explaining the reason. + +These labels must exist in the GitHub repository before using the command. If a label is +missing, the command will print a warning and skip the labeling step. + +Example usage: + +.. code-block:: bash + + # Dry run to see which PRs would be flagged and what action would be taken + breeze pr auto-triage --dry-run + + # Run with CI checks only (no LLM) + breeze pr auto-triage --check-mode ci + + # Filter by label and author + breeze pr auto-triage --label area:core --author some-user + + # Limit to 10 PRs + breeze pr auto-triage --max-num 10 + + # Verbose mode — show individual skip reasons during filtering + breeze pr auto-triage --verbose + +----- + +Next step: Follow the `Advanced breeze topics <14_advanced_breeze_topics.rst>`__ instructions to learn how to manage GitHub +pull requests with Breeze. diff --git a/dev/breeze/doc/13_advanced_breeze_topics.rst b/dev/breeze/doc/14_advanced_breeze_topics.rst similarity index 100% rename from dev/breeze/doc/13_advanced_breeze_topics.rst rename to dev/breeze/doc/14_advanced_breeze_topics.rst diff --git a/dev/breeze/doc/README.rst b/dev/breeze/doc/README.rst index 473c679b790d0..5ae0bcbc5a2dc 100644 --- a/dev/breeze/doc/README.rst +++ b/dev/breeze/doc/README.rst @@ -51,7 +51,8 @@ The following documents describe how to use the Breeze environment: * `UI tasks <10_ui_tasks.rst>`_ - describes how Breeze commands are used to support Apache Airflow project UI. * `Registry tasks <11_registry_tasks.rst>`_ - describes how to use Breeze for provider registry data extraction. * `Issues tasks <12_issues_tasks.rst>`_ - describes how Breeze commands are used to manage GitHub issues. -* `Advanced Breeze topics <13_advanced_breeze_topics.rst>`_ - describes advanced Breeze topics/internals of Breeze. +* `Pull request tasks <13_pr_tasks.rst>`_ - describes how Breeze commands are used to manage GitHub pull requests. +* `Advanced Breeze topics <14_advanced_breeze_topics.rst>`_ - describes advanced Breeze topics/internals of Breeze. You can also learn more context and Architecture Decisions taken when developing Breeze in the `Architecture Decision Records `_. diff --git a/dev/breeze/doc/ci/01_ci_environment.md b/dev/breeze/doc/ci/01_ci_environment.md index d434cf84d0537..c785018dbd533 100644 --- a/dev/breeze/doc/ci/01_ci_environment.md +++ b/dev/breeze/doc/ci/01_ci_environment.md @@ -36,7 +36,7 @@ robust and stable. We run a lot of tests for every pull request, for `canary` runs from `main` and `v*-\*-test` branches regularly as scheduled jobs. -Our execution environment for CI is [GitHub Actions](https://github.com/features/actions). GitHub Actions. +Our execution environment for CI is [GitHub Actions](https://github.com/features/actions). However. part of the philosophy we have is that we are not tightly coupled with any of the CI environments we use. Most of our CI jobs are diff --git a/dev/breeze/doc/ci/02_images.md b/dev/breeze/doc/ci/02_images.md index ac9772f4a0f10..a24490262c85e 100644 --- a/dev/breeze/doc/ci/02_images.md +++ b/dev/breeze/doc/ci/02_images.md @@ -342,7 +342,7 @@ faster. It is enough to download and uncompress the artifact that stores the image and run ``breeze ci-image load -i `` to load the image and mark the image as refreshed in the local cache. -You can see more details and examples in[Breeze](../06_managing_docker_images.rst) +You can see more details and examples in [Breeze](../06_managing_docker_images.rst). # Customizing the CI image @@ -350,7 +350,7 @@ Customizing the CI image allows to add your own dependencies to the image. The easiest way to build the customized image is to use `breeze` script, -but you can also build suc customized image by running appropriately +but you can also build such customized image by running appropriately crafted docker build in which you specify all the `build-args` that you need to add to customize it. You can read about all the args and ways you can build the image in the @@ -443,8 +443,8 @@ can be used for CI images: | `ADDITIONAL_DEV_APT_DEPS` | | Additional apt dev dependencies installed in the first part of the image | | `ADDITIONAL_DEV_APT_ENV` | | Additional env variables defined when installing dev deps | | `AIRFLOW_PIP_VERSION` | `26.0.1` | `pip` version used. | -| `AIRFLOW_UV_VERSION` | `0.10.8` | `uv` version used. | -| `AIRFLOW_PREK_VERSION` | `0.3.4` | `prek` version used. | +| `AIRFLOW_UV_VERSION` | `0.10.9` | `uv` version used. | +| `AIRFLOW_PREK_VERSION` | `0.3.5` | `prek` version used. | | `AIRFLOW_USE_UV` | `true` | Whether to use UV for installation. | | `PIP_PROGRESS_BAR` | `on` | Progress bar for PIP installation | diff --git a/dev/breeze/doc/ci/04_selective_checks.md b/dev/breeze/doc/ci/04_selective_checks.md index daeba8b4b17a2..ab20ef96adde2 100644 --- a/dev/breeze/doc/ci/04_selective_checks.md +++ b/dev/breeze/doc/ci/04_selective_checks.md @@ -160,7 +160,7 @@ providers. The selective check outputs available are described below. In case of `list-as-string` values, empty string means `everything`, where lack of the output means `nothing` and list elements are -separated by spaces. This is to accommodate for the wau how outputs of this kind can be easily used by +separated by spaces. This is to accommodate for the way how outputs of this kind can be easily used by GitHub Actions to pass the list of parameters to a command to execute @@ -185,7 +185,7 @@ GitHub Actions to pass the list of parameters to a command to execute | docker-cache | Which cache should be used for images ("registry", "local" , "disabled") | registry | | | docs-build | Whether to build documentation ("true"/"false") | true | | | docs-list-as-string | What filter to apply to docs building - based on which documentation packages should be built | apache-airflow helm-chart google | * | -| excluded-providers-as-string c | List of providers that should be excluded from the build as space-separated string | amazon google | * | +| excluded-providers-as-string | List of providers that should be excluded from the build as space-separated string | amazon google | * | | force-pip | Whether pip should be forced in the image build instead of uv ("true"/"false") | false | | | full-tests-needed | Whether this build runs complete set of tests or only subset (for faster PR builds) \[1\] | false | | | generated-dependencies-changed | Whether generated dependencies have changed ("true"/"false") | false | | diff --git a/dev/breeze/doc/ci/README.md b/dev/breeze/doc/ci/README.md index c5153e0757aa1..ad4fec8d82396 100644 --- a/dev/breeze/doc/ci/README.md +++ b/dev/breeze/doc/ci/README.md @@ -24,9 +24,9 @@ This directory contains detailed design of the Airflow CI setup. * [CI Environment](01_ci_environment.md) - contains description of the CI environment -* [Image Naming](02_images.md) - contains description of the naming conventions for the images +* [Images](02_images.md) - contains description of the CI and PROD images, how to build and customize them, and naming conventions * [GitHub Variables](03_github_variables.md) - contains description of the GitHub variables used in CI -* [Selective checks](04_selective_checks.md) - contains description of the selective checks performed in CI +* [Selective Checks](04_selective_checks.md) - contains description of the selective checks performed in CI * [Workflows](05_workflows.md) - contains description of the workflows used in CI * [Debugging](06_debugging.md) - contains description of debugging CI issues * [Running CI Locally](07_running_ci_locally.md) - contains description of running CI locally diff --git a/dev/breeze/doc/images/output-commands.svg b/dev/breeze/doc/images/output-commands.svg index 263431946505b..1fed95c89acc5 100644 --- a/dev/breeze/doc/images/output-commands.svg +++ b/dev/breeze/doc/images/output-commands.svg @@ -1,4 +1,4 @@ - + --integration                                          Core Integrations to enable when running (can be more   than one). (all | all-testable | cassandra | celery |  drill | elasticsearch | kafka | kerberos | keycloak |  -localstack | mongo | mssql | openlineage | otel | pinot -| qdrant | redis | redis | statsd | tinkerpop | trino | -ydb) +localstack | mongo | mssql | openlineage | opensearch | +otel | pinot | qdrant | redis | redis | statsd |  +tinkerpop | trino | ydb) --standalone-dag-processor/--no-standalone-dag-processoRun standalone dag processor for start-airflow          r(required for Airflow 3). [default:  standalone-dag-processor] @@ -365,63 +392,72 @@ ╭─ Database ───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ --backend               -bDatabase backend to use. Default is 'sqlite'. If 'none' is chosen, Breeze will start   with an invalid database configuration — no database will be available, and any        -attempt to run Airflow will fail. Use 'none' only for specific non-DB test cases.      -[default: sqlite](>sqlite< | mysql | postgres | none) ---postgres-version      -PVersion of Postgres used. [default: 14](13 | >14< | 15 | 16 | 17 | 18) ---mysql-version         -MVersion of MySQL used. [default: 8.0](>8.0< | 8.4) ---db-reset/--no-db-reset-dReset DB when entering the container. [default: no-db-reset] -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Build CI image (before entering shell) ─────────────────────────────────────────────────────────────────────────────╮ ---github-repository -gGitHub repository used to pull, push run images. [default: apache/airflow](TEXT) ---builder           Buildx builder used to perform `docker buildx build` commands. [default: autodetect] -(TEXT) ---use-uv/--no-use-uvUse uv instead of pip as packaging tool to build the image. [default: use-uv] +attempt to run Airflow will fail. Use 'none' only for specific non-DB test cases. If   +'custom' is chosen, no database container will be started and you must provide your    +own database connection via AIRFLOW__DATABASE__SQL_ALCHEMY_CONN environment variable.  +Only officially supported backends (postgres, mysql, sqlite) are tested. [default:  +sqlite](>sqlite< | mysql | postgres | none | custom) +--custom-db-url         SQLAlchemy connection URL for the custom database backend. Only used when              +--backend=custom is selected. Falls back to the AIRFLOW__DATABASE__SQL_ALCHEMY_CONN    +environment variable if not provided. (TEXT) +--postgres-version      -PVersion of Postgres used. [default: 14](13 | >14< | 15 | 16 | 17 | 18) +--mysql-version         -MVersion of MySQL used. [default: 8.0](>8.0< | 8.4) +--db-reset/--no-db-reset-dReset DB when entering the container. [default: no-db-reset] ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Other options ──────────────────────────────────────────────────────────────────────────────────────────────────────╮ ---forward-credentials-fForward local credentials to container when running. ---max-time           Maximum time that the command should take - if it takes longer, the command will fail.    -(INTEGER RANGE x>=1) -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Developer commands ─────────────────────────────────────────────────────────────────────────────────────────────────╮ -start-airflow          Enter breeze environment and starts all Airflow components in terminal multiplexer session. -Compile assets if contents of www directory changed.                                        -build-docs             Build documents.                                                                            -down                   Stop running breeze environment.                                                            -shell                  Enter breeze environment. this is the default command use when no other is selected.        -exec                   Joins the interactive shell of running airflow container.                                   -run                    Run a command in the Breeze environment without entering the interactive shell.             -cleanup                Cleans the cache of parameters, docker cache and optionally built CI/PROD images.           -generate-migration-fileAutogenerate the alembic migration file for the ORM changes.                                -doctor                 Auto-healing of breeze                                                                      -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Testing commands ───────────────────────────────────────────────────────────────────────────────────────────────────╮ -testing        Tools that developers can use to run tests                                                          -k8s            Tools that developers use to run Kubernetes tests                                                   -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Image commands ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ -ci-image         Tools that developers can use to manually manage CI images                                        -prod-image       Tools that developers can use to manually manage PROD images                                      -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Release management commands ────────────────────────────────────────────────────────────────────────────────────────╮ -release-management     Tools that release managers can use to prepare and manage Airflow releases                  -sbom                   Tools that release managers can use to prepare sbom information                             -workflow-run           Tools to manage Airflow repository workflows                                                -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ CI commands ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -ci   Tools that CI workflows use to cleanup/manage CI environment                                                  -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Registry commands ──────────────────────────────────────────────────────────────────────────────────────────────────╮ -registry             Tools for the Airflow Provider Registry                                                       +╭─ Build CI image (before entering shell) ─────────────────────────────────────────────────────────────────────────────╮ +--github-repository -gGitHub repository used to pull, push run images. [default: apache/airflow](TEXT) +--builder           Buildx builder used to perform `docker buildx build` commands. [default: autodetect] +(TEXT) +--use-uv/--no-use-uvUse uv instead of pip as packaging tool to build the image. [default: use-uv] +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Other options ──────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--forward-credentials-fForward local credentials to container when running. +--max-time           Maximum time that the command should take - if it takes longer, the command will fail.    +(INTEGER RANGE x>=1) +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Developer commands ─────────────────────────────────────────────────────────────────────────────────────────────────╮ +start-airflow          Enter breeze environment and starts all Airflow components in terminal multiplexer session. +Compile assets if contents of www directory changed.                                        +build-docs             Build documents.                                                                            +down                   Stop running breeze environment.                                                            +shell                  Enter breeze environment. this is the default command use when no other is selected.        +exec                   Joins the interactive shell of running airflow container.                                   +run                    Run a command in the Breeze environment without entering the interactive shell.             +cleanup                Cleans the cache of parameters, docker cache and optionally built CI/PROD images.           +generate-migration-fileAutogenerate the alembic migration file for the ORM changes.                                +doctor                 Auto-healing of breeze                                                                      +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Testing commands ───────────────────────────────────────────────────────────────────────────────────────────────────╮ +testing        Tools that developers can use to run tests                                                          +k8s            Tools that developers use to run Kubernetes tests                                                   +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Image commands ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ +ci-image         Tools that developers can use to manually manage CI images                                        +prod-image       Tools that developers can use to manually manage PROD images                                      +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Release management commands ────────────────────────────────────────────────────────────────────────────────────────╮ +release-management     Tools that release managers can use to prepare and manage Airflow releases                  +sbom                   Tools that release managers can use to prepare sbom information                             +workflow-run           Tools to manage Airflow repository workflows                                                ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ UI commands ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -ui     Tools for UI development and maintenance                                                                    +╭─ CI commands ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +ci   Tools that CI workflows use to cleanup/manage CI environment                                                  ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Issues commands ────────────────────────────────────────────────────────────────────────────────────────────────────╮ -issues             Tools for managing GitHub issues.                                                               +╭─ Registry commands ──────────────────────────────────────────────────────────────────────────────────────────────────╮ +registry             Tools for the Airflow Provider Registry                                                       ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Setup commands ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ -setup       Tools that developers can use to configure Breeze                                                      +╭─ UI commands ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +ui     Tools for UI development and maintenance                                                                    ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Issues commands ────────────────────────────────────────────────────────────────────────────────────────────────────╮ +issues             Tools for managing GitHub issues.                                                               +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ PR commands ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +pr     Tools for managing GitHub pull requests.                                                                    +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Setup commands ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ +setup       Tools that developers can use to configure Breeze                                                      +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/dev/breeze/doc/images/output_pr.svg b/dev/breeze/doc/images/output_pr.svg new file mode 100644 index 0000000000000..97cc4d5efe937 --- /dev/null +++ b/dev/breeze/doc/images/output_pr.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Command: pr + + + + + + + + + + +Usage:breeze pr[OPTIONSCOMMAND [ARGS]... + +Tools for managing GitHub pull requests. + +╭─ Common options ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--help-hShow this message and exit. +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ PR commands ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +auto-triage  Find open PRs from non-collaborators that don't meet quality criteria and convert to draft.           +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + diff --git a/dev/breeze/doc/images/output_pr.txt b/dev/breeze/doc/images/output_pr.txt new file mode 100644 index 0000000000000..070bb74360cf5 --- /dev/null +++ b/dev/breeze/doc/images/output_pr.txt @@ -0,0 +1 @@ +e48502d9b2f145e867ce6608e8cd8c9d diff --git a/dev/breeze/doc/images/output_pr_auto-triage.svg b/dev/breeze/doc/images/output_pr_auto-triage.svg new file mode 100644 index 0000000000000..75d526a0787a9 --- /dev/null +++ b/dev/breeze/doc/images/output_pr_auto-triage.svg @@ -0,0 +1,428 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Command: pr auto-triage + + + + + + + + + + +Usage:breeze pr auto-triage[OPTIONS] + +Find open PRs from non-collaborators that don't meet quality criteria and convert to draft. + +╭─ GitHub parameters ──────────────────────────────────────────────────────────────────────────────────────────────────╮ +--github-token     The token used to authenticate to GitHub. (TEXT) +--github-repository-gGitHub repository used to pull, push run images. [default: apache/airflow](TEXT) +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Target selection ───────────────────────────────────────────────────────────────────────────────────────────────────╮ +--prTriage a specific PR by number instead of searching. (INTEGER) +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Select people ──────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--author               Filter PRs to a specific author. (TEXT) +--include-collaboratorsInclude PRs from collaborators/members/owners (normally skipped). +--reviews-for-me       Only show PRs where review is requested for the authenticated user. +--reviews-for          Only show PRs where review is requested for this user. Can be repeated. (TEXT) +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Filter options ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--label                Filter PRs by label. Supports wildcards (e.g. 'area:*', 'provider:amazon*'). Can be         +repeated. (area:API | area:CLI | area:ConfigTemplates | area:DAG-processing |  +area:Executors-core | area:Lineage | area:Logging | area:Plugins | area:Scheduler |  +area:Secrets | area:Triggerer | area:UI | area:airflow-ctl | area:core-operators |  +area:db-migrations | area:deadline-alerts | area:dev-tools | area:docker-tests |  +area:go-sdk | area:helm-chart | area:kubernetes-tests | area:production-image |  +area:providers | area:registry | area:system-tests | area:task-sdk | area:translations |  +backport-to-v3-1-test | kind:documentation | provider:airbyte | provider:alibaba |  +provider:amazon | provider:apache-beam | provider:apache-cassandra | provider:apache-drill  +| provider:apache-druid | provider:apache-flink | provider:apache-hdfs |  +provider:apache-hive | provider:apache-iceberg | provider:apache-impala |  +provider:apache-kafka | provider:apache-kylin | provider:apache-livy | provider:apache-pig  +| provider:apache-pinot | provider:apache-spark | provider:apache-tinkerpop |  +provider:apprise | provider:arangodb | provider:asana | provider:atlassian-jira |  +provider:celery | provider:cloudant | provider:cncf-kubernetes | provider:cohere |  +provider:common-ai | provider:common-compat | provider:common-io |  +provider:common-messaging | provider:common-sql | provider:databricks | provider:datadog |  +provider:dbt-cloud | provider:dingding | provider:discord | provider:docker | provider:edge +| provider:elasticsearch | provider:exasol | provider:fab | provider:facebook |  +provider:ftp | provider:git | provider:github | provider:google | provider:grpc |  +provider:hashicorp | provider:http | provider:imap | provider:influxdb |  +provider:informatica | provider:jdbc | provider:jenkins | provider:keycloak |  +provider:microsoft-azure | provider:microsoft-mssql | provider:microsoft-psrp |  +provider:microsoft-winrm | provider:mongo | provider:mysql | provider:neo4j | provider:odbc +| provider:openai | provider:openfaas | provider:openlineage | provider:opensearch |  +provider:opsgenie | provider:oracle | provider:pagerduty | provider:papermill |  +provider:pgvector | provider:pinecone | provider:postgres | provider:presto |  +provider:qdrant | provider:redis | provider:salesforce | provider:samba | provider:segment  +| provider:sendgrid | provider:sftp | provider:singularity | provider:slack | provider:smtp +| provider:snowflake | provider:sqlite | provider:ssh | provider:standard |  +provider:tableau | provider:telegram | provider:teradata | provider:trino |  +provider:vertica | provider:weaviate | provider:yandex | provider:ydb | provider:zendesk |  +translation:ar | translation:ca | translation:de | translation:default | translation:el |  +translation:es | translation:fr | translation:he | translation:hi | translation:hu |  +translation:it | translation:ja | translation:ko | translation:nl | translation:pl |  +translation:pt | translation:th | translation:tr | translation:zh-CN | translation:zh-TW) +--exclude-label        Exclude PRs with this label. Supports wildcards. Can be repeated. (TEXT) +--created-after        Only PRs created on or after this date (YYYY-MM-DD). (TEXT) +--created-before       Only PRs created on or before this date (YYYY-MM-DD). (TEXT) +--updated-after        Only PRs updated on or after this date (YYYY-MM-DD). (TEXT) +--updated-before       Only PRs updated on or before this date (YYYY-MM-DD). (TEXT) +--include-drafts       Include draft PRs in triage (normally skipped). Passing drafts can be marked as ready for   +review.                                                                                     +--pending-approval-onlyOnly show PRs with workflow runs awaiting approval. +--checks-state         Only assess PRs with this CI checks state. [default: any](failure|success|pending|any) +--min-commits-behind   Only assess PRs that are at least this many commits behind base branch. [default: 0] +(INTEGER) +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Pagination and sorting ─────────────────────────────────────────────────────────────────────────────────────────────╮ +--batch-sizeNumber of PRs to fetch per GraphQL page. [default: 50](INTEGER) +--max-num   Maximum number of non-collaborator PRs to assess (0 = no limit). [default: 0](INTEGER) +--sort      Sort order for PR search results. [default: created-desc] +(created-asc|created-desc|updated-asc|updated-desc) +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Assessment options ─────────────────────────────────────────────────────────────────────────────────────────────────╮ +--check-mode     Which checks to run: 'both' (API + LLM), 'api' (deterministic only), 'llm' (LLM only). [default:  +both](both|api|llm) +--llm-model      LLM model for assessment (format: provider/model). Use 'claude/' prefix for Claude CLI, 'codex/'  +for OpenAI Codex CLI. [default: claude/claude-sonnet-4-6](claude/claude-opus-4-6 |  +>claude/claude-sonnet-4-6< | claude/claude-opus-4-20250514 | claude/claude-sonnet-4-20250514 |  +claude/claude-haiku-4-5-20251001 | claude/sonnet | claude/opus | claude/haiku | codex/o3 |  +codex/o4-mini | codex/gpt-4.1) +--llm-concurrencyNumber of concurrent LLM assessment calls. [default: 4](INTEGER) +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Action options ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--answer-triageForce answer to triage prompts: [d]raft, [c]lose, [r]eady, [s]kip, [q]uit, [y]es, [n]o.             +(d|c|r|s|q|y|n) +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Common options ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--dry-run-DIf dry-run is set, commands are only printed, not executed. +--verbose-vPrint verbose information about performed steps. +--help   -hShow this message and exit. +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + diff --git a/dev/breeze/doc/images/output_pr_auto-triage.txt b/dev/breeze/doc/images/output_pr_auto-triage.txt new file mode 100644 index 0000000000000..f744d4af0bf44 --- /dev/null +++ b/dev/breeze/doc/images/output_pr_auto-triage.txt @@ -0,0 +1 @@ +ee5968fdb0cc0eaaa8c65feff6c78139 diff --git a/dev/breeze/doc/images/output_registry.svg b/dev/breeze/doc/images/output_registry.svg index 80d5d4def0813..2a077a0ba4bd7 100644 --- a/dev/breeze/doc/images/output_registry.svg +++ b/dev/breeze/doc/images/output_registry.svg @@ -1,4 +1,4 @@ - + + + + HITL Review + + +
+ + + diff --git a/providers/common/ai/src/airflow/providers/common/ai/plugins/www/package.json b/providers/common/ai/src/airflow/providers/common/ai/plugins/www/package.json new file mode 100644 index 0000000000000..44d26e852c5b7 --- /dev/null +++ b/providers/common/ai/src/airflow/providers/common/ai/plugins/www/package.json @@ -0,0 +1,52 @@ +{ + "name": "hitl-review", + "packageManager": "pnpm@9.14.2", + "private": true, + "version": "0.0.0", + "engines": { + "node": ">=22" + }, + "type": "module", + "main": "./dist/main.js", + "module": "./dist/main.js", + "types": "./dist/main.d.ts", + "exports": { + ".": { + "import": "./dist/main.js", + "types": "./dist/main.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "vite --port 5174 --strictPort", + "build": "vite build", + "build:types": "tsc --p tsconfig.lib.json", + "build:lib": "vite build", + "lint": "tsc --p tsconfig.app.json --noEmit", + "preview": "vite preview" + }, + "dependencies": { + "@chakra-ui/react": "^3.34.0", + "@emotion/react": "^11.14.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react-swc": "^4.2.3", + "typescript": "~5.9.3", + "vite": "^7.3.1", + "vite-plugin-css-injected-by-js": "^3.5.2", + "vite-plugin-dts": "^4.5.4" + }, + "pnpm": { + "overrides": { + "minimatch@>=10.0.0 <10.2.3": ">=10.2.3" + } + } +} diff --git a/providers/common/ai/src/airflow/providers/common/ai/plugins/www/pnpm-lock.yaml b/providers/common/ai/src/airflow/providers/common/ai/plugins/www/pnpm-lock.yaml new file mode 100644 index 0000000000000..2512dc1099273 --- /dev/null +++ b/providers/common/ai/src/airflow/providers/common/ai/plugins/www/pnpm-lock.yaml @@ -0,0 +1,3753 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + minimatch@>=10.0.0 <10.2.3: '>=10.2.3' + +importers: + + .: + dependencies: + '@chakra-ui/react': + specifier: ^3.34.0 + version: 3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@19.2.14)(react@19.2.4) + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.14)(react@19.2.4) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 + devDependencies: + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react-swc': + specifier: ^4.2.3 + version: 4.2.3(@swc/helpers@0.5.19)(vite@7.3.1) + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1 + vite-plugin-css-injected-by-js: + specifier: ^3.5.2 + version: 3.5.2(vite@7.3.1) + vite-plugin-dts: + specifier: ^4.5.4 + version: 4.5.4(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1) + +packages: + + '@ark-ui/react@5.34.1': + resolution: {integrity: sha512-RJlXCvsHzbK9LVxUVtaSD5pyF1PL8IUR1rHHkf0H0Sa397l6kOFE4EH7MCSj3pDumj2NsmKDVeVgfkfG0KCuEw==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@chakra-ui/react@3.34.0': + resolution: {integrity: sha512-VLhpVwv5IVxhwajO10KnS1VQT4hDqQMQP/A796Ya+uVu8AdoSX+5HHyTLTkYIeXIDMe0xLqJfov04OBKbBchJA==} + peerDependencies: + '@emotion/react': '>=11' + react: '>=18' + react-dom: '>=18' + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@internationalized/date@3.11.0': + resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==} + + '@internationalized/number@3.6.5': + resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@microsoft/api-extractor-model@7.33.4': + resolution: {integrity: sha512-u1LTaNTikZAQ9uK6KG1Ms7nvNedsnODnspq/gH2dcyETWvH4hVNGNDvRAEutH66kAmxA4/necElqGNs1FggC8w==} + + '@microsoft/api-extractor@7.57.6': + resolution: {integrity: sha512-0rFv/D8Grzw1Mjs2+8NGUR+o4h9LVm5zKRtMeWnpdB5IMJF4TeHCL1zR5LMCIudkOvyvjbhMG5Wjs0B5nqsrRQ==} + hasBin: true + + '@microsoft/tsdoc-config@0.18.1': + resolution: {integrity: sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==} + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + + '@pandacss/is-valid-prop@1.9.0': + resolution: {integrity: sha512-AZvpXWGyjbHc8TC+YVloQ31Z2c4j2xMvYj6UfVxuZdB5w4c9+4N8wy5R7I/XswNh8e4cfUlkvsEGDXjhJRgypw==} + + '@rolldown/pluginutils@1.0.0-rc.2': + resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@rushstack/node-core-library@5.20.3': + resolution: {integrity: sha512-95JgEPq2k7tHxhF9/OJnnyHDXfC9cLhhta0An/6MlkDsX2A6dTzDrTUG18vx4vjc280V0fi0xDH9iQczpSuWsw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/problem-matcher@0.2.1': + resolution: {integrity: sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/rig-package@0.7.2': + resolution: {integrity: sha512-9XbFWuqMYcHUso4mnETfhGVUSaADBRj6HUAAEYk50nMPn8WRICmBuCphycQGNB3duIR6EEZX3Xj3SYc2XiP+9A==} + + '@rushstack/terminal@0.22.3': + resolution: {integrity: sha512-gHC9pIMrUPzAbBiI4VZMU7Q+rsCzb8hJl36lFIulIzoceKotyKL3Rd76AZ2CryCTKEg+0bnTj406HE5YY5OQvw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@5.3.3': + resolution: {integrity: sha512-c+ltdcvC7ym+10lhwR/vWiOhsrm/bP3By2VsFcs5qTKv+6tTmxgbVrtJ5NdNjANiV5TcmOZgUN+5KYQ4llsvEw==} + + '@swc/core-darwin-arm64@1.15.18': + resolution: {integrity: sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.18': + resolution: {integrity: sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.18': + resolution: {integrity: sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.18': + resolution: {integrity: sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.18': + resolution: {integrity: sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.18': + resolution: {integrity: sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.18': + resolution: {integrity: sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.18': + resolution: {integrity: sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.18': + resolution: {integrity: sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.18': + resolution: {integrity: sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.18': + resolution: {integrity: sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.19': + resolution: {integrity: sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==} + + '@swc/types@0.1.25': + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + + '@types/argparse@1.0.38': + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-react-swc@4.2.3': + resolution: {integrity: sha512-QIluDil2prhY1gdA3GGwxZzTAmLdi8cQ2CcuMW4PB/Wu4e/1pzqrwhYWVd09LInCRlDUidQjd0B70QWbjWtLxA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7 + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue/compiler-core@3.5.29': + resolution: {integrity: sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==} + + '@vue/compiler-dom@3.5.29': + resolution: {integrity: sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/language-core@2.2.0': + resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/shared@3.5.29': + resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} + + '@zag-js/accordion@1.35.3': + resolution: {integrity: sha512-wmw6yo5Zr6ShiKGTc5ICEOJCurWAOSGubIpGISiHi3cZ4tlxKF/vpATIUT3eq8xzdB56YK57yKCujs/WmwqqoA==} + + '@zag-js/anatomy@1.35.3': + resolution: {integrity: sha512-oqU9iLNNylrtJMBX5Xu4DsxnPNvtZLiobryv2oNtsDI1mi1Fca/XHghQC9K5aYT0qNsmHj1M3W5WAWTaOtPLkQ==} + + '@zag-js/angle-slider@1.35.3': + resolution: {integrity: sha512-HXRlmsbNEJSBT53fq9XQKL/vwZWwJC3nprskI7s4f/jy8a4uXPTlv7N7zuBYjew+ScTMzZah6fLWzUztBehmSg==} + + '@zag-js/aria-hidden@1.35.3': + resolution: {integrity: sha512-dk5POebn10WneQfLrEgbTzwolaXWpCSHL6F3jCTinW9IbOx7BXghzJD21iU5Iun+y9CorqJPW3p7LplYNUMO5Q==} + + '@zag-js/async-list@1.35.3': + resolution: {integrity: sha512-SXX3wGzLK/maKS1PJ3XfLIGWbu0022f/OhcFsT1PbiHnoFZTH7h2fBhirrCBfy2TYFQ6r5uxgjkhPUNkuaeYnA==} + + '@zag-js/auto-resize@1.35.3': + resolution: {integrity: sha512-ufG8HSqzLd9h5rnos8aumj8iORlRskeR/gbpJu1NHrnHBWIrpuXm6KJJR2oZhTFY1BUMMk8eYIBA2QkVuiJzWA==} + + '@zag-js/avatar@1.35.3': + resolution: {integrity: sha512-lbQ2Q4Va8AAScKULOHw2tCQez+0JRYGHSMFq6i+dJmeT3dlSgRanm69ra6K2po6hM9E4v6pRe+xOVE+9QMDnuA==} + + '@zag-js/carousel@1.35.3': + resolution: {integrity: sha512-F+b8HzUeZfB+xUkAkLG4r0Ubui8pj7pSgZhi26ZiWgsM7tsd7cD+xRMXkvPEITN5Fd5QCe3KlVBuE00w5byjmg==} + + '@zag-js/cascade-select@1.35.3': + resolution: {integrity: sha512-Nifdx77hEuAdXqr1wpZSPjLXqygRhq/WvnPjGhCeSqFPpy62uT4JZ3avyjUZ4I0UhvIpkleUcXtFwQ3cSMh4ww==} + + '@zag-js/checkbox@1.35.3': + resolution: {integrity: sha512-8XBt/Wg2zSQWqV2ZFqZBQUjYRkOYHA2O3IEi0VVYtds3S1n7Pu/HqkZT5qDw+E/SY2+X9Uyx4hO7h2XrlsiZQQ==} + + '@zag-js/clipboard@1.35.3': + resolution: {integrity: sha512-obTwynBpp6c17fLHe5tg//FQ497QsyCEry+K3bTdlrivWW200wvfHxZ6RKVbKwDAwhH+ye0bI1xkYAId8j7sdA==} + + '@zag-js/collapsible@1.35.3': + resolution: {integrity: sha512-IweG8JOBCerJwLO6QzTZGEMlsYUmQfQSeD0jniFguMM8vcunvGVSrM+AaL8pDbmXd+snXokaGyJpGO3vzMW6Fw==} + + '@zag-js/collection@1.35.3': + resolution: {integrity: sha512-BYoWJ4b7ma2PgiuQbRSnP603f2DlK6se5JtViUHTamZScLLLWnWHuQ6zFa1KS5kiIkbb7CFM6/bJ3WNYLch8Ig==} + + '@zag-js/color-picker@1.35.3': + resolution: {integrity: sha512-i9roSgtqeA1b4Q+jWqnxjXB//BQXMP5m1FQ4YcZVq/0yT14A53JIknchuqrh3wC3yPsJMXFqCoKg+NET2+OVig==} + + '@zag-js/color-utils@1.35.3': + resolution: {integrity: sha512-vxkEVgz4YdSbdaPvjiRI1VsJAdwzu/dUNvzqOaiVcPDrHr/FFgmUbv0SOFjnfSb2QWGI8EDEMn02RW9ym+BzGw==} + + '@zag-js/combobox@1.35.3': + resolution: {integrity: sha512-s1qmttTGJTMjlDakL+uvWSEggpafKr1vhOeZCh8j+N4eFt9bLAwaffjuh/1JzWBvzovw7WoMVkizdTXPlN8oYg==} + + '@zag-js/core@1.35.3': + resolution: {integrity: sha512-fGAHyqOYSEFmo52t7wI4dvbFfLyJmUlyf7wknsiUlzUHlrn3yv5PAZYZ2TibpOD1hwXIp4AoCjbiIPPZBxirZw==} + + '@zag-js/date-picker@1.35.3': + resolution: {integrity: sha512-4G10h6pzzLbd84SE2CKtqi6Z9wEBhSyx4GRSxxy3tsf5wAxnz4anRFat9CGwn2YVUYcUJpD+umYgBMPt6zGDnA==} + peerDependencies: + '@internationalized/date': '>=3.0.0' + + '@zag-js/date-utils@1.35.3': + resolution: {integrity: sha512-1co0FPpZ6nO5dN8sZtECkMYaf+3E5zu0KSIJZpZiXb4TgsZMDyHu7K7IsiKFHk9qmhuF6AdPpNxBju91pSXMFg==} + peerDependencies: + '@internationalized/date': '>=3.0.0' + + '@zag-js/dialog@1.35.3': + resolution: {integrity: sha512-byosV+aBHH5LoFKnjEgC7WdqJid7bP9UhgWLSC7+IXbxrif9Czg1YVp6ZlQM6Nx6uD1vnty4touI3P7D7CTKcw==} + + '@zag-js/dismissable@1.35.3': + resolution: {integrity: sha512-XPk+lqmsZp2Z1yMb5K1yj/e7Sobv4D7zK66B1GS97lk9Xzz8vuSgsimcLy0p7RXQl3KL6H5L69inSuQa2exybQ==} + + '@zag-js/dom-query@1.35.3': + resolution: {integrity: sha512-1RbFZoT4CjlHN9TUNse1++ZVOyKo45ktucTIT349o6HMsoWWKmTJDPvFkMBbmu/qY6XXn4dT+LJEp4bL3DR+Qw==} + + '@zag-js/drawer@1.35.3': + resolution: {integrity: sha512-DN5bwa7bDCDaUSbNzFxMc2U/WmbLcXvPSQjyOpKI6CC3VbW2kKaOnjJ5qQG+W5YBO0FpmJBtaxRV7lke4sZH2w==} + + '@zag-js/editable@1.35.3': + resolution: {integrity: sha512-HcjeacS61vQXfNT9IalZj/+oS45yW5bIDO2NjJWV7zNe5AG29NCceUnvBhy+hrUKPnKcjfDocdW5rCL+Lvs/CQ==} + + '@zag-js/file-upload@1.35.3': + resolution: {integrity: sha512-oIYwnDct4ERo2mfmcxsBIJnlmpzjrzYx82SQsXWD3NGKx3cgdh2lwBX+ebItaLH1jkgzBa3z0TWxc6rfvcUXbw==} + + '@zag-js/file-utils@1.35.3': + resolution: {integrity: sha512-Tb05RCzx4swc156hd4jLiO7z+Gxg/HQ+JCds03jgTbrFJAz2D56YaMeI7gSDc1m4Xre3nyqQpSo9AeX5nzbE/w==} + + '@zag-js/floating-panel@1.35.3': + resolution: {integrity: sha512-nTZypcS0X46Oo1kpCQTnP5UlzjhypOAj3B4dq2z/3bAOC0TntYTnFkj8PbEJtExk7364xfMyxfgZOiv7Aqq01w==} + + '@zag-js/focus-trap@1.35.3': + resolution: {integrity: sha512-evErLlGFdDVCI8xipNS5k0rAvO+KFRA9g273bbfWAL1+mT54mcB/XHa85nC3QpPgMNrSh+6LUNq9fapyOGoyYg==} + + '@zag-js/focus-visible@1.35.3': + resolution: {integrity: sha512-g4F8PRGIoFoKBrHiQ1HQh5AjCS7brFRXHvpbDNb9+T11FGlF5Turb+6OVRoNV8MmiuqMltO2I28l36YsGc//uQ==} + + '@zag-js/highlight-word@1.35.3': + resolution: {integrity: sha512-K+mvEBbf3SUFjQeMeJQYb3cjri3x6sPaPhcKWayalelSLB/StWEGqcpmz+a6uUYrCUAK5kEi3Hn0YLGfn0GOig==} + + '@zag-js/hover-card@1.35.3': + resolution: {integrity: sha512-xVoKOtvrnzhYzciZ1csgiV76IQ4DRtx1lsJeFSrfg5MH0kYWeC/pcmm3yCd2+Qh/45J7DbSXeZneqxpyiF5Vvw==} + + '@zag-js/i18n-utils@1.35.3': + resolution: {integrity: sha512-k7UcNxbnC2jvGwCoHYAkFD3ZaRSMQNVHfuy8TujZQ+ci3IJovwgWLveZoRfFbXHkTLfhmbpE2tFXBdpwOVZutg==} + + '@zag-js/image-cropper@1.35.3': + resolution: {integrity: sha512-1PH6bg8JAQESHzNqjka2TJ0QGNBGBAO6rb7AZ+9CaCCLw0pIzbUJhqPMkwd9GhdWGKGP+e7wFitnjcT4W5Js8g==} + + '@zag-js/interact-outside@1.35.3': + resolution: {integrity: sha512-tOcuo/IztzpU7UKXtjVrLZtXzzcbhP4n2WynKwDRkTkq3mRCp61xXJp1csIBycI3JHm/CMeAEcPdRIioxIT/Zw==} + + '@zag-js/json-tree-utils@1.35.3': + resolution: {integrity: sha512-nOv2dPJf+1mxsobYiSlYt96hR1MK7iHKG1iDLoO5wLggS6GQA3ix1BerHJK0zdehoEZ71R45el5ghCG1HB9VzQ==} + + '@zag-js/listbox@1.35.3': + resolution: {integrity: sha512-FE6FOuBr6aWtOb8U8oDvAvcUzD6JKLXAe8WngiLFG+b2yyW4nlaz2AcKRG1bjjB066UMxMo9/+2p4D0Kf5Id1Q==} + + '@zag-js/live-region@1.35.3': + resolution: {integrity: sha512-64rWcfggYpyr2Fn4pdrB/lljMgm3quwn9is+vdDN85Vv3WShKWoz08T4njidm0hwcIbzas0bRqQYWDLLsAoSJQ==} + + '@zag-js/marquee@1.35.3': + resolution: {integrity: sha512-bKZVpmAJWPDORP7WOWnS+65W5ZQBQmRs8zvV33ZfCpFbkXjhRiqKSzIj223/VOc2NEDjyWagz2vioAxrFYVzww==} + + '@zag-js/menu@1.35.3': + resolution: {integrity: sha512-KyY0EZXkIU57Mjt+Lg+pupiePk3LcnQcB3Gl05Vva61bNjBjdKV71qwCQru/OxPZEwYgPo46L7TDIb56kfK/VQ==} + + '@zag-js/navigation-menu@1.35.3': + resolution: {integrity: sha512-8cCHx0X/KjEpr2BaMOxJS5LiA6fs/CNqVTF/sTTgZAv7Dm+MH0yNuKm4kpPvcLaVeBpVE09bnyCHrNKzZes+Fw==} + + '@zag-js/number-input@1.35.3': + resolution: {integrity: sha512-uqawVybAcLcefVEHMVONuAA5kDSDPP5TsROr5PnAyFlhM1iD85+r3KAfCueoDX5w2X4ibbu9o2tdV6zTFKD/nQ==} + + '@zag-js/pagination@1.35.3': + resolution: {integrity: sha512-fKm4s5KAd12RiCI/EDmmGKjPQ+i2qS/UsJPdMe65yb/4mY5OibwV2zyHcVeFsOD4gBZpnU6kYlDAGSttmLWLlQ==} + + '@zag-js/password-input@1.35.3': + resolution: {integrity: sha512-etd0gm6ELAm3y+cFhPU+TYm8khm9cL5Mg5m2DcZxu1Mqpj7JY0LsXZ8SFOdCZgTIHuMEhKBiYfnuyMAd4CJztA==} + + '@zag-js/pin-input@1.35.3': + resolution: {integrity: sha512-ZFt+WIHMdVlSg29BrQLFq5ijabiUO3tXMhoKhjjzTSe/tLqfNeu3UxFB6y/FYpn8+Cvn6xwvhu3lgnORYmI0zQ==} + + '@zag-js/popover@1.35.3': + resolution: {integrity: sha512-+MIEENPsbKPxzoNuDI/C5d5ZN9uxnfZ+MBDc5C5XSgjjg9FcvMXClNq7IFM1aZi24peRXg9cMNf//lApVRT37w==} + + '@zag-js/popper@1.35.3': + resolution: {integrity: sha512-gpB7Xn9WtlfrUsIVbSgNQGDwgNOL/cSGt0Id3wEQKArmqVC704EWtPvXzOMMybBEdm8YW2hQrXuo+o66abI1Sg==} + + '@zag-js/presence@1.35.3': + resolution: {integrity: sha512-ev5E7+U9IZAGvEaflpdVLHaZl8ZaQMhGB3ypd0yKhPwXeM51obV8w3+5HjzTqHPl8TKuoHWL31YaiUBd5EuS6w==} + + '@zag-js/progress@1.35.3': + resolution: {integrity: sha512-u0GxQN1AfXMAgzYOUMxKQA12DyuAP0svh2S//KvOorTSv7d5hAa8nZXi2cEv5abYsyfKJ6/bc1Z56byzW1jVZw==} + + '@zag-js/qr-code@1.35.3': + resolution: {integrity: sha512-t0Ehwogr49vTNtWyNdQU2tYex7uJyfAn7N/5LgD7FXw8aa+RBMWZWlqjCUvHqJ929tVMrn+LIrQnZCcwNunalA==} + + '@zag-js/radio-group@1.35.3': + resolution: {integrity: sha512-kOzocjqWk3dXuRfyfsHwfw63Z99NHbc7rvVUutSsfXANXi+DFYZHuqdPUwMt+29LfaL15XTOfuGV+yUXDCgQHQ==} + + '@zag-js/rating-group@1.35.3': + resolution: {integrity: sha512-BmhJZdbaTnd3nFWMY+nR+HF952UhWXfaXXxiBWptSLMBfAYImQTWBMrLgTHCSnVfmFATj4Gb7xQe79FQU8T5fA==} + + '@zag-js/react@1.35.3': + resolution: {integrity: sha512-x2PxYUCQ6OgOpUdmSkG5tbL9JWVqYRh42r4V2UeAdMh0MRwjAJtxjvAy50DZ8Sfia5o4UGdZMXJyDY2O7Pdhyw==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@zag-js/rect-utils@1.35.3': + resolution: {integrity: sha512-mt/oD3RXdyaX6ZPSd8BO13vvPBJ7QpVWieubE3O0WM3OPhU7ykDMRp/tR7cYMQrzUm04GlY9pbkmSSw2uABxlA==} + + '@zag-js/remove-scroll@1.35.3': + resolution: {integrity: sha512-e59z9SbEpPiw0qwNQa2cB5/h30ZCLREaHsCw1TKTANFhwg7v85k9Lq1H/G/49li1CAjmiaOU9BNGlDvbzpNETQ==} + + '@zag-js/scroll-area@1.35.3': + resolution: {integrity: sha512-IQwdUws/AckRIHK1z/wHdHurnOeGd8h8Dmspfh3VT7NkwTnxeJ4SW9di9smuD+d25eXkJRuX5zGEDHAyx2IaPQ==} + + '@zag-js/scroll-snap@1.35.3': + resolution: {integrity: sha512-NVa2yRm2DQnF6hTV9k7Xz7l8YCZBagZTiqSwNvWKUulKD1csjt2fpBxvUt2cK+1iQnLOey2ydhs7MMsAnXPbJA==} + + '@zag-js/select@1.35.3': + resolution: {integrity: sha512-ztszGHWvlbBDE0YT5LYPH+sMd6VH1ct5pH/M9VSzIUO6C5PARkW0NwSVQ1rCQJMj4sfvSE1gC1/r7urRzqEcUQ==} + + '@zag-js/signature-pad@1.35.3': + resolution: {integrity: sha512-jvtxxzAQ8fre11zWUh6HflG4Ycr5z83Wba4pONRJbUE/vNgkJQ7yJgfyUl1QTlkn8Arfg2Zwoxu9GIq80HLZWg==} + + '@zag-js/slider@1.35.3': + resolution: {integrity: sha512-Th142JO4Fqla5AWhGrTW6CQicwvTw87PdVpur/WotQ7brlZIww5HipzEMh5eQJSWfwpKD4PI2bYK9V/ZE/mpXA==} + + '@zag-js/splitter@1.35.3': + resolution: {integrity: sha512-IsIbRwzjr5amGANEDsZDSToaSn8wHUWvS2l0XHmf3BiiguVApaZgQTlfqthVQC9hBHMOaGIXIW1CFUOrQYkvUQ==} + + '@zag-js/steps@1.35.3': + resolution: {integrity: sha512-TYIrqV+v9/ULhvrTRBtQFFvJQPPTWOmjFXxlIxDwozek5R4dCIyeUYt1/ChJEc2mNETocbfDVSTxRO1dwCFpwQ==} + + '@zag-js/store@1.35.3': + resolution: {integrity: sha512-7kEV4T/20DU36UIfVMzuDlLhWSSEy/vabmpiB700tcdD9BBBODTiSg3ZeljW17dQbvE545vZOFEjVf/cQ5LVGA==} + + '@zag-js/switch@1.35.3': + resolution: {integrity: sha512-EP/2cJ46sd+6C5x5+89jn/9NOpM05CRESYB4RMhOnTe/WFtcS4IpiYtVHFhikdXkvJoibm67O2EHep2Pm/Xj4w==} + + '@zag-js/tabs@1.35.3': + resolution: {integrity: sha512-lZKlDmxE25miCikj9QZCCnL02SVV2K14KZy5bn7+XDgrWlfSNTpNTj8r5E3zGlSgio5pkTGou57ASqS7WaPDWg==} + + '@zag-js/tags-input@1.35.3': + resolution: {integrity: sha512-HqyoQ3DZFhByOGnDShFfxi6u0bIf7aSVTlwmAvcL+b2ZhyU6/wIMGc4WJE7BMx1NYWM/jNLHedvGExAI8R0kXQ==} + + '@zag-js/timer@1.35.3': + resolution: {integrity: sha512-edmgitbRgsq+msxvVB4wc17Q5d5k63zMWaLJnWjUdDGAgEtM6/HNxwGb3riv46S2U3RgYxaaHTNZ/M7EE5mvYw==} + + '@zag-js/toast@1.35.3': + resolution: {integrity: sha512-whlR791GHdnMD21nNPsl2Dbql8+qu1wBZl75QzwYrjR8FlKjp8bhr3gXKzQEddcBXe9GPEFGvUs4iCyXsuTbpg==} + + '@zag-js/toggle-group@1.35.3': + resolution: {integrity: sha512-Gn6JHzkQ4tlttjZcE0ZjIdxYkFeVp9VHrcMVizjJTkGZRmQ+kPZ5G/wOsZhIrvLX3Dw6Y0NkuBcP+jDHz/o3TA==} + + '@zag-js/toggle@1.35.3': + resolution: {integrity: sha512-aFfHKuR4sKzglhkmWLA+0RTNPs9dfeqwtc96qljawGYfAYWJXkEPYK9dFfVa+arZ7L84xBi24QSLiTg7LGSFLw==} + + '@zag-js/tooltip@1.35.3': + resolution: {integrity: sha512-/pImDGYl79MfLdvEphj3rSvNdj2tLW4GwGEncgdLM/GKwQiEUjfi/9EJOfLYP23M4lOOnoW7orehJ9xeaXOAkA==} + + '@zag-js/tour@1.35.3': + resolution: {integrity: sha512-DI2aCXmZaE9KcPZDs9itc2BO7ixLApJ/yVRfM69pXwVOrucdSeDDNPFkfbhj5XwB+9VjjZEkqWFHKntRIyPl5g==} + + '@zag-js/tree-view@1.35.3': + resolution: {integrity: sha512-DbHaLxSNa1goE3o3IsXxEdzp8P5dvmkk1rVWgNUUIhpA+44idEjSSNXJkHPl18Mk5blqSMVjK1EX91oqai01Vw==} + + '@zag-js/types@1.35.3': + resolution: {integrity: sha512-Fnm3AMs1lfb55hlkip/eJeWHOjFB3gSi1JkZlkkdltG2l7y/zsHkumPSe6jIKy+DRRIFKRCyXVTatbPN27bO3w==} + + '@zag-js/utils@1.35.3': + resolution: {integrity: sha512-LHcC+9y6TFhDsIz9I3koYxONl2JFfx5yQDzc6ZEQO2cqzXedRcN0R9IPqNGCX7JuhGt14ctDkVCm1JWGP2J6Wg==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + alien-signals@0.4.14: + resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.1: + resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-freehand@1.2.3: + resolution: {integrity: sha512-bHZSfqDHGNlPpgH2yxXgPHlQSPpEbo+qg7li0M78J9vNAi2yjwLeA4x79BEQhX44lEWpCLSFCeRZwpw0niiXPA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + proxy-compare@3.0.1: + resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==} + + proxy-memoize@3.0.1: + resolution: {integrity: sha512-VDdG/VYtOgdGkWJx7y0o7p+zArSf2383Isci8C+BP3YXgMYDoPd3cCBjw0JdWb6YBb9sFiOPbAADDVTPJnh+9g==} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + uqr@0.1.2: + resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite-plugin-css-injected-by-js@3.5.2: + resolution: {integrity: sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==} + peerDependencies: + vite: '>2.0.0-0' + + vite-plugin-dts@4.5.4: + resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} + peerDependencies: + typescript: '*' + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@ark-ui/react@5.34.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@internationalized/date': 3.11.0 + '@zag-js/accordion': 1.35.3 + '@zag-js/anatomy': 1.35.3 + '@zag-js/angle-slider': 1.35.3 + '@zag-js/async-list': 1.35.3 + '@zag-js/auto-resize': 1.35.3 + '@zag-js/avatar': 1.35.3 + '@zag-js/carousel': 1.35.3 + '@zag-js/cascade-select': 1.35.3 + '@zag-js/checkbox': 1.35.3 + '@zag-js/clipboard': 1.35.3 + '@zag-js/collapsible': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/color-picker': 1.35.3 + '@zag-js/color-utils': 1.35.3 + '@zag-js/combobox': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/date-picker': 1.35.3(@internationalized/date@3.11.0) + '@zag-js/date-utils': 1.35.3(@internationalized/date@3.11.0) + '@zag-js/dialog': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/drawer': 1.35.3 + '@zag-js/editable': 1.35.3 + '@zag-js/file-upload': 1.35.3 + '@zag-js/file-utils': 1.35.3 + '@zag-js/floating-panel': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/highlight-word': 1.35.3 + '@zag-js/hover-card': 1.35.3 + '@zag-js/i18n-utils': 1.35.3 + '@zag-js/image-cropper': 1.35.3 + '@zag-js/json-tree-utils': 1.35.3 + '@zag-js/listbox': 1.35.3 + '@zag-js/marquee': 1.35.3 + '@zag-js/menu': 1.35.3 + '@zag-js/navigation-menu': 1.35.3 + '@zag-js/number-input': 1.35.3 + '@zag-js/pagination': 1.35.3 + '@zag-js/password-input': 1.35.3 + '@zag-js/pin-input': 1.35.3 + '@zag-js/popover': 1.35.3 + '@zag-js/presence': 1.35.3 + '@zag-js/progress': 1.35.3 + '@zag-js/qr-code': 1.35.3 + '@zag-js/radio-group': 1.35.3 + '@zag-js/rating-group': 1.35.3 + '@zag-js/react': 1.35.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@zag-js/scroll-area': 1.35.3 + '@zag-js/select': 1.35.3 + '@zag-js/signature-pad': 1.35.3 + '@zag-js/slider': 1.35.3 + '@zag-js/splitter': 1.35.3 + '@zag-js/steps': 1.35.3 + '@zag-js/switch': 1.35.3 + '@zag-js/tabs': 1.35.3 + '@zag-js/tags-input': 1.35.3 + '@zag-js/timer': 1.35.3 + '@zag-js/toast': 1.35.3 + '@zag-js/toggle': 1.35.3 + '@zag-js/toggle-group': 1.35.3 + '@zag-js/tooltip': 1.35.3 + '@zag-js/tour': 1.35.3 + '@zag-js/tree-view': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/runtime@7.28.6': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@chakra-ui/react@3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@ark-ui/react': 5.34.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@emotion/is-prop-valid': 1.4.0 + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.4) + '@emotion/utils': 1.4.2 + '@pandacss/is-valid-prop': 1.9.0 + csstype: 3.2.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/runtime': 7.28.6 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@1.4.0': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.4) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@internationalized/date@3.11.0': + dependencies: + '@swc/helpers': 0.5.19 + + '@internationalized/number@3.6.5': + dependencies: + '@swc/helpers': 0.5.19 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@microsoft/api-extractor-model@7.33.4': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@rushstack/node-core-library': 5.20.3 + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor@7.57.6': + dependencies: + '@microsoft/api-extractor-model': 7.33.4 + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@rushstack/node-core-library': 5.20.3 + '@rushstack/rig-package': 0.7.2 + '@rushstack/terminal': 0.22.3 + '@rushstack/ts-command-line': 5.3.3 + diff: 8.0.3 + lodash: 4.17.23 + minimatch: 10.2.4 + resolve: 1.22.11 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + + '@microsoft/tsdoc-config@0.18.1': + dependencies: + '@microsoft/tsdoc': 0.16.0 + ajv: 8.18.0 + jju: 1.4.0 + resolve: 1.22.11 + + '@microsoft/tsdoc@0.16.0': {} + + '@pandacss/is-valid-prop@1.9.0': {} + + '@rolldown/pluginutils@1.0.0-rc.2': {} + + '@rollup/pluginutils@5.3.0(rollup@4.59.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.59.0 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@rushstack/node-core-library@5.20.3': + dependencies: + ajv: 8.18.0 + ajv-draft-04: 1.0.0(ajv@8.18.0) + ajv-formats: 3.0.1(ajv@8.18.0) + fs-extra: 11.3.4 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.11 + semver: 7.5.4 + + '@rushstack/problem-matcher@0.2.1': {} + + '@rushstack/rig-package@0.7.2': + dependencies: + resolve: 1.22.11 + strip-json-comments: 3.1.1 + + '@rushstack/terminal@0.22.3': + dependencies: + '@rushstack/node-core-library': 5.20.3 + '@rushstack/problem-matcher': 0.2.1 + supports-color: 8.1.1 + + '@rushstack/ts-command-line@5.3.3': + dependencies: + '@rushstack/terminal': 0.22.3 + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + + '@swc/core-darwin-arm64@1.15.18': + optional: true + + '@swc/core-darwin-x64@1.15.18': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.18': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.18': + optional: true + + '@swc/core-linux-arm64-musl@1.15.18': + optional: true + + '@swc/core-linux-x64-gnu@1.15.18': + optional: true + + '@swc/core-linux-x64-musl@1.15.18': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.18': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.18': + optional: true + + '@swc/core-win32-x64-msvc@1.15.18': + optional: true + + '@swc/core@1.15.18(@swc/helpers@0.5.19)': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.25 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.18 + '@swc/core-darwin-x64': 1.15.18 + '@swc/core-linux-arm-gnueabihf': 1.15.18 + '@swc/core-linux-arm64-gnu': 1.15.18 + '@swc/core-linux-arm64-musl': 1.15.18 + '@swc/core-linux-x64-gnu': 1.15.18 + '@swc/core-linux-x64-musl': 1.15.18 + '@swc/core-win32-arm64-msvc': 1.15.18 + '@swc/core-win32-ia32-msvc': 1.15.18 + '@swc/core-win32-x64-msvc': 1.15.18 + '@swc/helpers': 0.5.19 + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.19': + dependencies: + tslib: 2.8.1 + + '@swc/types@0.1.25': + dependencies: + '@swc/counter': 0.1.3 + + '@types/argparse@1.0.38': {} + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/parse-json@4.0.2': {} + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-react-swc@4.2.3(@swc/helpers@0.5.19)(vite@7.3.1)': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.2 + '@swc/core': 1.15.18(@swc/helpers@0.5.19) + vite: 7.3.1 + transitivePeerDependencies: + - '@swc/helpers' + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/shared': 3.5.29 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.29': + dependencies: + '@vue/compiler-core': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/language-core@2.2.0(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.29 + alien-signals: 0.4.14 + minimatch: 9.0.9 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + + '@vue/shared@3.5.29': {} + + '@zag-js/accordion@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/anatomy@1.35.3': {} + + '@zag-js/angle-slider@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/aria-hidden@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 + + '@zag-js/async-list@1.35.3': + dependencies: + '@zag-js/core': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/auto-resize@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 + + '@zag-js/avatar@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/carousel@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/scroll-snap': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/cascade-select@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/checkbox@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/clipboard@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/collapsible@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/collection@1.35.3': + dependencies: + '@zag-js/utils': 1.35.3 + + '@zag-js/color-picker@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/color-utils': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/color-utils@1.35.3': + dependencies: + '@zag-js/utils': 1.35.3 + + '@zag-js/combobox@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/core@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/date-picker@1.35.3(@internationalized/date@3.11.0)': + dependencies: + '@internationalized/date': 3.11.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/date-utils': 1.35.3(@internationalized/date@3.11.0) + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/live-region': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/date-utils@1.35.3(@internationalized/date@3.11.0)': + dependencies: + '@internationalized/date': 3.11.0 + + '@zag-js/dialog@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/remove-scroll': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/dismissable@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/dom-query@1.35.3': + dependencies: + '@zag-js/types': 1.35.3 + + '@zag-js/drawer@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/remove-scroll': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/editable@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/file-upload@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/file-utils': 1.35.3 + '@zag-js/i18n-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/file-utils@1.35.3': + dependencies: + '@zag-js/i18n-utils': 1.35.3 + + '@zag-js/floating-panel@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/store': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/focus-trap@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 + + '@zag-js/focus-visible@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 + + '@zag-js/highlight-word@1.35.3': {} + + '@zag-js/hover-card@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/i18n-utils@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 + + '@zag-js/image-cropper@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/interact-outside@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/json-tree-utils@1.35.3': {} + + '@zag-js/listbox@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/live-region@1.35.3': {} + + '@zag-js/marquee@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/menu@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/navigation-menu@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/number-input@1.35.3': + dependencies: + '@internationalized/number': 3.6.5 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/pagination@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/password-input@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/pin-input@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/popover@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/remove-scroll': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/popper@1.35.3': + dependencies: + '@floating-ui/dom': 1.7.6 + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/presence@1.35.3': + dependencies: + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + + '@zag-js/progress@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/qr-code@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + proxy-memoize: 3.0.1 + uqr: 0.1.2 + + '@zag-js/radio-group@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/rating-group@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/react@1.35.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@zag-js/core': 1.35.3 + '@zag-js/store': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@zag-js/rect-utils@1.35.3': {} + + '@zag-js/remove-scroll@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 + + '@zag-js/scroll-area@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/scroll-snap@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 + + '@zag-js/select@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/signature-pad@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + perfect-freehand: 1.2.3 + + '@zag-js/slider@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/splitter@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/steps@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/store@1.35.3': + dependencies: + proxy-compare: 3.0.1 + + '@zag-js/switch@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tabs@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tags-input@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/auto-resize': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/live-region': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/timer@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/toast@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/toggle-group@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/toggle@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tooltip@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tour@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tree-view@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/types@1.35.3': + dependencies: + csstype: 3.2.3 + + '@zag-js/utils@1.35.3': {} + + acorn@8.16.0: {} + + ajv-draft-04@1.0.0(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alien-signals@0.4.14: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.28.6 + cosmiconfig: 7.1.0 + resolve: 1.22.11 + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + callsites@3.1.0: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + comma-separated-tokens@2.0.3: {} + + compare-versions@6.1.1: {} + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + convert-source-map@1.9.0: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + csstype@3.2.3: {} + + de-indent@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@8.0.3: {} + + entities@7.0.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + estree-util-is-identifier-name@3.0.0: {} + + estree-walker@2.0.2: {} + + exsolve@1.0.8: {} + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + find-root@1.1.0: {} + + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + he@1.2.0: {} + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + html-url-attributes@3.0.1: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-lazy@4.0.0: {} + + inline-style-parser@0.2.7: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-arrayish@0.2.1: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-decimal@2.0.1: {} + + is-hexadecimal@2.0.1: {} + + is-plain-obj@4.1.0: {} + + jju@1.4.0: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + kolorist@1.8.0: {} + + lines-and-columns@1.2.4: {} + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.1 + pkg-types: 2.3.0 + quansync: 0.2.11 + + lodash@4.17.23: {} + + longest-streak@3.1.0: {} + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + mlly@1.8.1: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-browserify@1.0.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + perfect-freehand@1.2.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.1 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + property-information@7.1.0: {} + + proxy-compare@3.0.1: {} + + proxy-memoize@3.0.1: + dependencies: + proxy-compare: 3.0.1 + + quansync@0.2.11: {} + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.14 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.4 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react@19.2.4: {} + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + scheduler@0.27.0: {} + + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + + source-map-js@1.2.1: {} + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + space-separated-tokens@2.0.2: {} + + sprintf-js@1.0.3: {} + + string-argv@0.3.2: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-json-comments@3.1.1: {} + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + stylis@4.2.0: {} + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + tslib@2.8.1: {} + + typescript@5.8.2: {} + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + universalify@2.0.1: {} + + uqr@0.1.2: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite-plugin-css-injected-by-js@3.5.2(vite@7.3.1): + dependencies: + vite: 7.3.1 + + vite-plugin-dts@4.5.4(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1): + dependencies: + '@microsoft/api-extractor': 7.57.6 + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + '@volar/typescript': 2.4.28 + '@vue/language-core': 2.2.0(typescript@5.9.3) + compare-versions: 6.1.1 + debug: 4.4.3 + kolorist: 1.8.0 + local-pkg: 1.1.2 + magic-string: 0.30.21 + typescript: 5.9.3 + optionalDependencies: + vite: 7.3.1 + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + + vite@7.3.1: + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + + vscode-uri@3.1.0: {} + + yallist@4.0.0: {} + + yaml@1.10.2: {} + + zwitch@2.0.4: {} diff --git a/providers/common/ai/src/airflow/providers/common/ai/plugins/www/src/api.ts b/providers/common/ai/src/airflow/providers/common/ai/plugins/www/src/api.ts new file mode 100644 index 0000000000000..bc653e88d0c11 --- /dev/null +++ b/providers/common/ai/src/airflow/providers/common/ai/plugins/www/src/api.ts @@ -0,0 +1,93 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { SessionResponse } from "src/types/feedback"; + +function getBase(): string { + if (typeof document === "undefined") return "/hitl-review"; + const baseHref = document.querySelector("head > base")?.getAttribute("href") ?? ""; + const baseUrl = new URL(baseHref, globalThis.location.origin); + const basePath = baseUrl.pathname.replace(/\/$/, "") || ""; + return basePath ? `${basePath}/hitl-review` : "/hitl-review"; +} + +const BASE = getBase(); + +function buildQs(dagId: string, runId: string, taskId: string, mapIndex: number): string { + return ( + `dag_id=${encodeURIComponent(dagId)}` + + `&run_id=${encodeURIComponent(runId)}` + + `&task_id=${encodeURIComponent(taskId)}` + + `&map_index=${mapIndex}` + ); +} + +export class ApiError extends Error { + taskActive?: boolean; + + constructor(message: string, taskActive?: boolean) { + super(message); + this.taskActive = taskActive; + } +} + +async function apiFetch( + path: string, + qs: string, + init?: RequestInit, +): Promise { + const sep = path.includes("?") ? "&" : "?"; + const res = await fetch(`${BASE}${path}${sep}${qs}`, init); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + const detail = (body as { detail?: string | { message?: string; task_active?: boolean } }).detail; + let message: string; + let taskActive: boolean | undefined; + if (typeof detail === "object" && detail !== null) { + message = detail.message ?? res.statusText; + taskActive = detail.task_active; + } else { + message = detail ?? res.statusText; + } + throw new ApiError(message, taskActive); + } + return res.json() as Promise; +} + +export function createApi(dagId: string, runId: string, taskId: string, mapIndex: number) { + const qs = buildQs(dagId, runId, taskId, mapIndex); + + return { + fetchSession: () => + apiFetch("/sessions/find", qs), + + submitFeedback: (feedback: string) => + apiFetch("/sessions/feedback", qs, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ feedback }), + }), + + approve: () => + apiFetch("/sessions/approve", qs, { method: "POST" }), + + reject: () => + apiFetch("/sessions/reject", qs, { method: "POST" }), + }; +} diff --git a/providers/common/ai/src/airflow/providers/common/ai/plugins/www/src/components/ChatPage.tsx b/providers/common/ai/src/airflow/providers/common/ai/plugins/www/src/components/ChatPage.tsx new file mode 100644 index 0000000000000..6094cc9be4cd2 --- /dev/null +++ b/providers/common/ai/src/airflow/providers/common/ai/plugins/www/src/components/ChatPage.tsx @@ -0,0 +1,351 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + Badge, + Box, + Button, + Flex, + HStack, + Heading, + Spinner, + Text, + Textarea, + VStack, +} from "@chakra-ui/react"; +import { + type FC, + type KeyboardEvent, + useCallback, + useEffect, + useRef, + useState, +} from "react"; + +import { MessageBubble } from "src/components/MessageBubble"; +import { NoSession } from "src/components/NoSession"; +import { useSession } from "src/hooks/useSession"; +import { isTerminalStatus } from "src/types/feedback"; +import { toaster } from "src/toaster"; + +interface ChatPageProps { + dagId: string; + runId: string; + taskId: string; + mapIndex: number; +} + +type ConfirmAction = "approve" | "reject" | null; + +const STATUS_BADGE: Record< + string, + { colorPalette: "green" | "red" | "yellow" | "blue"; label: string } +> = { + pending_review: { colorPalette: "yellow", label: "Pending Review" }, + approved: { colorPalette: "green", label: "Approved" }, + rejected: { colorPalette: "red", label: "Rejected" }, + changes_requested: { colorPalette: "blue", label: "Regenerating..." }, + max_iterations_exceeded: { colorPalette: "red", label: "Max iterations exceeded" }, + timeout_exceeded: { colorPalette: "red", label: "Timeout exceeded" }, +}; + +export const ChatPage: FC = ({ dagId, runId, taskId, mapIndex }) => { + const { session, loading, error, sendFeedback, approve, reject } = useSession( + dagId, + runId, + taskId, + mapIndex, + ); + + const [feedbackText, setFeedbackText] = useState(""); + const [confirmAction, setConfirmAction] = useState(null); + const [isSending, setIsSending] = useState(false); + const chatRef = useRef(null); + const textareaRef = useRef(null); + const prevConvLenRef = useRef(0); + + useEffect(() => { + if (!session?.conversation || !chatRef.current) return; + const len = session.conversation.length; + if (len > prevConvLenRef.current) { + chatRef.current.scrollTop = chatRef.current.scrollHeight; + } + prevConvLenRef.current = len; + }, [session?.conversation]); + + const autoResize = useCallback(() => { + const ta = textareaRef.current; + if (ta) { + ta.style.height = "auto"; + ta.style.height = `${ta.scrollHeight}px`; + } + }, []); + + const handleSend = useCallback(async () => { + const text = feedbackText.trim(); + if (!text || isSending) return; + setIsSending(true); + try { + await sendFeedback(text); + setFeedbackText(""); + toaster.create({ title: "Feedback sent", type: "success", duration: 4000 }); + } catch (err) { + toaster.create({ + title: err instanceof Error ? err.message : "Error", + type: "error", + duration: 5000, + }); + } finally { + setIsSending(false); + } + }, [feedbackText, sendFeedback, isSending]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.ctrlKey && e.key === "Enter") { + void handleSend(); + } + }, + [handleSend], + ); + + const execConfirm = useCallback(async () => { + const action = confirmAction; + setConfirmAction(null); + if (!action || isSending) return; + setIsSending(true); + try { + if (action === "approve") { + await approve(); + toaster.create({ title: "Approved", type: "success", duration: 4000 }); + } else if (action === "reject") { + await reject(); + toaster.create({ title: "Rejected", type: "success", duration: 4000 }); + } + } catch (err) { + toaster.create({ + title: err instanceof Error ? err.message : "Error", + type: "error", + duration: 5000, + }); + } finally { + setIsSending(false); + } + }, [confirmAction, approve, reject, isSending]); + + if (loading) { + return ( + + + + Connecting to session + + Looking up the HITL review session for this task... + + + + ); + } + + if (!session) { + return ; + } + + const isTerminal = isTerminalStatus(session); + const canAct = session.status === "pending_review" && !session.task_completed; + const badge = STATUS_BADGE[session.status] ?? STATUS_BADGE["pending_review"]; + + return ( + + + HITL Review + + + Task: {session.task_id} + + + DAG: {session.dag_id} + + + Iteration: {session.iteration}/{session.max_iterations} + + + {badge.label} + + + + + {error && ( + + {error} + + )} + + + {session.conversation.map((entry, idx) => ( + + ))} + {session.status === "pending_review" && !isTerminal && ( + + Waiting for your review + + )} + + + {isTerminal && ( + + + {session.status === "approved" + ? "Output Approved" + : session.status === "rejected" + ? "Output Rejected" + : session.status === "max_iterations_exceeded" + ? "Max iterations exceeded" + : session.status === "timeout_exceeded" + ? "Timeout exceeded" + : "Task Completed — Session Ended"} + + + )} + + {!isTerminal && ( + + {confirmAction === null ? ( + + +