From 8581546518d38dd6213e9ac4e2df63412674df42 Mon Sep 17 00:00:00 2001 From: Pyasma Date: Sun, 8 Mar 2026 18:43:05 +0530 Subject: [PATCH 001/280] connectionspage.ts changes --- .../ui/tests/e2e/pages/ConnectionsPage.ts | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) 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..3e5ee7fb6cc64 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,17 @@ 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.getByRole("columnheader", { name: "Connection ID" }).first(); + this.connectionTypeHeader = page.getByRole("columnheader", { name: "Connection Type" }).first(); + this.hostHeader = page.getByRole("columnheader", { name: "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 @@ -160,18 +164,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 +251,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 +301,10 @@ export class ConnectionsPage extends BasePage { await expect .poll( async () => { - const count1 = 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(); + const count = await this.page.locator("tbody tr").count(); - if (count1 === count2 && count1 > 0) { - stableRowCount = count1; + if (count > 0) { + stableRowCount = count; return true; } @@ -362,6 +351,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); @@ -407,7 +401,7 @@ export class ConnectionsPage extends BasePage { // Get count twice to ensure it's stable const count1 = ids.length; - await this.page.evaluate(() => new Promise((r) => setTimeout(r, 200))); + await this.page.waitForTimeout(200); const count2 = await this.getConnectionIds().then((allIds) => allIds.length); // Stable when count doesn't change @@ -496,7 +490,7 @@ export class ConnectionsPage extends BasePage { if (count1 === 0) return true; - await this.page.evaluate(() => new Promise((r) => setTimeout(r, 300))); + await this.page.waitForTimeout(300); const count2 = await this.page.locator("tbody tr").count(); return count1 === count2; From f326075ac8bf6d0ee26b4f9d95db9b56f7e57911 Mon Sep 17 00:00:00 2001 From: Pyasma Date: Sun, 8 Mar 2026 18:46:08 +0530 Subject: [PATCH 002/280] connections.spec.ts changes --- .../ui/tests/e2e/specs/connections.spec.ts | 74 ++++++------------- 1 file changed, 21 insertions(+), 53 deletions(-) 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(); From 1a3a12372ae69e2134b96312efe2c6b3dc4ab2fd Mon Sep 17 00:00:00 2001 From: Pyasma Date: Tue, 10 Mar 2026 17:02:08 +0530 Subject: [PATCH 003/280] refining connection table header locators, adding a dialog backdrop wait before editing, and extending search input visibility timeout. --- .../ui/tests/e2e/pages/ConnectionsPage.ts | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) 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 3e5ee7fb6cc64..746842f6da226 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts @@ -98,9 +98,15 @@ export class ConnectionsPage extends BasePage { // Sorting and filtering this.tableHeader = page.locator('[role="columnheader"]').first(); - this.connectionIdHeader = page.getByRole("columnheader", { name: "Connection ID" }).first(); - this.connectionTypeHeader = page.getByRole("columnheader", { name: "Connection Type" }).first(); - this.hostHeader = page.getByRole("columnheader", { name: "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"); @@ -117,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) { @@ -132,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!") @@ -432,7 +446,7 @@ export class ConnectionsPage extends BasePage { 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); From f4f023a4eb8b597744cfedf24f4654110c41e344 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:22:16 +0100 Subject: [PATCH 004/280] chore(deps): bump pnpm/action-setup from 4.0.0 to 4.2.0 (#63066) Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 4.0.0 to 4.2.0. - [Release notes](https://github.com/pnpm/action-setup/releases) - [Commits](https://github.com/pnpm/action-setup/compare/fe02b34f77f8bc703788d5817da081398fad5dd2...41ff72655975bd51cab0327fa583b6e92b6d3061) --- updated-dependencies: - dependency-name: pnpm/action-setup dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/basic-tests.yml | 2 +- .github/workflows/registry-build.yml | 2 +- .github/workflows/ui-e2e-tests.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/basic-tests.yml b/.github/workflows/basic-tests.yml index d197df3e74612..a51fbfdcdf7b5 100644 --- a/.github/workflows/basic-tests.yml +++ b/.github/workflows/basic-tests.yml @@ -159,7 +159,7 @@ jobs: 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 diff --git a/.github/workflows/registry-build.yml b/.github/workflows/registry-build.yml index 1c8b6d367b5cc..c5e18c92ef1ab 100644 --- a/.github/workflows/registry-build.yml +++ b/.github/workflows/registry-build.yml @@ -206,7 +206,7 @@ jobs: fi - name: "Setup pnpm" - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 9 diff --git a/.github/workflows/ui-e2e-tests.yml b/.github/workflows/ui-e2e-tests.yml index 7f31fb3efaad0..e51eb95d0b661 100644 --- a/.github/workflows/ui-e2e-tests.yml +++ b/.github/workflows/ui-e2e-tests.yml @@ -121,7 +121,7 @@ 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 From 57b5a4788428768224a41e68d4abef665e3dce43 Mon Sep 17 00:00:00 2001 From: Dheeraj Turaga Date: Sun, 8 Mar 2026 09:26:34 -0500 Subject: [PATCH 005/280] docs(edge3): add set-worker-concurrency command to deployment guide (#63083) --- providers/edge3/docs/deployment.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/providers/edge3/docs/deployment.rst b/providers/edge3/docs/deployment.rst index 7076b8239239c..6c78033c0a463 100644 --- a/providers/edge3/docs/deployment.rst +++ b/providers/edge3/docs/deployment.rst @@ -244,3 +244,4 @@ instance. The commands are: - ``airflow edge remove-remote-edge-worker``: Remove a worker instance from the cluster - ``airflow edge add-worker-queues``: Add queues to an edge worker - ``airflow edge remove-worker-queues``: Remove queues from an edge worker +- ``airflow edge set-worker-concurrency``: Set the concurrency of a running remote edge worker From fba748a0ba09239e93a8e39d362931854e891e37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:30:37 +0100 Subject: [PATCH 006/280] chore(deps): bump aws-actions/configure-aws-credentials (#63093) Bumps [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) from 4.0.1 to 6.0.0. - [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases) - [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md) - [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/010d0da01d0b5a38af31e9c3470dbfdabdecca3a...8df5847569e6427dd6c4fb1cf565c83acfa8afa7) --- updated-dependencies: - dependency-name: aws-actions/configure-aws-credentials dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-image-checks.yml | 2 +- .github/workflows/publish-docs-to-s3.yml | 2 +- .github/workflows/registry-build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-image-checks.yml b/.github/workflows/ci-image-checks.yml index b24995e032c65..585ac16a5b88f 100644 --- a/.github/workflows/ci-image-checks.yml +++ b/.github/workflows/ci-image-checks.yml @@ -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 }} diff --git a/.github/workflows/publish-docs-to-s3.yml b/.github/workflows/publish-docs-to-s3.yml index 56e9f7e21b70b..963181cd25622 100644 --- a/.github/workflows/publish-docs-to-s3.yml +++ b/.github/workflows/publish-docs-to-s3.yml @@ -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/registry-build.yml b/.github/workflows/registry-build.yml index c5e18c92ef1ab..f75f5e935fe91 100644 --- a/.github/workflows/registry-build.yml +++ b/.github/workflows/registry-build.yml @@ -126,7 +126,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 }} From a87aea90c7f46cf336462c9db5c839f516586ace Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:31:10 +0100 Subject: [PATCH 007/280] chore(deps): bump actions/setup-java from 4.7.1 to 5.2.0 (#63095) Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.7.1 to 5.2.0. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/c5195efecf7bdfc987ee8bae7a71cb8b11521c00...be666c2fcd27ec809703dec50e508c2fdc7f6654) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: 5.2.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/basic-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/basic-tests.yml b/.github/workflows/basic-tests.yml index a51fbfdcdf7b5..1eba352225001 100644 --- a/.github/workflows/basic-tests.yml +++ b/.github/workflows/basic-tests.yml @@ -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' From 56f060e7bf2cf2017978be748b45caaece73a4fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:13:01 +0100 Subject: [PATCH 008/280] chore(deps): bump the edge-ui-package-updates group across 1 directory with 6 updates (#63070) Bumps the edge-ui-package-updates group with 6 updates in the /providers/edge3/src/airflow/providers/edge3/plugins/www directory: | Package | From | To | | --- | --- | --- | | [@chakra-ui/react](https://github.com/chakra-ui/chakra-ui/tree/HEAD/packages/react) | `3.33.0` | `3.34.0` | | [react-icons](https://github.com/react-icons/react-icons) | `5.5.0` | `5.6.0` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.55.0` | `8.56.1` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.55.0` | `8.56.1` | | [happy-dom](https://github.com/capricorn86/happy-dom) | `20.7.0` | `20.8.3` | | [vite-plugin-css-injected-by-js](https://github.com/marco-prontera/vite-plugin-css-injected-by-js) | `3.5.2` | `4.0.1` | Updates `@chakra-ui/react` from 3.33.0 to 3.34.0 - [Release notes](https://github.com/chakra-ui/chakra-ui/releases) - [Changelog](https://github.com/chakra-ui/chakra-ui/blob/main/packages/react/CHANGELOG.md) - [Commits](https://github.com/chakra-ui/chakra-ui/commits/@chakra-ui/react@3.34.0/packages/react) Updates `react-icons` from 5.5.0 to 5.6.0 - [Release notes](https://github.com/react-icons/react-icons/releases) - [Commits](https://github.com/react-icons/react-icons/compare/v5.5.0...v5.6.0) Updates `@typescript-eslint/eslint-plugin` from 8.55.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.55.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/parser) Updates `happy-dom` from 20.7.0 to 20.8.3 - [Release notes](https://github.com/capricorn86/happy-dom/releases) - [Commits](https://github.com/capricorn86/happy-dom/compare/v20.7.0...v20.8.3) Updates `vite-plugin-css-injected-by-js` from 3.5.2 to 4.0.1 - [Release notes](https://github.com/marco-prontera/vite-plugin-css-injected-by-js/releases) - [Commits](https://github.com/marco-prontera/vite-plugin-css-injected-by-js/compare/v3.5.2...v4.0.1) --- updated-dependencies: - dependency-name: "@chakra-ui/react" dependency-version: 3.34.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: edge-ui-package-updates - dependency-name: react-icons dependency-version: 5.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: edge-ui-package-updates - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: edge-ui-package-updates - dependency-name: "@typescript-eslint/parser" dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: edge-ui-package-updates - dependency-name: happy-dom dependency-version: 20.8.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: edge-ui-package-updates - dependency-name: vite-plugin-css-injected-by-js dependency-version: 4.0.1 dependency-type: direct:development update-type: version-update:semver-major dependency-group: edge-ui-package-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../providers/edge3/plugins/www/package.json | 12 +- .../edge3/plugins/www/pnpm-lock.yaml | 1565 ++++++++--------- 2 files changed, 705 insertions(+), 872 deletions(-) diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/package.json b/providers/edge3/src/airflow/providers/edge3/plugins/www/package.json index 03c19141b1e35..65da58e2d6671 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/package.json +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/package.json @@ -32,7 +32,7 @@ "coverage": "vitest run --coverage" }, "dependencies": { - "@chakra-ui/react": "^3.33.0", + "@chakra-ui/react": "^3.34.0", "@emotion/react": "^11.14.0", "@tanstack/react-query": "^5.90.21", "@types/semver": "^7.7.1", @@ -41,7 +41,7 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-hotkeys-hook": "^5.2.4", - "react-icons": "^5.5.0", + "react-icons": "^5.6.0", "react-router-dom": "^7.13.1", "react-timeago": "^8.3.0", "semver": "^7.7.4", @@ -58,8 +58,8 @@ "@types/node": "^25.3.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "8.55.0", - "@typescript-eslint/parser": "8.55.0", + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/utils": "^8.56.1", "@vitejs/plugin-react-swc": "^4.2.3", "@vitest/coverage-v8": "^4.0.18", @@ -73,12 +73,12 @@ "eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-unicorn": "^63.0.0", "globals": "^17.4.0", - "happy-dom": "^20.7.0", + "happy-dom": "^20.8.3", "prettier": "^3.8.1", "typescript": "~5.9.3", "typescript-eslint": "^8.56.1", "vite": "^7.3.1", - "vite-plugin-css-injected-by-js": "^3.5.2", + "vite-plugin-css-injected-by-js": "^4.0.1", "vite-plugin-dts": "^4.5.4", "vitest": "^4.0.18" }, diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/pnpm-lock.yaml b/providers/edge3/src/airflow/providers/edge3/plugins/www/pnpm-lock.yaml index d14b5826c6007..27667afd728c2 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/pnpm-lock.yaml +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/pnpm-lock.yaml @@ -20,8 +20,8 @@ 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) '@emotion/react': specifier: ^11.14.0 version: 11.14.0(@types/react@19.2.14)(react@19.2.4) @@ -47,8 +47,8 @@ importers: specifier: ^5.2.4 version: 5.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 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-router-dom: specifier: ^7.13.1 version: 7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -93,11 +93,11 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) '@typescript-eslint/eslint-plugin': - specifier: 8.55.0 - version: 8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.56.1 + version: 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) '@typescript-eslint/parser': - specifier: 8.55.0 - version: 8.55.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.56.1 + version: 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/utils': specifier: ^8.56.1 version: 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) @@ -106,7 +106,7 @@ importers: version: 4.2.3(@swc/helpers@0.5.19)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)) '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@25.3.3)(happy-dom@20.7.0)(jiti@2.6.1)) + version: 4.0.18(vitest@4.0.18(@types/node@25.3.3)(happy-dom@20.8.3)(jiti@2.6.1)) eslint: specifier: ^10.0.2 version: 10.0.2(jiti@2.6.1) @@ -138,8 +138,8 @@ importers: specifier: ^17.4.0 version: 17.4.0 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 @@ -153,14 +153,14 @@ importers: specifier: ^7.3.1 version: 7.3.1(@types/node@25.3.3)(jiti@2.6.1) vite-plugin-css-injected-by-js: - specifier: ^3.5.2 - version: 3.5.2(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)) + specifier: ^4.0.1 + version: 4.0.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)) vite-plugin-dts: specifier: ^4.5.4 version: 4.5.4(@types/node@25.3.3)(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.3.3)(happy-dom@20.7.0)(jiti@2.6.1) + version: 4.0.18(@types/node@25.3.3)(happy-dom@20.8.3)(jiti@2.6.1) packages: @@ -180,8 +180,8 @@ packages: resolution: {integrity: sha512-pRrmXMCwnmrkS3MLgAIW5dXRzeTv6GLjkjb4HmxNnvAKXN1Nfzp4KmGADBQvlVUcqi+a5D+hfGDLLnd5NnYxog==} engines: {node: '>= 16'} - '@ark-ui/react@5.34.0': - resolution: {integrity: sha512-9NDgvAyu8sdiJGDjD5MTkgsSwnSU22iYs4emF8Kpn3O8GI4aOb4I65kxqaNjd2ROaFa83jdN9gKozaz3lyml7w==} + '@ark-ui/react@5.34.1': + resolution: {integrity: sha512-RJlXCvsHzbK9LVxUVtaSD5pyF1PL8IUR1rHHkf0H0Sa397l6kOFE4EH7MCSj3pDumj2NsmKDVeVgfkfG0KCuEw==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' @@ -261,8 +261,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' @@ -516,14 +516,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-fetch@0.4.0': resolution: {integrity: sha512-T8T3yCl2+AiVVDP6tvfnU/rXOkEHddMTOYCZXUVbydj7URVErh5BelIa8UWBkFYZBP2/mi2nViScNhe9eBolPw==} @@ -659,79 +659,66 @@ 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==} @@ -825,28 +812,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@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.15.18': resolution: {integrity: sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@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.15.18': resolution: {integrity: sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==} @@ -981,14 +964,6 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.55.0': - resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.55.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - '@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} @@ -997,13 +972,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.55.0': - resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==} - 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/parser@8.56.1': resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1011,45 +979,22 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.55.0': - resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@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/scope-manager@8.55.0': - resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@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} - '@typescript-eslint/tsconfig-utils@8.55.0': - resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@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: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.55.0': - resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==} - 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.56.1': resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1057,33 +1002,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.55.0': - resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@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.55.0': - resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@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/utils@8.55.0': - resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} - 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.56.1': resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1091,10 +1019,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.55.0': - resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@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} @@ -1172,234 +1096,234 @@ packages: '@vue/shared@3.5.29': resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} - '@zag-js/accordion@1.35.2': - resolution: {integrity: sha512-wawSszdN4rzGA0ejZxiOVAXMamXva6398bYuM35A21YcJsntpjSSxfwLsH8KDxwOxprrKTc9UksPzz+3hHTm2g==} + '@zag-js/accordion@1.35.3': + resolution: {integrity: sha512-wmw6yo5Zr6ShiKGTc5ICEOJCurWAOSGubIpGISiHi3cZ4tlxKF/vpATIUT3eq8xzdB56YK57yKCujs/WmwqqoA==} - '@zag-js/anatomy@1.35.2': - resolution: {integrity: sha512-aHZBDq3472g8t8uwZBwKAVCxtpP51lqQKuyGnHzh2S5dwqsZMilPhRUEcTybxJEdWRTiRJq+UiYiclaFcr5stQ==} + '@zag-js/anatomy@1.35.3': + resolution: {integrity: sha512-oqU9iLNNylrtJMBX5Xu4DsxnPNvtZLiobryv2oNtsDI1mi1Fca/XHghQC9K5aYT0qNsmHj1M3W5WAWTaOtPLkQ==} - '@zag-js/angle-slider@1.35.2': - resolution: {integrity: sha512-P3o6i88y91k9uO1oAVheAgAHdQQ0FvRJagm94d541ibwrn0F/QL4ZRZm0D5Y/u08CLhefVFROTxicEa+R01zFQ==} + '@zag-js/angle-slider@1.35.3': + resolution: {integrity: sha512-HXRlmsbNEJSBT53fq9XQKL/vwZWwJC3nprskI7s4f/jy8a4uXPTlv7N7zuBYjew+ScTMzZah6fLWzUztBehmSg==} - '@zag-js/aria-hidden@1.35.2': - resolution: {integrity: sha512-QWEU4fQwgrQRurqX7GUswin2iqE6w0gjpVWAhPukJ9891lZvRKiIp53SSnLQc+NvrxO3u2KA46+Onebt7GfLTA==} + '@zag-js/aria-hidden@1.35.3': + resolution: {integrity: sha512-dk5POebn10WneQfLrEgbTzwolaXWpCSHL6F3jCTinW9IbOx7BXghzJD21iU5Iun+y9CorqJPW3p7LplYNUMO5Q==} - '@zag-js/async-list@1.35.2': - resolution: {integrity: sha512-UZzncsKOKCzzxik6SiBRalRDbKao1RA2E545N8TxwMilDU9hqMSggimekX2r5uFu887puMqhay05LM7Kuhw/tw==} + '@zag-js/async-list@1.35.3': + resolution: {integrity: sha512-SXX3wGzLK/maKS1PJ3XfLIGWbu0022f/OhcFsT1PbiHnoFZTH7h2fBhirrCBfy2TYFQ6r5uxgjkhPUNkuaeYnA==} - '@zag-js/auto-resize@1.35.2': - resolution: {integrity: sha512-kg/0DKuvTxjBxfkgdcYU8dG9yfE0f8v6vTpUtYTJkPZUbgHVkQjtUwBwAaHYmROnK49Tl2IjxowNFiwRFpUF0w==} + '@zag-js/auto-resize@1.35.3': + resolution: {integrity: sha512-ufG8HSqzLd9h5rnos8aumj8iORlRskeR/gbpJu1NHrnHBWIrpuXm6KJJR2oZhTFY1BUMMk8eYIBA2QkVuiJzWA==} - '@zag-js/avatar@1.35.2': - resolution: {integrity: sha512-j2b63Hffc+p4Er5/MoU+u6J852ysoQzLtck5gWp9TiJgSBXgYyL2VibWkiUc6UPN9acuTrnKspnBdLgSb0n3eQ==} + '@zag-js/avatar@1.35.3': + resolution: {integrity: sha512-lbQ2Q4Va8AAScKULOHw2tCQez+0JRYGHSMFq6i+dJmeT3dlSgRanm69ra6K2po6hM9E4v6pRe+xOVE+9QMDnuA==} - '@zag-js/carousel@1.35.2': - resolution: {integrity: sha512-cBufDBObRpYw5myeZuPCmbIi3KxJxmL3bD/BhIVarHZxuz/3xhzoObZti9xQEsuO8MNB85R2HuaMkvx2swLf9Q==} + '@zag-js/carousel@1.35.3': + resolution: {integrity: sha512-F+b8HzUeZfB+xUkAkLG4r0Ubui8pj7pSgZhi26ZiWgsM7tsd7cD+xRMXkvPEITN5Fd5QCe3KlVBuE00w5byjmg==} - '@zag-js/cascade-select@1.35.2': - resolution: {integrity: sha512-/3c1lPIN4bYTzRW5bMZLHeq9Z/BakCNulqrvS9HTgaofiHjSs0RzZSdwvqG3lu+Z+FqIUn8PT7scJbPT23bz4Q==} + '@zag-js/cascade-select@1.35.3': + resolution: {integrity: sha512-Nifdx77hEuAdXqr1wpZSPjLXqygRhq/WvnPjGhCeSqFPpy62uT4JZ3avyjUZ4I0UhvIpkleUcXtFwQ3cSMh4ww==} - '@zag-js/checkbox@1.35.2': - resolution: {integrity: sha512-pKaBNJSkjEnJj2VVdVnF7t79dMH98L8Qtn45wsjKsONMpQOmZVNnCWcILlAYLDycO+qlI5ZHGVtMr/g5qFPW8Q==} + '@zag-js/checkbox@1.35.3': + resolution: {integrity: sha512-8XBt/Wg2zSQWqV2ZFqZBQUjYRkOYHA2O3IEi0VVYtds3S1n7Pu/HqkZT5qDw+E/SY2+X9Uyx4hO7h2XrlsiZQQ==} - '@zag-js/clipboard@1.35.2': - resolution: {integrity: sha512-XAWaZRRk38ZJ2UnmUW2os8wMzxh7AUPR0EEncmOasBvc3pZAYVLBN33N5uN6mTVtvkxosVlYyxvSI6iD9f5E/w==} + '@zag-js/clipboard@1.35.3': + resolution: {integrity: sha512-obTwynBpp6c17fLHe5tg//FQ497QsyCEry+K3bTdlrivWW200wvfHxZ6RKVbKwDAwhH+ye0bI1xkYAId8j7sdA==} - '@zag-js/collapsible@1.35.2': - resolution: {integrity: sha512-o8za0T4zXKuq5O4ghHtJHsr74Yj9CyyAqViva6l8bwa+WTv8bF7mppioBeCgnxJ8e3AQjYIdXzRX8FDsWAPJGg==} + '@zag-js/collapsible@1.35.3': + resolution: {integrity: sha512-IweG8JOBCerJwLO6QzTZGEMlsYUmQfQSeD0jniFguMM8vcunvGVSrM+AaL8pDbmXd+snXokaGyJpGO3vzMW6Fw==} - '@zag-js/collection@1.35.2': - resolution: {integrity: sha512-iaZWAjy7V72k+KZEL3UDbJKJgTxQmp8MUEfhe51upo1GVOV0kR+2t4TJGcDO3YZO6/2SlzVkJQZGVWwJud/Bnw==} + '@zag-js/collection@1.35.3': + resolution: {integrity: sha512-BYoWJ4b7ma2PgiuQbRSnP603f2DlK6se5JtViUHTamZScLLLWnWHuQ6zFa1KS5kiIkbb7CFM6/bJ3WNYLch8Ig==} - '@zag-js/color-picker@1.35.2': - resolution: {integrity: sha512-Fyspx05WR6uoyOPyMEP4LOjVrA0uLwLBPUU6DtUW5wfZuBF1QGRAIH9giOQQMDontDraHdXRJU37hKclwpUuxw==} + '@zag-js/color-picker@1.35.3': + resolution: {integrity: sha512-i9roSgtqeA1b4Q+jWqnxjXB//BQXMP5m1FQ4YcZVq/0yT14A53JIknchuqrh3wC3yPsJMXFqCoKg+NET2+OVig==} - '@zag-js/color-utils@1.35.2': - resolution: {integrity: sha512-4HBVf3qr/01mg9FXs9tgvRdkbqkCjYYrVGmBIetFPmt2mjFEsOTSfvap+e3Q1JCqPPT79dLWbWm1epMJ7zs/aw==} + '@zag-js/color-utils@1.35.3': + resolution: {integrity: sha512-vxkEVgz4YdSbdaPvjiRI1VsJAdwzu/dUNvzqOaiVcPDrHr/FFgmUbv0SOFjnfSb2QWGI8EDEMn02RW9ym+BzGw==} - '@zag-js/combobox@1.35.2': - resolution: {integrity: sha512-wNo2E4o6gdt6/mTKXUobN2dvbJyevSDFaZOuPXezRPFlDlFlZwgklawwGikRegXXdTPsm538qGAvxCLbQuLqeg==} + '@zag-js/combobox@1.35.3': + resolution: {integrity: sha512-s1qmttTGJTMjlDakL+uvWSEggpafKr1vhOeZCh8j+N4eFt9bLAwaffjuh/1JzWBvzovw7WoMVkizdTXPlN8oYg==} - '@zag-js/core@1.35.2': - resolution: {integrity: sha512-GDA27Val+dV/XEEVh9ghXky96eiC8W4JkSBFt21SecGqJl5LjKJQn9ZMg81TZ+kUwIEd0F3HTfk6FKLSjrC3qg==} + '@zag-js/core@1.35.3': + resolution: {integrity: sha512-fGAHyqOYSEFmo52t7wI4dvbFfLyJmUlyf7wknsiUlzUHlrn3yv5PAZYZ2TibpOD1hwXIp4AoCjbiIPPZBxirZw==} - '@zag-js/date-picker@1.35.2': - resolution: {integrity: sha512-C9Or9Qn1TQIt6VoGEfoxpM1pwGZedHX99w+QGZJZErBBBPXejG8rzmCGOwqRI6Ui8Sqlbi9yjWNCY9qRCXKlaQ==} + '@zag-js/date-picker@1.35.3': + resolution: {integrity: sha512-4G10h6pzzLbd84SE2CKtqi6Z9wEBhSyx4GRSxxy3tsf5wAxnz4anRFat9CGwn2YVUYcUJpD+umYgBMPt6zGDnA==} peerDependencies: '@internationalized/date': '>=3.0.0' - '@zag-js/date-utils@1.35.2': - resolution: {integrity: sha512-FGVkU2Dt8FRMTiXcPjarhM+FicSuRgLGvUb7y3/bi6v/lvr5KB/+nwEK+iwwH4IKBYO0Wi00Rn0yGYPX8kZjHQ==} + '@zag-js/date-utils@1.35.3': + resolution: {integrity: sha512-1co0FPpZ6nO5dN8sZtECkMYaf+3E5zu0KSIJZpZiXb4TgsZMDyHu7K7IsiKFHk9qmhuF6AdPpNxBju91pSXMFg==} peerDependencies: '@internationalized/date': '>=3.0.0' - '@zag-js/dialog@1.35.2': - resolution: {integrity: sha512-ruaURHvH2gQH4wgp9I/6fmIiCNbpRczJpsO7MN7PI5f3yvUWWy1yv7+wh/Wl9ni2hmglg316N+dccbalywyXIw==} + '@zag-js/dialog@1.35.3': + resolution: {integrity: sha512-byosV+aBHH5LoFKnjEgC7WdqJid7bP9UhgWLSC7+IXbxrif9Czg1YVp6ZlQM6Nx6uD1vnty4touI3P7D7CTKcw==} - '@zag-js/dismissable@1.35.2': - resolution: {integrity: sha512-FebClz8ZEV5oWCJ4PKpMZqUjs7YkGgxwwHqCz7zRlD5Ng7+BJxfutuak+geCF7/8hzjo0QRuLhF0Hs5UZe0VfQ==} + '@zag-js/dismissable@1.35.3': + resolution: {integrity: sha512-XPk+lqmsZp2Z1yMb5K1yj/e7Sobv4D7zK66B1GS97lk9Xzz8vuSgsimcLy0p7RXQl3KL6H5L69inSuQa2exybQ==} - '@zag-js/dom-query@1.35.2': - resolution: {integrity: sha512-iuqtod4+8bMeVC/r5LER/sEqHPRFCXHAyEAdJnAScBnHvnQGbFr0NDxjx3G378gndjYimgHZb98id5rpFwW1Fg==} + '@zag-js/dom-query@1.35.3': + resolution: {integrity: sha512-1RbFZoT4CjlHN9TUNse1++ZVOyKo45ktucTIT349o6HMsoWWKmTJDPvFkMBbmu/qY6XXn4dT+LJEp4bL3DR+Qw==} - '@zag-js/drawer@1.35.2': - resolution: {integrity: sha512-xBmMN0n3iP6f8eVXUy6WFUTZFe6eZmVTm1phAk+psXIlKJwTOH488t07q2XHwlAucxuL9yT+Z7Vpc4cbGbehsA==} + '@zag-js/drawer@1.35.3': + resolution: {integrity: sha512-DN5bwa7bDCDaUSbNzFxMc2U/WmbLcXvPSQjyOpKI6CC3VbW2kKaOnjJ5qQG+W5YBO0FpmJBtaxRV7lke4sZH2w==} - '@zag-js/editable@1.35.2': - resolution: {integrity: sha512-FWMX487UlSPvkUKKfwZtQ3p4SB/bKQtseQPvvLel2CW643Fq/+c+fq5yKonluoP2vLWL78F+Jx0g0rGfZT9FvQ==} + '@zag-js/editable@1.35.3': + resolution: {integrity: sha512-HcjeacS61vQXfNT9IalZj/+oS45yW5bIDO2NjJWV7zNe5AG29NCceUnvBhy+hrUKPnKcjfDocdW5rCL+Lvs/CQ==} - '@zag-js/file-upload@1.35.2': - resolution: {integrity: sha512-YOd8q3JQTzZ0XmeV4MQPl66ODlX7MtullCRlq1yOOdyVVFV1IVGSvuGR/ZNXib7tVFM1Dw5mEFz5NKhGFUSXxw==} + '@zag-js/file-upload@1.35.3': + resolution: {integrity: sha512-oIYwnDct4ERo2mfmcxsBIJnlmpzjrzYx82SQsXWD3NGKx3cgdh2lwBX+ebItaLH1jkgzBa3z0TWxc6rfvcUXbw==} - '@zag-js/file-utils@1.35.2': - resolution: {integrity: sha512-J2Y0K3m/EY8usofW4UTa2y6AlCb+cha2EKAyrjCdF62/wGvbbrwxIXokhrz4825m3kx/xzRv962CijxWmnNLxQ==} + '@zag-js/file-utils@1.35.3': + resolution: {integrity: sha512-Tb05RCzx4swc156hd4jLiO7z+Gxg/HQ+JCds03jgTbrFJAz2D56YaMeI7gSDc1m4Xre3nyqQpSo9AeX5nzbE/w==} - '@zag-js/floating-panel@1.35.2': - resolution: {integrity: sha512-rIuwpl/Dz+oAWPNtchuJ6f3zEbQpILqzl4eyqjH0ZbTzWQ7riuq+CvNKaDs6uhU8G8KhFDn54/qwu95OgL1YDw==} + '@zag-js/floating-panel@1.35.3': + resolution: {integrity: sha512-nTZypcS0X46Oo1kpCQTnP5UlzjhypOAj3B4dq2z/3bAOC0TntYTnFkj8PbEJtExk7364xfMyxfgZOiv7Aqq01w==} - '@zag-js/focus-trap@1.35.2': - resolution: {integrity: sha512-37prN3Ta8+HPyZP+jC5lWS1euQcTM/8aa6VSKfjcpZdjW6Xfl6N3DR65/+cvXFur3pNbEo5J1SCCbVOJU2pvRQ==} + '@zag-js/focus-trap@1.35.3': + resolution: {integrity: sha512-evErLlGFdDVCI8xipNS5k0rAvO+KFRA9g273bbfWAL1+mT54mcB/XHa85nC3QpPgMNrSh+6LUNq9fapyOGoyYg==} - '@zag-js/focus-visible@1.35.2': - resolution: {integrity: sha512-bfB50vZ6Gln2aH7tkaibJfaZZNEy5CJu5vlCSkCd04ccLnxLkO8dzeHOSeCpPw2VS0aer1oIvL4cpnfm6l8shA==} + '@zag-js/focus-visible@1.35.3': + resolution: {integrity: sha512-g4F8PRGIoFoKBrHiQ1HQh5AjCS7brFRXHvpbDNb9+T11FGlF5Turb+6OVRoNV8MmiuqMltO2I28l36YsGc//uQ==} - '@zag-js/highlight-word@1.35.2': - resolution: {integrity: sha512-ypK5KscytePkthY/AS41ZgWRl4v9+/jN1hTveiXLDJotk8B+pDOl/jGkVxE9Ka03+4amLIDgFcr3poA0UQNvzA==} + '@zag-js/highlight-word@1.35.3': + resolution: {integrity: sha512-K+mvEBbf3SUFjQeMeJQYb3cjri3x6sPaPhcKWayalelSLB/StWEGqcpmz+a6uUYrCUAK5kEi3Hn0YLGfn0GOig==} - '@zag-js/hover-card@1.35.2': - resolution: {integrity: sha512-2lqqQD7uNexbEitx0zlJD5JHsciloWeE3rWMpOzzEZDQ7NjenyCU1cugpe93I4OlNPUpxtmUn9jvD/8nOtIUjg==} + '@zag-js/hover-card@1.35.3': + resolution: {integrity: sha512-xVoKOtvrnzhYzciZ1csgiV76IQ4DRtx1lsJeFSrfg5MH0kYWeC/pcmm3yCd2+Qh/45J7DbSXeZneqxpyiF5Vvw==} - '@zag-js/i18n-utils@1.35.2': - resolution: {integrity: sha512-ICpqf18y9uLaBe9fhi7F13ircslUyzbE0orBhZBr16pNjFpIG0mCQ/zT7aL5/ZJcX8ncC1cH7DnwR9QaBk5vZQ==} + '@zag-js/i18n-utils@1.35.3': + resolution: {integrity: sha512-k7UcNxbnC2jvGwCoHYAkFD3ZaRSMQNVHfuy8TujZQ+ci3IJovwgWLveZoRfFbXHkTLfhmbpE2tFXBdpwOVZutg==} - '@zag-js/image-cropper@1.35.2': - resolution: {integrity: sha512-tGV06nsM2TrYl0KkAfvKhjKY4HzWQVhlEKw9mSk8PpcAb+ncMrFOtGy89kWR+IDUrUK+QF2LqW8ZC0cWoAxERA==} + '@zag-js/image-cropper@1.35.3': + resolution: {integrity: sha512-1PH6bg8JAQESHzNqjka2TJ0QGNBGBAO6rb7AZ+9CaCCLw0pIzbUJhqPMkwd9GhdWGKGP+e7wFitnjcT4W5Js8g==} - '@zag-js/interact-outside@1.35.2': - resolution: {integrity: sha512-++dzUJ5OSrLn6nJKTsu0oofKjlhhKn7kHAssI0MmNLYhdMnRSMli8INiR3y+OrvCi/pr3BXr2P9knq94LEZbBQ==} + '@zag-js/interact-outside@1.35.3': + resolution: {integrity: sha512-tOcuo/IztzpU7UKXtjVrLZtXzzcbhP4n2WynKwDRkTkq3mRCp61xXJp1csIBycI3JHm/CMeAEcPdRIioxIT/Zw==} - '@zag-js/json-tree-utils@1.35.2': - resolution: {integrity: sha512-DIE22ZoCnqMb3PgQ6EOsSm7lLLFjz2KQvpF5WV/CeFlmXcVkM7KWfTMMYB8IcXWWSgaXM7jxgpJdjEGsFs+7GQ==} + '@zag-js/json-tree-utils@1.35.3': + resolution: {integrity: sha512-nOv2dPJf+1mxsobYiSlYt96hR1MK7iHKG1iDLoO5wLggS6GQA3ix1BerHJK0zdehoEZ71R45el5ghCG1HB9VzQ==} - '@zag-js/listbox@1.35.2': - resolution: {integrity: sha512-KoF8zplbN1OedRup3xHthLMtv6BbsXXRRlKef7lThUOLrzyTtD+gteBwwBLconeHPiAxsNi3SYDgknRMfkA5RA==} + '@zag-js/listbox@1.35.3': + resolution: {integrity: sha512-FE6FOuBr6aWtOb8U8oDvAvcUzD6JKLXAe8WngiLFG+b2yyW4nlaz2AcKRG1bjjB066UMxMo9/+2p4D0Kf5Id1Q==} - '@zag-js/live-region@1.35.2': - resolution: {integrity: sha512-BgG14aSW2zo/RAXn8Kk2Ee3ol3HUFShtz25XJB9HH528g8KhSxfIC7NpLihW9MeNOMNLYAgieIHabEI7+mhs5g==} + '@zag-js/live-region@1.35.3': + resolution: {integrity: sha512-64rWcfggYpyr2Fn4pdrB/lljMgm3quwn9is+vdDN85Vv3WShKWoz08T4njidm0hwcIbzas0bRqQYWDLLsAoSJQ==} - '@zag-js/marquee@1.35.2': - resolution: {integrity: sha512-e0jWaxaYAxrcmUzHC2Jw6xEA5FaKosRcHUKixvpyTqwN5gDAatE1kYi3CVtyRysKlLcfwXaKzXqcozp9X3U7NQ==} + '@zag-js/marquee@1.35.3': + resolution: {integrity: sha512-bKZVpmAJWPDORP7WOWnS+65W5ZQBQmRs8zvV33ZfCpFbkXjhRiqKSzIj223/VOc2NEDjyWagz2vioAxrFYVzww==} - '@zag-js/menu@1.35.2': - resolution: {integrity: sha512-W4+ds1S4bBWoaPLqvtDpvCEZv3AsCt3bQYBtD9f/1uB5ciwMjpe1hVb5VKcBQuTt9r3SogZ4Cfg/DeTP5dgx7w==} + '@zag-js/menu@1.35.3': + resolution: {integrity: sha512-KyY0EZXkIU57Mjt+Lg+pupiePk3LcnQcB3Gl05Vva61bNjBjdKV71qwCQru/OxPZEwYgPo46L7TDIb56kfK/VQ==} - '@zag-js/navigation-menu@1.35.2': - resolution: {integrity: sha512-553lPpZ9WBNijg//ufD0iqNNRgjQDrU33rZGUKA4wmTIrr/sh8DXcdmf5KmS3/PnJXrjDRs/cJpmDhvrQqefiQ==} + '@zag-js/navigation-menu@1.35.3': + resolution: {integrity: sha512-8cCHx0X/KjEpr2BaMOxJS5LiA6fs/CNqVTF/sTTgZAv7Dm+MH0yNuKm4kpPvcLaVeBpVE09bnyCHrNKzZes+Fw==} - '@zag-js/number-input@1.35.2': - resolution: {integrity: sha512-fOI4VG531EjpQwipBhUBhHjvjG7Si5YYZQr5BdjOyw/Fh79lk/itL2wc3lfS8GnDPpgNaAkbcBmT6Y5pDriP1g==} + '@zag-js/number-input@1.35.3': + resolution: {integrity: sha512-uqawVybAcLcefVEHMVONuAA5kDSDPP5TsROr5PnAyFlhM1iD85+r3KAfCueoDX5w2X4ibbu9o2tdV6zTFKD/nQ==} - '@zag-js/pagination@1.35.2': - resolution: {integrity: sha512-UoSVQ8UBlQ1RcT11pkk1gaAUamMmUAboPYqteU203jPo5G3l4CmrvgURQCgVeiHxNjDkV0FfmeiZ/qNy1LyECg==} + '@zag-js/pagination@1.35.3': + resolution: {integrity: sha512-fKm4s5KAd12RiCI/EDmmGKjPQ+i2qS/UsJPdMe65yb/4mY5OibwV2zyHcVeFsOD4gBZpnU6kYlDAGSttmLWLlQ==} - '@zag-js/password-input@1.35.2': - resolution: {integrity: sha512-bVkO0Z8VKl7NwAOE2njA8icdVOXWPd62/5PpkgOQREr8NSTnbJ2+++uY9BwVFtpnp+WfkTdlFGksUhO7qdROxA==} + '@zag-js/password-input@1.35.3': + resolution: {integrity: sha512-etd0gm6ELAm3y+cFhPU+TYm8khm9cL5Mg5m2DcZxu1Mqpj7JY0LsXZ8SFOdCZgTIHuMEhKBiYfnuyMAd4CJztA==} - '@zag-js/pin-input@1.35.2': - resolution: {integrity: sha512-XgNcr+Qs/HvAvCH9LJtDssyc2Q41JupqMyl8O0HLIhxdGFg+ae+HwCBdpmfs+Pn3wLMBIqMSDktUfQ9USgmwuA==} + '@zag-js/pin-input@1.35.3': + resolution: {integrity: sha512-ZFt+WIHMdVlSg29BrQLFq5ijabiUO3tXMhoKhjjzTSe/tLqfNeu3UxFB6y/FYpn8+Cvn6xwvhu3lgnORYmI0zQ==} - '@zag-js/popover@1.35.2': - resolution: {integrity: sha512-YS2GpZ8/l+V0I475f+RBYTLEImjyqs2j/chq4RMBtSKn5t4vxeV7PMoTtd8xg3IEtsfq8XKeBYYGWv0jQII8HQ==} + '@zag-js/popover@1.35.3': + resolution: {integrity: sha512-+MIEENPsbKPxzoNuDI/C5d5ZN9uxnfZ+MBDc5C5XSgjjg9FcvMXClNq7IFM1aZi24peRXg9cMNf//lApVRT37w==} - '@zag-js/popper@1.35.2': - resolution: {integrity: sha512-d867Lb0XYmde8sPPtrkGvW+0Y1owCIIV1aMycMnMmQXxSsc35ZrgF3g8Pwzn2vouIEQP/D7xhbuyGcJzWXyRug==} + '@zag-js/popper@1.35.3': + resolution: {integrity: sha512-gpB7Xn9WtlfrUsIVbSgNQGDwgNOL/cSGt0Id3wEQKArmqVC704EWtPvXzOMMybBEdm8YW2hQrXuo+o66abI1Sg==} - '@zag-js/presence@1.35.2': - resolution: {integrity: sha512-NGK+dYDNkqu9TQzGzAQyKRfF85AdKLVR11qETxjUzBV/0Deah3XwKBqre6lGC3rjv4e3u4hqezY7JWdnp3diTQ==} + '@zag-js/presence@1.35.3': + resolution: {integrity: sha512-ev5E7+U9IZAGvEaflpdVLHaZl8ZaQMhGB3ypd0yKhPwXeM51obV8w3+5HjzTqHPl8TKuoHWL31YaiUBd5EuS6w==} - '@zag-js/progress@1.35.2': - resolution: {integrity: sha512-53m2pxkqcVBuubMzKx7Gs6q6kgySz7Xj/g3av4ASNM33FJGPgYOi61dGI0QR8L38txuqy4pdwLKxuRBuJOa2kQ==} + '@zag-js/progress@1.35.3': + resolution: {integrity: sha512-u0GxQN1AfXMAgzYOUMxKQA12DyuAP0svh2S//KvOorTSv7d5hAa8nZXi2cEv5abYsyfKJ6/bc1Z56byzW1jVZw==} - '@zag-js/qr-code@1.35.2': - resolution: {integrity: sha512-UA522xrboFunJViaVbnQipbdlxinbhF6mJTsPekfyEvYdUw9v69nbRpV/VEsvneCocfYa5lNDL25jNZWIjKc7g==} + '@zag-js/qr-code@1.35.3': + resolution: {integrity: sha512-t0Ehwogr49vTNtWyNdQU2tYex7uJyfAn7N/5LgD7FXw8aa+RBMWZWlqjCUvHqJ929tVMrn+LIrQnZCcwNunalA==} - '@zag-js/radio-group@1.35.2': - resolution: {integrity: sha512-FmhB35p1AgOxcGGKOBeQtJva972KqgQ4iiqkbgUIJEmbeHQgyhQCNJVx/IFpm+5IaC3IlZz5Jri0p5frJ+sYyw==} + '@zag-js/radio-group@1.35.3': + resolution: {integrity: sha512-kOzocjqWk3dXuRfyfsHwfw63Z99NHbc7rvVUutSsfXANXi+DFYZHuqdPUwMt+29LfaL15XTOfuGV+yUXDCgQHQ==} - '@zag-js/rating-group@1.35.2': - resolution: {integrity: sha512-hXCwtunVIiFo+DmvGo+nYl178ww+Av/nLMj/yEzFEKfaSET6VATB3kyRRHSp4E5Vc08b3fI9TIhS89lwRfnbCA==} + '@zag-js/rating-group@1.35.3': + resolution: {integrity: sha512-BmhJZdbaTnd3nFWMY+nR+HF952UhWXfaXXxiBWptSLMBfAYImQTWBMrLgTHCSnVfmFATj4Gb7xQe79FQU8T5fA==} - '@zag-js/react@1.35.2': - resolution: {integrity: sha512-RrLLRvVH55/xFZe2GTHzQ1JM/2csUM9uUHS1LiXeZxHQQWdlLMa9MH7Vv3K9FA7h30G4JQ6mj7VIdGWtq3eDkQ==} + '@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.2': - resolution: {integrity: sha512-qJUM3611+M6mWAgcUuvyyu3PKSJbOrb045uZTGOOtmZXSHuuRqHQfgo/QN4MlA4veGiYi8unVnQ4Z70uZzKEnA==} + '@zag-js/rect-utils@1.35.3': + resolution: {integrity: sha512-mt/oD3RXdyaX6ZPSd8BO13vvPBJ7QpVWieubE3O0WM3OPhU7ykDMRp/tR7cYMQrzUm04GlY9pbkmSSw2uABxlA==} - '@zag-js/remove-scroll@1.35.2': - resolution: {integrity: sha512-qdKOzbVRZpp4119hJjAyPx4CRaXR12gdLd2uPRV70W8LE7duVyfX2sigEE2OtTxzdffwFOi7z44eL6zbWz5Cfg==} + '@zag-js/remove-scroll@1.35.3': + resolution: {integrity: sha512-e59z9SbEpPiw0qwNQa2cB5/h30ZCLREaHsCw1TKTANFhwg7v85k9Lq1H/G/49li1CAjmiaOU9BNGlDvbzpNETQ==} - '@zag-js/scroll-area@1.35.2': - resolution: {integrity: sha512-n923pfUFmy9KNAnJcutmDsPZPAM7zJiwfemig+aPWpSIzLv198Sez0nyHZ9Azu2X2NcnEKhgsGR8FFbeRRF05A==} + '@zag-js/scroll-area@1.35.3': + resolution: {integrity: sha512-IQwdUws/AckRIHK1z/wHdHurnOeGd8h8Dmspfh3VT7NkwTnxeJ4SW9di9smuD+d25eXkJRuX5zGEDHAyx2IaPQ==} - '@zag-js/scroll-snap@1.35.2': - resolution: {integrity: sha512-cDbh1kXxFRDCxjA7nbYOI3oSQbUQbe9BNZML/blDIhQn84tKku+Nsd/+dZZZyB3Ney0mVdtOjEbkBsnFAkulRA==} + '@zag-js/scroll-snap@1.35.3': + resolution: {integrity: sha512-NVa2yRm2DQnF6hTV9k7Xz7l8YCZBagZTiqSwNvWKUulKD1csjt2fpBxvUt2cK+1iQnLOey2ydhs7MMsAnXPbJA==} - '@zag-js/select@1.35.2': - resolution: {integrity: sha512-Y2zqpsJ1XjfuKUhi2GphwTn8aEHR7GPK2ZepJJz9e1BGKCFl5bL1SsDsT59N+GKY+7LiBEESyPi8+PIjnk7Prw==} + '@zag-js/select@1.35.3': + resolution: {integrity: sha512-ztszGHWvlbBDE0YT5LYPH+sMd6VH1ct5pH/M9VSzIUO6C5PARkW0NwSVQ1rCQJMj4sfvSE1gC1/r7urRzqEcUQ==} - '@zag-js/signature-pad@1.35.2': - resolution: {integrity: sha512-SSFDhPZ49hkZoJTqEgY7AJ7IJNgDQKoT7vByCuzPlIfIpt+KcIykwkOZKExtTlYo3qGfc7N5Eip6dS27i4pAkQ==} + '@zag-js/signature-pad@1.35.3': + resolution: {integrity: sha512-jvtxxzAQ8fre11zWUh6HflG4Ycr5z83Wba4pONRJbUE/vNgkJQ7yJgfyUl1QTlkn8Arfg2Zwoxu9GIq80HLZWg==} - '@zag-js/slider@1.35.2': - resolution: {integrity: sha512-u9O2w7qph/PmIVaGFd8VMVP9FRhjSlKarnYismzc8Ah+emPso0QzmnVpqoYmZI5Iggv5RtVy6tmUgxnbQqi5ag==} + '@zag-js/slider@1.35.3': + resolution: {integrity: sha512-Th142JO4Fqla5AWhGrTW6CQicwvTw87PdVpur/WotQ7brlZIww5HipzEMh5eQJSWfwpKD4PI2bYK9V/ZE/mpXA==} - '@zag-js/splitter@1.35.2': - resolution: {integrity: sha512-tFaWjv6b83MKbgPLkox03FD3OeKR4b2cEz1r9L3nvRGt+/YRVqBqF8ZjlXX0SQMX8JPzkGdJnLostwEfjseTlQ==} + '@zag-js/splitter@1.35.3': + resolution: {integrity: sha512-IsIbRwzjr5amGANEDsZDSToaSn8wHUWvS2l0XHmf3BiiguVApaZgQTlfqthVQC9hBHMOaGIXIW1CFUOrQYkvUQ==} - '@zag-js/steps@1.35.2': - resolution: {integrity: sha512-ID06v8M2Z1NJmId0ifO2+dSfuNO6+fa2ZD0ux0vws2pMp6L/HA8Z2mDxXXMaoX8Yssris91Q0XcDiw2f5DU1YQ==} + '@zag-js/steps@1.35.3': + resolution: {integrity: sha512-TYIrqV+v9/ULhvrTRBtQFFvJQPPTWOmjFXxlIxDwozek5R4dCIyeUYt1/ChJEc2mNETocbfDVSTxRO1dwCFpwQ==} - '@zag-js/store@1.35.2': - resolution: {integrity: sha512-8ak4muOo739qO+xlXIna9bcqGFbsB3jOcRt9+D04Vgj8IZ2wAZI0yK7/0vUJVJOj33CkgzAQJIVzOWxFty9mwQ==} + '@zag-js/store@1.35.3': + resolution: {integrity: sha512-7kEV4T/20DU36UIfVMzuDlLhWSSEy/vabmpiB700tcdD9BBBODTiSg3ZeljW17dQbvE545vZOFEjVf/cQ5LVGA==} - '@zag-js/switch@1.35.2': - resolution: {integrity: sha512-2N+t+kAKE/+BkloVZ7v2Io8PnlFYaZJTE4m4/Qd1zLbsKg6lxBkTEk4tVtIqQ7ob/9O+xVO30SzVLj0jsOSihA==} + '@zag-js/switch@1.35.3': + resolution: {integrity: sha512-EP/2cJ46sd+6C5x5+89jn/9NOpM05CRESYB4RMhOnTe/WFtcS4IpiYtVHFhikdXkvJoibm67O2EHep2Pm/Xj4w==} - '@zag-js/tabs@1.35.2': - resolution: {integrity: sha512-YbqxXFbdC7IPdiRpDjwol7TiUnqq6RPV70W87VQAMUPDpoLShfoSULC5IvrOryG7C5dP2Eh1QXfn3WN71czevA==} + '@zag-js/tabs@1.35.3': + resolution: {integrity: sha512-lZKlDmxE25miCikj9QZCCnL02SVV2K14KZy5bn7+XDgrWlfSNTpNTj8r5E3zGlSgio5pkTGou57ASqS7WaPDWg==} - '@zag-js/tags-input@1.35.2': - resolution: {integrity: sha512-C90JfiNz0UQb5egQ+UZD8C2BpE783x9lVGPUVpG3YTFBPieSbINWlcI3qCpuN/6ohtY8LtJQvKwSnqjPJCKaSg==} + '@zag-js/tags-input@1.35.3': + resolution: {integrity: sha512-HqyoQ3DZFhByOGnDShFfxi6u0bIf7aSVTlwmAvcL+b2ZhyU6/wIMGc4WJE7BMx1NYWM/jNLHedvGExAI8R0kXQ==} - '@zag-js/timer@1.35.2': - resolution: {integrity: sha512-0nfQ3qU7uoBw82v9JmXle1F/voesO9PkQpoQ9F/ygGHvKYlcbePX+0A04BUD/H9BDZjHTIoLhd8tP13dX/mjgQ==} + '@zag-js/timer@1.35.3': + resolution: {integrity: sha512-edmgitbRgsq+msxvVB4wc17Q5d5k63zMWaLJnWjUdDGAgEtM6/HNxwGb3riv46S2U3RgYxaaHTNZ/M7EE5mvYw==} - '@zag-js/toast@1.35.2': - resolution: {integrity: sha512-49tIclhb2yyt/jRcjjuQ5REssaVHSRcx/PSeeaUZZQfUtwGAmq1xj1PB2D8NXGayPQ9Ge/ILghbeu/77nsqhng==} + '@zag-js/toast@1.35.3': + resolution: {integrity: sha512-whlR791GHdnMD21nNPsl2Dbql8+qu1wBZl75QzwYrjR8FlKjp8bhr3gXKzQEddcBXe9GPEFGvUs4iCyXsuTbpg==} - '@zag-js/toggle-group@1.35.2': - resolution: {integrity: sha512-3HEtPTLSTstiSbFCYM3DKHqoXeXeaEBPjYTJQ+a4BhtlMn/qXAbW6HrgCm49EtSkCLJDD50MxJNUtGBZq6boqg==} + '@zag-js/toggle-group@1.35.3': + resolution: {integrity: sha512-Gn6JHzkQ4tlttjZcE0ZjIdxYkFeVp9VHrcMVizjJTkGZRmQ+kPZ5G/wOsZhIrvLX3Dw6Y0NkuBcP+jDHz/o3TA==} - '@zag-js/toggle@1.35.2': - resolution: {integrity: sha512-HS1Isst1cngHyV8t8TJUCz6LVucQVOzRbLyT7Dw3hETNQeJWbdeqsy1ERorTMmDaPYiGDDrRcHQp6hTKwuowhQ==} + '@zag-js/toggle@1.35.3': + resolution: {integrity: sha512-aFfHKuR4sKzglhkmWLA+0RTNPs9dfeqwtc96qljawGYfAYWJXkEPYK9dFfVa+arZ7L84xBi24QSLiTg7LGSFLw==} - '@zag-js/tooltip@1.35.2': - resolution: {integrity: sha512-nQX79TKmWqA1nUsK1s2ZQ9ArYH9VnLHeCVUxHguavqaVS/dF+qVOwm5KSaVpUHw1U8YbpPpyKp6Xv86Vf8j4eA==} + '@zag-js/tooltip@1.35.3': + resolution: {integrity: sha512-/pImDGYl79MfLdvEphj3rSvNdj2tLW4GwGEncgdLM/GKwQiEUjfi/9EJOfLYP23M4lOOnoW7orehJ9xeaXOAkA==} - '@zag-js/tour@1.35.2': - resolution: {integrity: sha512-FLlfOmJIMnICMYQjS1yraBBw/BF9pQ8S4hMLvjdhZiZnZ+FnQLQq+avFlXul8RbECI/sXLnOC8ouy8koGIV+2A==} + '@zag-js/tour@1.35.3': + resolution: {integrity: sha512-DI2aCXmZaE9KcPZDs9itc2BO7ixLApJ/yVRfM69pXwVOrucdSeDDNPFkfbhj5XwB+9VjjZEkqWFHKntRIyPl5g==} - '@zag-js/tree-view@1.35.2': - resolution: {integrity: sha512-7s0ihoTyTCu8cGfxHZQO5mGcX/KK+JCkB3kk6Eo8DFLkFv1tgID9/UEofn51LicgNejrx1rKio3YlB13KMRanQ==} + '@zag-js/tree-view@1.35.3': + resolution: {integrity: sha512-DbHaLxSNa1goE3o3IsXxEdzp8P5dvmkk1rVWgNUUIhpA+44idEjSSNXJkHPl18Mk5blqSMVjK1EX91oqai01Vw==} - '@zag-js/types@1.35.2': - resolution: {integrity: sha512-Y78aZ4yrQJGGiFBh9j/PAPRbFEvHYb0YJ6PlbhqMes3liIH2GOPEdCllFvxMW/Zxck81SvEodrVoVbNLhKL0Ng==} + '@zag-js/types@1.35.3': + resolution: {integrity: sha512-Fnm3AMs1lfb55hlkip/eJeWHOjFB3gSi1JkZlkkdltG2l7y/zsHkumPSe6jIKy+DRRIFKRCyXVTatbPN27bO3w==} - '@zag-js/utils@1.35.2': - resolution: {integrity: sha512-E/9S9hzXmeL94wkBY7PFv3ynopqCl0TLL//yhgMzRj0D00F54OWgjgKNY8bUZqSEPztJe/uNJEAwQ6QuyBIfsw==} + '@zag-js/utils@1.35.3': + resolution: {integrity: sha512-LHcC+9y6TFhDsIz9I3koYxONl2JFfx5yQDzc6ZEQO2cqzXedRcN0R9IPqNGCX7JuhGt14ctDkVCm1JWGP2J6Wg==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -2066,8 +1990,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: @@ -2646,8 +2570,8 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - 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: '*' @@ -3000,8 +2924,8 @@ packages: peerDependencies: react: '*' - vite-plugin-css-injected-by-js@3.5.2: - resolution: {integrity: sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==} + vite-plugin-css-injected-by-js@4.0.1: + resolution: {integrity: sha512-WfyRojojQyAO/KzWf+efcXpTPv6zJPXaRmr9EYq5a4v5I3PWCR7kR01hiri2lW6+rHm3a57kpwsf+iahIJi1Qw==} peerDependencies: vite: '>2.0.0-0' @@ -3188,73 +3112,73 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.1.1 - '@ark-ui/react@5.34.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)': dependencies: '@internationalized/date': 3.11.0 - '@zag-js/accordion': 1.35.2 - '@zag-js/anatomy': 1.35.2 - '@zag-js/angle-slider': 1.35.2 - '@zag-js/async-list': 1.35.2 - '@zag-js/auto-resize': 1.35.2 - '@zag-js/avatar': 1.35.2 - '@zag-js/carousel': 1.35.2 - '@zag-js/cascade-select': 1.35.2 - '@zag-js/checkbox': 1.35.2 - '@zag-js/clipboard': 1.35.2 - '@zag-js/collapsible': 1.35.2 - '@zag-js/collection': 1.35.2 - '@zag-js/color-picker': 1.35.2 - '@zag-js/color-utils': 1.35.2 - '@zag-js/combobox': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/date-picker': 1.35.2(@internationalized/date@3.11.0) - '@zag-js/date-utils': 1.35.2(@internationalized/date@3.11.0) - '@zag-js/dialog': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/drawer': 1.35.2 - '@zag-js/editable': 1.35.2 - '@zag-js/file-upload': 1.35.2 - '@zag-js/file-utils': 1.35.2 - '@zag-js/floating-panel': 1.35.2 - '@zag-js/focus-trap': 1.35.2 - '@zag-js/highlight-word': 1.35.2 - '@zag-js/hover-card': 1.35.2 - '@zag-js/i18n-utils': 1.35.2 - '@zag-js/image-cropper': 1.35.2 - '@zag-js/json-tree-utils': 1.35.2 - '@zag-js/listbox': 1.35.2 - '@zag-js/marquee': 1.35.2 - '@zag-js/menu': 1.35.2 - '@zag-js/navigation-menu': 1.35.2 - '@zag-js/number-input': 1.35.2 - '@zag-js/pagination': 1.35.2 - '@zag-js/password-input': 1.35.2 - '@zag-js/pin-input': 1.35.2 - '@zag-js/popover': 1.35.2 - '@zag-js/presence': 1.35.2 - '@zag-js/progress': 1.35.2 - '@zag-js/qr-code': 1.35.2 - '@zag-js/radio-group': 1.35.2 - '@zag-js/rating-group': 1.35.2 - '@zag-js/react': 1.35.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@zag-js/scroll-area': 1.35.2 - '@zag-js/select': 1.35.2 - '@zag-js/signature-pad': 1.35.2 - '@zag-js/slider': 1.35.2 - '@zag-js/splitter': 1.35.2 - '@zag-js/steps': 1.35.2 - '@zag-js/switch': 1.35.2 - '@zag-js/tabs': 1.35.2 - '@zag-js/tags-input': 1.35.2 - '@zag-js/timer': 1.35.2 - '@zag-js/toast': 1.35.2 - '@zag-js/toggle': 1.35.2 - '@zag-js/toggle-group': 1.35.2 - '@zag-js/tooltip': 1.35.2 - '@zag-js/tour': 1.35.2 - '@zag-js/tree-view': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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) @@ -3362,9 +3286,9 @@ 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.34.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 @@ -3561,16 +3485,16 @@ 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-fetch@0.4.0': {} @@ -3970,22 +3894,6 @@ snapshots: dependencies: '@types/node': 25.3.3 - '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(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: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.55.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/type-utils': 8.55.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.55.0 - eslint: 10.0.2(jiti@2.6.1) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@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: '@eslint-community/regexpp': 4.12.2 @@ -4002,18 +3910,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.55.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.55.0 - debug: 4.4.3 - eslint: 10.0.2(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.56.1 @@ -4026,15 +3922,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.55.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) - '@typescript-eslint/types': 8.55.0 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) @@ -4044,36 +3931,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.55.0': - dependencies: - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/visitor-keys': 8.55.0 - '@typescript-eslint/scope-manager@8.56.1': dependencies: '@typescript-eslint/types': 8.56.1 '@typescript-eslint/visitor-keys': 8.56.1 - '@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.55.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.0.2(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/type-utils@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.56.1 @@ -4086,25 +3952,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.55.0': {} - '@typescript-eslint/types@8.56.1': {} - '@typescript-eslint/typescript-estree@8.55.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.55.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/visitor-keys': 8.55.0 - debug: 4.4.3 - minimatch: 10.2.4 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) @@ -4120,17 +3969,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.55.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - eslint: 10.0.2(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) @@ -4142,11 +3980,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.55.0': - dependencies: - '@typescript-eslint/types': 8.55.0 - eslint-visitor-keys: 4.2.1 - '@typescript-eslint/visitor-keys@8.56.1': dependencies: '@typescript-eslint/types': 8.56.1 @@ -4160,7 +3993,7 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.3.3)(happy-dom@20.7.0)(jiti@2.6.1))': + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.3.3)(happy-dom@20.8.3)(jiti@2.6.1))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -4172,7 +4005,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.3.3)(happy-dom@20.7.0)(jiti@2.6.1) + vitest: 4.0.18(@types/node@25.3.3)(happy-dom@20.8.3)(jiti@2.6.1) '@vitest/expect@4.0.18': dependencies: @@ -4258,561 +4091,561 @@ snapshots: '@vue/shared@3.5.29': {} - '@zag-js/accordion@1.35.2': + '@zag-js/accordion@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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 - '@zag-js/anatomy@1.35.2': {} + '@zag-js/anatomy@1.35.3': {} - '@zag-js/angle-slider@1.35.2': + '@zag-js/angle-slider@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/rect-utils': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/aria-hidden@1.35.3': dependencies: - '@zag-js/dom-query': 1.35.2 + '@zag-js/dom-query': 1.35.3 - '@zag-js/async-list@1.35.2': + '@zag-js/async-list@1.35.3': dependencies: - '@zag-js/core': 1.35.2 - '@zag-js/utils': 1.35.2 + '@zag-js/core': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/auto-resize@1.35.2': + '@zag-js/auto-resize@1.35.3': dependencies: - '@zag-js/dom-query': 1.35.2 + '@zag-js/dom-query': 1.35.3 - '@zag-js/avatar@1.35.2': + '@zag-js/avatar@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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 - '@zag-js/carousel@1.35.2': + '@zag-js/carousel@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/scroll-snap': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/cascade-select@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/collection': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-visible': 1.35.2 - '@zag-js/popper': 1.35.2 - '@zag-js/rect-utils': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/checkbox@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-visible': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/clipboard@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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 - '@zag-js/collapsible@1.35.2': + '@zag-js/collapsible@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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 - '@zag-js/collection@1.35.2': + '@zag-js/collection@1.35.3': dependencies: - '@zag-js/utils': 1.35.2 + '@zag-js/utils': 1.35.3 - '@zag-js/color-picker@1.35.2': + '@zag-js/color-picker@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/color-utils': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/popper': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/color-utils@1.35.3': dependencies: - '@zag-js/utils': 1.35.2 + '@zag-js/utils': 1.35.3 - '@zag-js/combobox@1.35.2': + '@zag-js/combobox@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/aria-hidden': 1.35.2 - '@zag-js/collection': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-visible': 1.35.2 - '@zag-js/popper': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/core@1.35.3': dependencies: - '@zag-js/dom-query': 1.35.2 - '@zag-js/utils': 1.35.2 + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/date-picker@1.35.2(@internationalized/date@3.11.0)': + '@zag-js/date-picker@1.35.3(@internationalized/date@3.11.0)': dependencies: '@internationalized/date': 3.11.0 - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/date-utils': 1.35.2(@internationalized/date@3.11.0) - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/live-region': 1.35.2 - '@zag-js/popper': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/date-utils@1.35.2(@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.2': + '@zag-js/dialog@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/aria-hidden': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-trap': 1.35.2 - '@zag-js/remove-scroll': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/dismissable@1.35.3': dependencies: - '@zag-js/dom-query': 1.35.2 - '@zag-js/interact-outside': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/dom-query@1.35.3': dependencies: - '@zag-js/types': 1.35.2 + '@zag-js/types': 1.35.3 - '@zag-js/drawer@1.35.2': + '@zag-js/drawer@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/aria-hidden': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-trap': 1.35.2 - '@zag-js/remove-scroll': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/editable@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/interact-outside': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/file-upload@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/file-utils': 1.35.2 - '@zag-js/i18n-utils': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/file-utils@1.35.3': dependencies: - '@zag-js/i18n-utils': 1.35.2 + '@zag-js/i18n-utils': 1.35.3 - '@zag-js/floating-panel@1.35.2': + '@zag-js/floating-panel@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/popper': 1.35.2 - '@zag-js/rect-utils': 1.35.2 - '@zag-js/store': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/focus-trap@1.35.3': dependencies: - '@zag-js/dom-query': 1.35.2 + '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-visible@1.35.2': + '@zag-js/focus-visible@1.35.3': dependencies: - '@zag-js/dom-query': 1.35.2 + '@zag-js/dom-query': 1.35.3 - '@zag-js/highlight-word@1.35.2': {} + '@zag-js/highlight-word@1.35.3': {} - '@zag-js/hover-card@1.35.2': + '@zag-js/hover-card@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/popper': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/i18n-utils@1.35.3': dependencies: - '@zag-js/dom-query': 1.35.2 + '@zag-js/dom-query': 1.35.3 - '@zag-js/image-cropper@1.35.2': + '@zag-js/image-cropper@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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 - '@zag-js/interact-outside@1.35.2': + '@zag-js/interact-outside@1.35.3': dependencies: - '@zag-js/dom-query': 1.35.2 - '@zag-js/utils': 1.35.2 + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/json-tree-utils@1.35.2': {} + '@zag-js/json-tree-utils@1.35.3': {} - '@zag-js/listbox@1.35.2': + '@zag-js/listbox@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/collection': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-visible': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': {} + '@zag-js/live-region@1.35.3': {} - '@zag-js/marquee@1.35.2': + '@zag-js/marquee@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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 - '@zag-js/menu@1.35.2': + '@zag-js/menu@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-visible': 1.35.2 - '@zag-js/popper': 1.35.2 - '@zag-js/rect-utils': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/navigation-menu@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/number-input@1.35.3': dependencies: '@internationalized/number': 3.6.5 - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/pagination@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/password-input@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/pin-input@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/popover@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/aria-hidden': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-trap': 1.35.2 - '@zag-js/popper': 1.35.2 - '@zag-js/remove-scroll': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/popper@1.35.2': - dependencies: - '@floating-ui/dom': 1.7.5 - '@zag-js/dom-query': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/presence@1.35.2': - dependencies: - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - - '@zag-js/progress@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/qr-code@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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 + + '@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.2': + '@zag-js/radio-group@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-visible': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/rating-group@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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 - '@zag-js/react@1.35.2(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.35.2 - '@zag-js/store': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': {} + '@zag-js/rect-utils@1.35.3': {} - '@zag-js/remove-scroll@1.35.2': + '@zag-js/remove-scroll@1.35.3': dependencies: - '@zag-js/dom-query': 1.35.2 + '@zag-js/dom-query': 1.35.3 - '@zag-js/scroll-area@1.35.2': + '@zag-js/scroll-area@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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 - '@zag-js/scroll-snap@1.35.2': + '@zag-js/scroll-snap@1.35.3': dependencies: - '@zag-js/dom-query': 1.35.2 + '@zag-js/dom-query': 1.35.3 - '@zag-js/select@1.35.2': + '@zag-js/select@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/collection': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-visible': 1.35.2 - '@zag-js/popper': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 + '@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.2': + '@zag-js/signature-pad@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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.35.2': + '@zag-js/slider@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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 - '@zag-js/splitter@1.35.2': + '@zag-js/splitter@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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 - '@zag-js/steps@1.35.2': + '@zag-js/steps@1.35.3': dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.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 - '@zag-js/store@1.35.2': + '@zag-js/store@1.35.3': dependencies: proxy-compare: 3.0.1 - '@zag-js/switch@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-visible': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/tabs@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/tags-input@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/auto-resize': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/interact-outside': 1.35.2 - '@zag-js/live-region': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/timer@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/toast@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/toggle-group@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/toggle@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/tooltip@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-visible': 1.35.2 - '@zag-js/popper': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/tour@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dismissable': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/focus-trap': 1.35.2 - '@zag-js/interact-outside': 1.35.2 - '@zag-js/popper': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/tree-view@1.35.2': - dependencies: - '@zag-js/anatomy': 1.35.2 - '@zag-js/collection': 1.35.2 - '@zag-js/core': 1.35.2 - '@zag-js/dom-query': 1.35.2 - '@zag-js/types': 1.35.2 - '@zag-js/utils': 1.35.2 - - '@zag-js/types@1.35.2': + '@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.2': {} + '@zag-js/utils@1.35.3': {} acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -5633,7 +5466,7 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - happy-dom@20.7.0: + happy-dom@20.8.3: dependencies: '@types/node': 25.3.3 '@types/whatwg-mimetype': 3.0.2 @@ -6199,7 +6032,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-icons@5.5.0(react@19.2.4): + react-icons@5.6.0(react@19.2.4): dependencies: react: 19.2.4 @@ -6614,7 +6447,7 @@ snapshots: dependencies: react: 19.2.4 - vite-plugin-css-injected-by-js@3.5.2(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)): + vite-plugin-css-injected-by-js@4.0.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)): dependencies: vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1) @@ -6650,7 +6483,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 - vitest@4.0.18(@types/node@25.3.3)(happy-dom@20.7.0)(jiti@2.6.1): + vitest@4.0.18(@types/node@25.3.3)(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.3)(jiti@2.6.1)) @@ -6674,7 +6507,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.3.3 - happy-dom: 20.7.0 + happy-dom: 20.8.3 transitivePeerDependencies: - jiti - less From 26a5567f801501832532b686f4e719ab78b22672 Mon Sep 17 00:00:00 2001 From: Jens Scheffler <95105677+jscheffl@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:14:07 +0100 Subject: [PATCH 009/280] Fix dependabot fndings in providers registry (#63117) --- registry/pnpm-lock.yaml | 48 +++++++++++++++++++----------------- registry/pnpm-workspace.yaml | 21 ++++++++++++++++ 2 files changed, 46 insertions(+), 23 deletions(-) create mode 100644 registry/pnpm-workspace.yaml diff --git a/registry/pnpm-lock.yaml b/registry/pnpm-lock.yaml index d0ae9b1a3b5a7..b3421a7ede67d 100644 --- a/registry/pnpm-lock.yaml +++ b/registry/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + markdown-it@>=13.0.0 <14.1.1: '>=14.1.1' + minimatch@<3.1.3: '>=3.1.3' + minimatch@<3.1.4: '>=3.1.4' + importers: .: @@ -136,8 +141,9 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - 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} bcp-47-match@2.0.3: resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} @@ -152,8 +158,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + 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==} @@ -184,9 +191,6 @@ packages: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -412,8 +416,8 @@ packages: resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} engines: {node: '>=12'} - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} hasBin: true maximatch@0.1.0: @@ -436,8 +440,9 @@ packages: engines: {node: '>=10.0.0'} hasBin: true - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -690,7 +695,7 @@ snapshots: kleur: 4.1.5 liquidjs: 10.24.0 luxon: 3.7.2 - markdown-it: 14.1.0 + markdown-it: 14.1.1 minimist: 1.2.8 moo: 0.5.2 node-retrieve-globals: 6.0.1 @@ -781,7 +786,7 @@ snapshots: asap@2.0.6: {} - balanced-match@1.0.2: {} + balanced-match@4.0.4: {} bcp-47-match@2.0.3: {} @@ -798,10 +803,9 @@ snapshots: binary-extensions@2.3.0: {} - brace-expansion@1.1.12: + brace-expansion@5.0.4: dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 + balanced-match: 4.0.4 braces@3.0.3: dependencies: @@ -836,8 +840,6 @@ snapshots: commander@7.2.0: {} - concat-map@0.0.1: {} - debug@2.6.9: dependencies: ms: 2.0.0 @@ -940,7 +942,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 10.2.4 once: 1.4.0 path-is-absolute: 1.0.1 @@ -1029,7 +1031,7 @@ snapshots: luxon@3.7.2: {} - markdown-it@14.1.0: + markdown-it@14.1.1: dependencies: argparse: 2.0.1 entities: 4.5.0 @@ -1043,7 +1045,7 @@ snapshots: array-differ: 1.0.0 array-union: 1.0.2 arrify: 1.0.1 - minimatch: 3.1.2 + minimatch: 10.2.4 mdurl@2.0.0: {} @@ -1055,9 +1057,9 @@ snapshots: mime@3.0.0: {} - minimatch@3.1.2: + minimatch@10.2.4: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 5.0.4 minimist@1.2.8: {} diff --git a/registry/pnpm-workspace.yaml b/registry/pnpm-workspace.yaml new file mode 100644 index 0000000000000..198d76e0be7b1 --- /dev/null +++ b/registry/pnpm-workspace.yaml @@ -0,0 +1,21 @@ +# 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. +--- +overrides: + markdown-it@>=13.0.0 <14.1.1: '>=14.1.1' + minimatch@<3.1.3: '>=3.1.3' + minimatch@<3.1.4: '>=3.1.4' From c88b71570e359d9e9919d94530b4c6117b41bd23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:14:44 +0100 Subject: [PATCH 010/280] chore(deps): bump actions/github-script from 7.0.1 to 8.0.0 (#63097) Bumps [actions/github-script](https://github.com/actions/github-script) from 7.0.1 to 8.0.0. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/60a0d83039c74a4aee543508d2ffcb1c3799cdea...ed597411d8f924073f98dfc5c65a23a2325f34cd) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/automatic-backport.yml | 2 +- .github/workflows/milestone-tag-assistant.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/milestone-tag-assistant.yml b/.github/workflows/milestone-tag-assistant.yml index 18fd030e9b98a..8f5467fa7cde8 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: From ab5632a0bbb023ecac2e74f15921d2a9713ad25d Mon Sep 17 00:00:00 2001 From: Jens Scheffler <95105677+jscheffl@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:33:51 +0100 Subject: [PATCH 011/280] Add new Provider Registry to dependabot (#63118) --- .github/dependabot.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9c9a16c3cc2b5..06da7e4ed6c13 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -150,6 +150,27 @@ updates: patterns: - "*" + - package-ecosystem: npm + cooldown: + default-days: 4 + directories: + - /registry + schedule: + interval: daily + groups: + core-ui-package-updates: + patterns: + - "*" + update-types: + - "minor" + - "patch" + 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: From 7772f7c76a00b2998fa0bc4ab7f0c1d56662eb07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:43:42 +0100 Subject: [PATCH 012/280] chore(deps): bump actions/setup-go from 5.5.0 to 6.3.0 (#63133) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.5.0 to 6.3.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/d35c59abb061a4a6fb18e82ac0862c26744d6ab5...4b73464bb391d4059bd26b0524d20df3927bd417) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 6.3.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-amd-arm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-amd-arm.yml b/.github/workflows/ci-amd-arm.yml index 7a29f4b4c91f5..c06506f552030 100644 --- a/.github/workflows/ci-amd-arm.yml +++ b/.github/workflows/ci-amd-arm.yml @@ -835,7 +835,7 @@ jobs: 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 From 7fe4f3b18d620183d6b0fbd92ccc49584e18ab6a Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Sun, 8 Mar 2026 16:50:12 +0100 Subject: [PATCH 013/280] Add BaseXcom to airflow.sdk (#63116) --- task-sdk/src/airflow/sdk/__init__.py | 3 +++ task-sdk/src/airflow/sdk/__init__.pyi | 2 ++ 2 files changed, 5 insertions(+) diff --git a/task-sdk/src/airflow/sdk/__init__.py b/task-sdk/src/airflow/sdk/__init__.py index 669c87e019ae2..fb7410c320259 100644 --- a/task-sdk/src/airflow/sdk/__init__.py +++ b/task-sdk/src/airflow/sdk/__init__.py @@ -34,6 +34,7 @@ "BaseOperator", "BaseOperatorLink", "BaseSensorOperator", + "BaseXCom", "BranchMixIn", "Connection", "Context", @@ -108,6 +109,7 @@ from airflow.sdk.bases.operatorlink import BaseOperatorLink from airflow.sdk.bases.sensor import BaseSensorOperator, PokeReturnValue from airflow.sdk.bases.skipmixin import SkipMixin + from airflow.sdk.bases.xcom import BaseXCom from airflow.sdk.configuration import AirflowSDKConfigParser from airflow.sdk.definitions.asset import Asset, AssetAlias, AssetAll, AssetAny, AssetWatcher from airflow.sdk.definitions.asset.decorators import asset @@ -173,6 +175,7 @@ "BaseOperator": ".bases.operator", "BaseOperatorLink": ".bases.operatorlink", "BaseSensorOperator": ".bases.sensor", + "BaseXCom": ".bases.xcom", "BranchMixIn": ".bases.branch", "Connection": ".definitions.connection", "Context": ".definitions.context", diff --git a/task-sdk/src/airflow/sdk/__init__.pyi b/task-sdk/src/airflow/sdk/__init__.pyi index ed9943700b588..222b776c1aaad 100644 --- a/task-sdk/src/airflow/sdk/__init__.pyi +++ b/task-sdk/src/airflow/sdk/__init__.pyi @@ -40,6 +40,7 @@ from airflow.sdk.bases.sensor import ( PokeReturnValue as PokeReturnValue, ) from airflow.sdk.bases.skipmixin import SkipMixin as SkipMixin +from airflow.sdk.bases.xcom import BaseXCom as BaseXCom from airflow.sdk.configuration import AirflowSDKConfigParser from airflow.sdk.definitions.asset import ( Asset as Asset, @@ -112,6 +113,7 @@ __all__ = [ "BaseOperator", "BaseOperatorLink", "BaseSensorOperator", + "BaseXCom", "BranchMixIn", "Connection", "Context", From ee5c7d239af5d2e369626edc23e4a733fce5ac7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:21:39 +0100 Subject: [PATCH 014/280] chore(deps-dev): bump @types/react-dom (#63136) Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 18.3.7 to 19.0.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) --- updated-dependencies: - dependency-name: "@types/react-dom" dependency-version: 19.0.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../react_plugin_template/package.json | 2 +- .../react_plugin_template/pnpm-lock.yaml | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/dev/react-plugin-tools/react_plugin_template/package.json b/dev/react-plugin-tools/react_plugin_template/package.json index eca39944eebea..0370bf827d9c9 100644 --- a/dev/react-plugin-tools/react_plugin_template/package.json +++ b/dev/react-plugin-tools/react_plugin_template/package.json @@ -47,7 +47,7 @@ "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/node": "^22.15.3", "@types/react": "^18.3.19", - "@types/react-dom": "^18.3.5", + "@types/react-dom": "^19.0.1", "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/utils": "^8.56.1", diff --git a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml index 23e8cfcc7588a..90578a583819a 100644 --- a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml +++ b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml @@ -46,7 +46,7 @@ importers: version: 6.9.1 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.0.1)(@types/react@18.3.28)(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.8.1) @@ -57,8 +57,8 @@ importers: specifier: ^18.3.19 version: 18.3.28 '@types/react-dom': - specifier: ^18.3.5 - version: 18.3.7(@types/react@18.3.28) + specifier: ^19.0.1 + version: 19.0.1 '@typescript-eslint/eslint-plugin': specifier: 8.56.1 version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3) @@ -867,10 +867,8 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/react-dom@18.3.7': - resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} - peerDependencies: - '@types/react': ^18.0.0 + '@types/react-dom@19.0.1': + resolution: {integrity: sha512-hljHij7MpWPKF6u5vojuyfV0YA4YURsQG7KT6SzV0Zs2BXAtgdTxG6A229Ub/xiWV4w/7JL8fi6aAyjshH4meA==} '@types/react@18.3.28': resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} @@ -3606,7 +3604,7 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.0.1)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@babel/runtime': 7.28.6 '@testing-library/dom': 10.4.1 @@ -3614,7 +3612,7 @@ snapshots: react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 18.3.28 - '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@types/react-dom': 19.0.1 '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.1)': dependencies: @@ -3653,7 +3651,7 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@18.3.7(@types/react@18.3.28)': + '@types/react-dom@19.0.1': dependencies: '@types/react': 18.3.28 From f50bf2467afc6d4d0e7b469858d2b918a7c10861 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:24:34 +0100 Subject: [PATCH 015/280] chore(deps-dev): bump vite-plugin-css-injected-by-js (#63134) Bumps [vite-plugin-css-injected-by-js](https://github.com/marco-prontera/vite-plugin-css-injected-by-js) from 3.5.2 to 4.0.1. - [Release notes](https://github.com/marco-prontera/vite-plugin-css-injected-by-js/releases) - [Commits](https://github.com/marco-prontera/vite-plugin-css-injected-by-js/compare/v3.5.2...v4.0.1) --- updated-dependencies: - dependency-name: vite-plugin-css-injected-by-js dependency-version: 4.0.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../react_plugin_template/package.json | 2 +- .../react_plugin_template/pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev/react-plugin-tools/react_plugin_template/package.json b/dev/react-plugin-tools/react_plugin_template/package.json index 0370bf827d9c9..82b0845d5813a 100644 --- a/dev/react-plugin-tools/react_plugin_template/package.json +++ b/dev/react-plugin-tools/react_plugin_template/package.json @@ -68,7 +68,7 @@ "typescript": "~5.9.3", "typescript-eslint": "^8.56.1", "vite": "^7.1.11", - "vite-plugin-css-injected-by-js": "^3.5.2", + "vite-plugin-css-injected-by-js": "^4.0.1", "vite-plugin-dts": "^4.3.0", "vitest": "^3.2.4" }, diff --git a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml index 90578a583819a..d9c1086d36878 100644 --- a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml +++ b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml @@ -120,8 +120,8 @@ importers: specifier: ^7.1.11 version: 7.3.1(@types/node@22.19.11) vite-plugin-css-injected-by-js: - specifier: ^3.5.2 - version: 3.5.2(vite@7.3.1(@types/node@22.19.11)) + specifier: ^4.0.1 + version: 4.0.1(vite@7.3.1(@types/node@22.19.11)) vite-plugin-dts: specifier: ^4.3.0 version: 4.5.4(@types/node@22.19.11)(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.11)) @@ -2750,8 +2750,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-plugin-css-injected-by-js@3.5.2: - resolution: {integrity: sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==} + vite-plugin-css-injected-by-js@4.0.1: + resolution: {integrity: sha512-WfyRojojQyAO/KzWf+efcXpTPv6zJPXaRmr9EYq5a4v5I3PWCR7kR01hiri2lW6+rHm3a57kpwsf+iahIJi1Qw==} peerDependencies: vite: '>2.0.0-0' @@ -6163,7 +6163,7 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@3.5.2(vite@7.3.1(@types/node@22.19.11)): + vite-plugin-css-injected-by-js@4.0.1(vite@7.3.1(@types/node@22.19.11)): dependencies: vite: 7.3.1(@types/node@22.19.11) From 5ba88e36131a282af94c39373bceb269c61e8cd5 Mon Sep 17 00:00:00 2001 From: Justin Pakzad <114518232+justinpakzad@users.noreply.github.com> Date: Sun, 8 Mar 2026 13:25:39 -0400 Subject: [PATCH 016/280] Fix - S3GetBucketTaggingOperator ignoring aws_conn_id parameter (#63137) --- .../amazon/src/airflow/providers/amazon/aws/operators/s3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/amazon/src/airflow/providers/amazon/aws/operators/s3.py b/providers/amazon/src/airflow/providers/amazon/aws/operators/s3.py index d1e4f7848bbae..f4582dc6ba6f2 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/operators/s3.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/operators/s3.py @@ -150,7 +150,7 @@ class S3GetBucketTaggingOperator(AwsBaseOperator[S3Hook]): template_fields: Sequence[str] = aws_template_fields("bucket_name") aws_hook_class = S3Hook - def __init__(self, bucket_name: str, aws_conn_id: str | None = "aws_default", **kwargs) -> None: + def __init__(self, bucket_name: str, **kwargs) -> None: super().__init__(**kwargs) self.bucket_name = bucket_name From 251e8c805553c6b56821baac972a6245d9fa7f31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:26:13 +0100 Subject: [PATCH 017/280] chore(deps-dev): bump globals (#63127) Bumps [globals](https://github.com/sindresorhus/globals) from 15.15.0 to 17.4.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v15.15.0...v17.4.0) --- updated-dependencies: - dependency-name: globals dependency-version: 17.4.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../react_plugin_template/package.json | 2 +- .../react_plugin_template/pnpm-lock.yaml | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/dev/react-plugin-tools/react_plugin_template/package.json b/dev/react-plugin-tools/react_plugin_template/package.json index 82b0845d5813a..1a666c6a4ba21 100644 --- a/dev/react-plugin-tools/react_plugin_template/package.json +++ b/dev/react-plugin-tools/react_plugin_template/package.json @@ -62,7 +62,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-unicorn": "^55.0.0", - "globals": "^15.15.0", + "globals": "^17.4.0", "happy-dom": "^20.8.3", "prettier": "^3.5.3", "typescript": "~5.9.3", diff --git a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml index d9c1086d36878..b80043ff34d50 100644 --- a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml +++ b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml @@ -102,8 +102,8 @@ importers: specifier: ^55.0.0 version: 55.0.0(eslint@9.39.3) globals: - specifier: ^15.15.0 - version: 15.15.0 + specifier: ^17.4.0 + version: 17.4.0 happy-dom: specifier: ^20.8.3 version: 20.8.3 @@ -1830,6 +1830,10 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} + globals@17.4.0: + resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -5159,6 +5163,8 @@ snapshots: globals@15.15.0: {} + globals@17.4.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 From 9bb760946651c662f3d7bdce20ece0ada84557de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:33:04 +0100 Subject: [PATCH 018/280] chore(deps-dev): bump @eslint/js (#63130) Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.39.3 to 9.39.4. - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/commits/v9.39.4/packages/js) --- updated-dependencies: - dependency-name: "@eslint/js" dependency-version: 9.39.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../react_plugin_template/package.json | 2 +- .../react_plugin_template/pnpm-lock.yaml | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/dev/react-plugin-tools/react_plugin_template/package.json b/dev/react-plugin-tools/react_plugin_template/package.json index 1a666c6a4ba21..933b8e6d88c2b 100644 --- a/dev/react-plugin-tools/react_plugin_template/package.json +++ b/dev/react-plugin-tools/react_plugin_template/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@eslint/compat": "^1.2.9", - "@eslint/js": "^9.25.1", + "@eslint/js": "^9.39.4", "@stylistic/eslint-plugin": "^2.13.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", diff --git a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml index b80043ff34d50..e922a87562c63 100644 --- a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml +++ b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml @@ -36,8 +36,8 @@ importers: specifier: ^1.2.9 version: 1.4.1(eslint@9.39.3) '@eslint/js': - specifier: ^9.25.1 - version: 9.39.3 + specifier: ^9.39.4 + version: 9.39.4 '@stylistic/eslint-plugin': specifier: ^2.13.0 version: 2.13.0(eslint@9.39.3)(typescript@5.9.3) @@ -470,6 +470,10 @@ packages: resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@2.1.7': resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3290,6 +3294,8 @@ snapshots: '@eslint/js@9.39.3': {} + '@eslint/js@9.39.4': {} + '@eslint/object-schema@2.1.7': {} '@eslint/plugin-kit@0.4.1': From 516d6d72f4c1d5b8d9776378b72fbe3a0673c78b Mon Sep 17 00:00:00 2001 From: Asquator <92979391+Asquator@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:46:48 +0200 Subject: [PATCH 019/280] fix: Always provide a relevant TI context in Dag callback (#61274) * Skeleton solution * Handle more cases * Refactored None handling * Fixed task name * Materialized tasks for double iteration * Use default argument in min * Use default argument in max * Use default argument in max * Changed end_date to start_date * Fix logic for on_success_callback * Handle the case where ti has no start_date * Logic for DAG testing * Cosmetics * Simplified logic * Pass last succeeded TI on success * Added template method * Cosmetics * More cosmetics * Lint * Added default keys for mypy check to succeed * Updated the docs * Fixed timezone aware datetime comparison * Clarified Dag run timeouts in the docs * Fix condition to look for failures across all relevant TIs * Mypy & ruff * Timezone awareness for deadlocked tasks case * Select from all tis for success callback * Improved deadlock callback logic * Mypy & ruff * Made the test more explicit * Format * Updated backward docs version to 3.1.9 according to milestone * Revert "Updated backward docs version to 3.1.9 according to milestone" This reverts commit 4defea7c29c2c5b9c2a82c53b506247f52d3221e. * Type hint Thank you Co-authored-by: Kaxil Naik * Reapply "Updated backward docs version to 3.1.9 according to milestone" This reverts commit a31476a21162378ca1f228bdc439d148a3449db4. * Removed redundant iter() call * Removed redundant logic in deadlock case * Updated backward docs version to 3.2.0 according to milestone Co-authored-by: Elad Kalif <45845474+eladkal@users.noreply.github.com> * Added a newsfragment entry * Removed trailing whitespaces * Shortened nesfragment to just one line --------- Co-authored-by: TheoS Co-authored-by: Kaxil Naik Co-authored-by: Elad Kalif <45845474+eladkal@users.noreply.github.com> --- .../logging-monitoring/callbacks.rst | 60 +++-- .../newsfragments/61274.improvement.rst | 1 + .../src/airflow/jobs/scheduler_job_runner.py | 26 +-- airflow-core/src/airflow/models/dagrun.py | 149 ++++++------ .../tests/unit/jobs/test_scheduler_job.py | 4 +- airflow-core/tests/unit/models/test_dag.py | 8 +- airflow-core/tests/unit/models/test_dagrun.py | 213 ++++++++---------- 7 files changed, 234 insertions(+), 227 deletions(-) create mode 100644 airflow-core/newsfragments/61274.improvement.rst 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..25838377e5c25 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 ^^^^^^^^^^^^^^^ 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/src/airflow/jobs/scheduler_job_runner.py b/airflow-core/src/airflow/jobs/scheduler_job_runner.py index 367447968c2e0..9f9aa78cd74a1 100644 --- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py +++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py @@ -58,7 +58,6 @@ from airflow.assets.evaluation import AssetEvaluator from airflow.callbacks.callback_requests import ( DagCallbackRequest, - DagRunContext, EmailRequest, TaskCallbackRequest, ) @@ -2437,7 +2436,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 +2469,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") diff --git a/airflow-core/src/airflow/models/dagrun.py b/airflow-core/src/airflow/models/dagrun.py index 0de1a784fa409..4bc47dddea7ac 100644 --- a/airflow-core/src/airflow/models/dagrun.py +++ b/airflow-core/src/airflow/models/dagrun.py @@ -28,7 +28,6 @@ from uuid import UUID import structlog -from natsort import natsorted from sqlalchemy import ( JSON, Enum, @@ -1220,21 +1219,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 +1249,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,25 +1281,23 @@ 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 @@ -1417,27 +1408,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 +1450,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 +1475,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/tests/unit/jobs/test_scheduler_job.py b/airflow-core/tests/unit/jobs/test_scheduler_job.py index ae6388f1b8a01..57536416caba6 100644 --- a/airflow-core/tests/unit/jobs/test_scheduler_job.py +++ b/airflow-core/tests/unit/jobs/test_scheduler_job.py @@ -3524,7 +3524,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 +3720,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), ), ) 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..f3de13422fac3 100644 --- a/airflow-core/tests/unit/models/test_dagrun.py +++ b/airflow-core/tests/unit/models/test_dagrun.py @@ -23,7 +23,7 @@ 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 @@ -434,9 +434,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 +468,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" @@ -682,7 +738,7 @@ 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", ) @@ -732,7 +788,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 +1395,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 +3168,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 +3189,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 +3203,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 +3221,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 +3235,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 +3255,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,7 +3280,12 @@ 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 From 34278142cfb2f7106779c6738c9cf59f43194d2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:00:37 +0100 Subject: [PATCH 020/280] chore(deps-dev): bump the fab-ui-package-updates group across 1 directory with 3 updates (#63067) Bumps the fab-ui-package-updates group with 3 updates in the /providers/fab/src/airflow/providers/fab/www directory: [css-minimizer-webpack-plugin](https://github.com/webpack/css-minimizer-webpack-plugin), [terser-webpack-plugin](https://github.com/webpack/terser-webpack-plugin) and [webpack](https://github.com/webpack/webpack). Updates `css-minimizer-webpack-plugin` from 7.0.4 to 8.0.0 - [Release notes](https://github.com/webpack/css-minimizer-webpack-plugin/releases) - [Changelog](https://github.com/webpack/css-minimizer-webpack-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/webpack/css-minimizer-webpack-plugin/compare/v7.0.4...v8.0.0) Updates `terser-webpack-plugin` from 5.3.16 to 5.3.17 - [Release notes](https://github.com/webpack/terser-webpack-plugin/releases) - [Changelog](https://github.com/webpack/terser-webpack-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/webpack/terser-webpack-plugin/compare/v5.3.16...v5.3.17) Updates `webpack` from 5.105.3 to 5.105.4 - [Release notes](https://github.com/webpack/webpack/releases) - [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack/compare/v5.105.3...v5.105.4) --- updated-dependencies: - dependency-name: css-minimizer-webpack-plugin dependency-version: 8.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: fab-ui-package-updates - dependency-name: terser-webpack-plugin dependency-version: 5.3.17 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: fab-ui-package-updates - dependency-name: webpack dependency-version: 5.105.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: fab-ui-package-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../airflow/providers/fab/www/package.json | 6 +- .../airflow/providers/fab/www/pnpm-lock.yaml | 449 +++++++++--------- 2 files changed, 231 insertions(+), 224 deletions(-) diff --git a/providers/fab/src/airflow/providers/fab/www/package.json b/providers/fab/src/airflow/providers/fab/www/package.json index 4978f805dfa28..b3106458723b7 100644 --- a/providers/fab/src/airflow/providers/fab/www/package.json +++ b/providers/fab/src/airflow/providers/fab/www/package.json @@ -46,7 +46,7 @@ "babel-loader": "^10.0.0", "copy-webpack-plugin": "^14.0.0", "css-loader": "7.1.4", - "css-minimizer-webpack-plugin": "^7.0.4", + "css-minimizer-webpack-plugin": "^8.0.0", "eslint": "^10.0.2", "file-loader": "^6.2.0", "mini-css-extract-plugin": "^2.10.0", @@ -54,9 +54,9 @@ "moment-locales-webpack-plugin": "^1.2.0", "prettier": "^3.8.1", "stylelint": "^17.4.0", - "terser-webpack-plugin": "^5.3.16", + "terser-webpack-plugin": "^5.3.17", "url-loader": "4.1.1", - "webpack": "^5.105.3", + "webpack": "^5.105.4", "webpack-cli": "^6.0.1", "webpack-license-plugin": "^4.5.1", "webpack-manifest-plugin": "^6.0.1" diff --git a/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml b/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml index d4347260793f9..8bfe6c3596fa5 100644 --- a/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml +++ b/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml @@ -35,31 +35,31 @@ importers: version: 7.29.0(@babel/core@7.29.0) babel-loader: specifier: ^10.0.0 - version: 10.0.0(@babel/core@7.29.0)(webpack@5.105.3) + version: 10.0.0(@babel/core@7.29.0)(webpack@5.105.4) copy-webpack-plugin: specifier: ^14.0.0 - version: 14.0.0(webpack@5.105.3) + version: 14.0.0(webpack@5.105.4) css-loader: specifier: 7.1.4 - version: 7.1.4(webpack@5.105.3) + version: 7.1.4(webpack@5.105.4) css-minimizer-webpack-plugin: - specifier: ^7.0.4 - version: 7.0.4(webpack@5.105.3) + specifier: ^8.0.0 + version: 8.0.0(webpack@5.105.4) eslint: specifier: ^10.0.2 version: 10.0.2 file-loader: specifier: ^6.2.0 - version: 6.2.0(webpack@5.105.3) + version: 6.2.0(webpack@5.105.4) mini-css-extract-plugin: specifier: ^2.10.0 - version: 2.10.0(webpack@5.105.3) + version: 2.10.0(webpack@5.105.4) moment: specifier: ^2.30.1 version: 2.30.1 moment-locales-webpack-plugin: specifier: ^1.2.0 - version: 1.2.0(moment@2.30.1)(webpack@5.105.3) + version: 1.2.0(moment@2.30.1)(webpack@5.105.4) prettier: specifier: ^3.8.1 version: 3.8.1 @@ -67,23 +67,23 @@ importers: specifier: ^17.4.0 version: 17.4.0 terser-webpack-plugin: - specifier: ^5.3.16 - version: 5.3.16(webpack@5.105.3) + specifier: ^5.3.17 + version: 5.3.17(webpack@5.105.4) url-loader: specifier: 4.1.1 - version: 4.1.1(file-loader@6.2.0(webpack@5.105.3))(webpack@5.105.3) + version: 4.1.1(file-loader@6.2.0(webpack@5.105.4))(webpack@5.105.4) webpack: - specifier: ^5.105.3 - version: 5.105.3(webpack-cli@6.0.1) + specifier: ^5.105.4 + version: 5.105.4(webpack-cli@6.0.1) webpack-cli: specifier: ^6.0.1 - version: 6.0.1(webpack@5.105.3) + version: 6.0.1(webpack@5.105.4) webpack-license-plugin: specifier: ^4.5.1 - version: 4.5.1(webpack@5.105.3) + version: 4.5.1(webpack@5.105.4) webpack-manifest-plugin: specifier: ^6.0.1 - version: 6.0.1(webpack@5.105.3) + version: 6.0.1(webpack@5.105.4) packages: @@ -772,8 +772,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@25.2.3': - resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} + '@types/node@25.3.5': + resolution: {integrity: sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==} '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -987,8 +987,8 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001776: - resolution: {integrity: sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==} + caniuse-lite@1.0.30001777: + resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -1081,9 +1081,9 @@ packages: webpack: optional: true - css-minimizer-webpack-plugin@7.0.4: - resolution: {integrity: sha512-2iACis+P8qdLj1tHcShtztkGhCNIRUajJj7iX0IM9a5FA0wXGwjV8Nf6+HsBjBfb4LO8TTAVoetBbM54V6f3+Q==} - engines: {node: '>= 18.12.0'} + css-minimizer-webpack-plugin@8.0.0: + resolution: {integrity: sha512-9bEpzHs8gEq6/cbEj418jXL/YWjBUD2YTLLk905Npt2JODqnRITin0+So5Vx4Dp5vyi2Lpt9pp2QHzQ7fdxNrw==} + engines: {node: '>= 20.9.0'} peerDependencies: '@parcel/css': '*' '@swc/css': '*' @@ -1117,6 +1117,10 @@ packages: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} @@ -1126,8 +1130,8 @@ packages: engines: {node: '>=4'} hasBin: true - cssnano-preset-default@7.0.10: - resolution: {integrity: sha512-6ZBjW0Lf1K1Z+0OKUAUpEN62tSXmYChXWi2NAA0afxEVsj9a+MbcB1l5qel6BHJHmULai2fCGRthCeKSFbScpA==} + cssnano-preset-default@7.0.11: + resolution: {integrity: sha512-waWlAMuCakP7//UCY+JPrQS1z0OSLeOXk2sKWJximKWGupVxre50bzPlvpbUwZIDylhf/ptf0Pk+Yf7C+hoa3g==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 @@ -1138,8 +1142,8 @@ packages: peerDependencies: postcss: ^8.4.32 - cssnano@7.1.2: - resolution: {integrity: sha512-HYOPBsNvoiFeR1eghKD5C3ASm64v9YVyJB4Ivnl2gqKoQYvjjN/G0rztvKQq8OxocUtC6sjqY8jwYngIB4AByA==} + cssnano@7.1.3: + resolution: {integrity: sha512-mLFHQAzyapMVFLiJIn7Ef4C2UCEvtlTlbyILR6B5ZsUAV3D/Pa761R5uC1YPhyBkRd3eqaDm2ncaNrD7R4mTRg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 @@ -1610,6 +1614,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + meow@14.1.0: resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} engines: {node: '>=20'} @@ -1674,8 +1681,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -1748,20 +1755,20 @@ packages: peerDependencies: postcss: ^8.4.38 - postcss-colormin@7.0.5: - resolution: {integrity: sha512-ekIBP/nwzRWhEMmIxHHbXHcMdzd1HIUzBECaj5KEdLz9DVP2HzT065sEhvOx1dkLjYW7jyD0CngThx6bpFi2fA==} + postcss-colormin@7.0.6: + resolution: {integrity: sha512-oXM2mdx6IBTRm39797QguYzVEWzbdlFiMNfq88fCCN1Wepw3CYmJ/1/Ifa/KjWo+j5ZURDl2NTldLJIw51IeNQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 - postcss-convert-values@7.0.8: - resolution: {integrity: sha512-+XNKuPfkHTCEo499VzLMYn94TiL3r9YqRE3Ty+jP7UX4qjewUONey1t7CG21lrlTLN07GtGM8MqFVp86D4uKJg==} + postcss-convert-values@7.0.9: + resolution: {integrity: sha512-l6uATQATZaCa0bckHV+r6dLXfWtUBKXxO3jK+AtxxJJtgMPD+VhhPCCx51I4/5w8U5uHV67g3w7PXj+V3wlMlg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 - postcss-discard-comments@7.0.5: - resolution: {integrity: sha512-IR2Eja8WfYgN5n32vEGSctVQ1+JARfu4UH8M7bgGh1bC+xI/obsPJXaBpQF7MAByvgwZinhpHpdrmXtvVVlKcQ==} + postcss-discard-comments@7.0.6: + resolution: {integrity: sha512-Sq+Fzj1Eg5/CPf1ERb0wS1Im5cvE2gDXCE+si4HCn1sf+jpQZxDI4DXEp8t77B/ImzDceWE2ebJQFXdqZ6GRJw==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 @@ -1790,8 +1797,8 @@ packages: peerDependencies: postcss: ^8.4.32 - postcss-merge-rules@7.0.7: - resolution: {integrity: sha512-njWJrd/Ms6XViwowaaCc+/vqhPG3SmXn725AGrnl+BgTuRPEacjiLEaGq16J6XirMJbtKkTwnt67SS+e2WGoew==} + postcss-merge-rules@7.0.8: + resolution: {integrity: sha512-BOR1iAM8jnr7zoQSlpeBmCsWV5Uudi/+5j7k05D0O/WP3+OFMPD86c1j/20xiuRtyt45bhxw/7hnhZNhW2mNFA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 @@ -1808,14 +1815,14 @@ packages: peerDependencies: postcss: ^8.4.32 - postcss-minify-params@7.0.5: - resolution: {integrity: sha512-FGK9ky02h6Ighn3UihsyeAH5XmLEE2MSGH5Tc4tXMFtEDx7B+zTG6hD/+/cT+fbF7PbYojsmmWjyTwFwW1JKQQ==} + postcss-minify-params@7.0.6: + resolution: {integrity: sha512-YOn02gC68JijlaXVuKvFSCvQOhTpblkcfDre2hb/Aaa58r2BIaK4AtE/cyZf2wV7YKAG+UlP9DT+By0ry1E4VQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 - postcss-minify-selectors@7.0.5: - resolution: {integrity: sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug==} + postcss-minify-selectors@7.0.6: + resolution: {integrity: sha512-lIbC0jy3AAwDxEgciZlBullDiMBeBCT+fz5G8RcA9MWqh/hfUkpOI3vNDUNEZHgokaoiv0juB9Y8fGcON7rU/A==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 @@ -1880,8 +1887,8 @@ packages: peerDependencies: postcss: ^8.4.32 - postcss-normalize-unicode@7.0.5: - resolution: {integrity: sha512-X6BBwiRxVaFHrb2WyBMddIeB5HBjJcAaUHyhLrM2FsxSq5TFqcHSsK7Zu1otag+o0ZphQGJewGH1tAyrD0zX1Q==} + postcss-normalize-unicode@7.0.6: + resolution: {integrity: sha512-z6bwTV84YW6ZvvNoaNLuzRW4/uWxDKYI1iIDrzk6D2YTL7hICApy+Q1LP6vBEsljX8FM7YSuV9qI79XESd4ddQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 @@ -1904,8 +1911,8 @@ packages: peerDependencies: postcss: ^8.4.32 - postcss-reduce-initial@7.0.5: - resolution: {integrity: sha512-RHagHLidG8hTZcnr4FpyMB2jtgd/OcyAazjMhoy5qmWJOx1uxKh4ntk0Pb46ajKM0rkf32lRH4C8c9qQiPR6IA==} + postcss-reduce-initial@7.0.6: + resolution: {integrity: sha512-G6ZyK68AmrPdMB6wyeA37ejnnRG2S8xinJrZJnOv+IaRKf6koPAVbQsiC7MfkmXaGmF1UO+QCijb27wfpxuRNg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 @@ -1926,14 +1933,14 @@ packages: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} - postcss-svgo@7.1.0: - resolution: {integrity: sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w==} + postcss-svgo@7.1.1: + resolution: {integrity: sha512-zU9H9oEDrUFKa0JB7w+IYL7Qs9ey1mZyjhbf0KLxwJDdDRtoPvCmaEfknzqfHj44QS9VD6c5sJnBAVYTLRg/Sg==} engines: {node: ^18.12.0 || ^20.9.0 || >= 18} peerDependencies: postcss: ^8.4.32 - postcss-unique-selectors@7.0.4: - resolution: {integrity: sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ==} + postcss-unique-selectors@7.0.5: + resolution: {integrity: sha512-3QoYmEt4qg/rUWDn6Tc8+ZVPmbp4G1hXDtCNWDx0st8SjtCbRcxRXDDM1QrEiXGG3A45zscSJFb4QH90LViyxg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 @@ -2047,10 +2054,6 @@ packages: engines: {node: '>=10'} hasBin: true - serialize-javascript@7.0.3: - resolution: {integrity: sha512-h+cZ/XXarqDgCjo+YSyQU/ulDEESGGf8AMK9pPNmhNSl/FzPl6L8pMp1leca5z6NuG6tvV/auC8/43tmovowww==} - engines: {node: '>=20.0.0'} - serialize-javascript@7.0.4: resolution: {integrity: sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==} engines: {node: '>=20.0.0'} @@ -2118,8 +2121,8 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} - stylehacks@7.0.7: - resolution: {integrity: sha512-bJkD0JkEtbRrMFtwgpJyBbFIwfDDONQ1Ov3sDLZQP8HuJ73kBOyx66H4bOcAbVWmnfLdvQ0AJwXxOMkpujcO6g==} + stylehacks@7.0.8: + resolution: {integrity: sha512-I3f053GBLIiS5Fg6OMFhq/c+yW+5Hc2+1fgq7gElDMMSqwlRb3tBf2ef6ucLStYRpId4q//bQO1FjcyNyy4yDQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 @@ -2165,8 +2168,8 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - terser-webpack-plugin@5.3.16: - resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} + terser-webpack-plugin@5.3.17: + resolution: {integrity: sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -2198,8 +2201,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} @@ -2281,8 +2284,8 @@ packages: resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} engines: {node: '>=10.13.0'} - webpack@5.105.3: - resolution: {integrity: sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==} + webpack@5.105.4: + resolution: {integrity: sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -3075,7 +3078,7 @@ snapshots: '@jest/pattern@30.0.1': dependencies: - '@types/node': 25.2.3 + '@types/node': 25.3.5 jest-regex-util: 30.0.1 '@jest/schemas@30.0.5': @@ -3088,7 +3091,7 @@ snapshots: '@jest/schemas': 30.0.5 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 25.2.3 + '@types/node': 25.3.5 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -3170,9 +3173,9 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@25.2.3': + '@types/node@25.3.5': dependencies: - undici-types: 7.16.0 + undici-types: 7.18.2 '@types/yargs-parser@21.0.3': {} @@ -3258,20 +3261,20 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.105.3)': + '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': dependencies: - webpack: 5.105.3(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.3) + webpack: 5.105.4(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.105.4) - '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.105.3)': + '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': dependencies: - webpack: 5.105.3(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.3) + webpack: 5.105.4(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.105.4) - '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.105.3)': + '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': dependencies: - webpack: 5.105.3(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.3) + webpack: 5.105.4(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.105.4) '@xtuc/ieee754@1.2.0': {} @@ -3326,11 +3329,11 @@ snapshots: astral-regex@2.0.0: {} - babel-loader@10.0.0(@babel/core@7.29.0)(webpack@5.105.3): + babel-loader@10.0.0(@babel/core@7.29.0)(webpack@5.105.4): dependencies: '@babel/core': 7.29.0 find-up: 5.0.0 - webpack: 5.105.3(webpack-cli@6.0.1) + webpack: 5.105.4(webpack-cli@6.0.1) babel-plugin-polyfill-corejs2@0.4.15(@babel/core@7.29.0): dependencies: @@ -3383,9 +3386,9 @@ snapshots: browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.10.0 - caniuse-lite: 1.0.30001776 + caniuse-lite: 1.0.30001777 electron-to-chromium: 1.5.307 - node-releases: 2.0.27 + node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) buffer-from@1.1.2: {} @@ -3403,11 +3406,11 @@ snapshots: caniuse-api@3.0.0: dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001776 + caniuse-lite: 1.0.30001777 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001776: {} + caniuse-lite@1.0.30001777: {} chalk@4.1.2: dependencies: @@ -3444,14 +3447,14 @@ snapshots: convert-source-map@2.0.0: {} - copy-webpack-plugin@14.0.0(webpack@5.105.3): + copy-webpack-plugin@14.0.0(webpack@5.105.4): dependencies: glob-parent: 6.0.2 normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 7.0.4 tinyglobby: 0.2.15 - webpack: 5.105.3(webpack-cli@6.0.1) + webpack: 5.105.4(webpack-cli@6.0.1) core-js-compat@3.48.0: dependencies: @@ -3470,13 +3473,13 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-declaration-sorter@7.3.1(postcss@8.5.6): + css-declaration-sorter@7.3.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 css-functions-list@3.3.3: {} - css-loader@7.1.4(webpack@5.105.3): + css-loader@7.1.4(webpack@5.105.4): dependencies: icss-utils: 5.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -3487,17 +3490,17 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: - webpack: 5.105.3(webpack-cli@6.0.1) + webpack: 5.105.4(webpack-cli@6.0.1) - css-minimizer-webpack-plugin@7.0.4(webpack@5.105.3): + css-minimizer-webpack-plugin@8.0.0(webpack@5.105.4): dependencies: '@jridgewell/trace-mapping': 0.3.31 - cssnano: 7.1.2(postcss@8.5.6) + cssnano: 7.1.3(postcss@8.5.8) jest-worker: 30.2.0 - postcss: 8.5.6 + postcss: 8.5.8 schema-utils: 4.3.3 - serialize-javascript: 7.0.3 - webpack: 5.105.3(webpack-cli@6.0.1) + serialize-javascript: 7.0.4 + webpack: 5.105.4(webpack-cli@6.0.1) css-select@5.2.2: dependencies: @@ -3517,53 +3520,58 @@ snapshots: mdn-data: 2.12.2 source-map-js: 1.2.1 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + css-what@6.2.2: {} cssesc@3.0.0: {} - cssnano-preset-default@7.0.10(postcss@8.5.6): + cssnano-preset-default@7.0.11(postcss@8.5.8): dependencies: browserslist: 4.28.1 - css-declaration-sorter: 7.3.1(postcss@8.5.6) - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 - postcss-calc: 10.1.1(postcss@8.5.6) - postcss-colormin: 7.0.5(postcss@8.5.6) - postcss-convert-values: 7.0.8(postcss@8.5.6) - postcss-discard-comments: 7.0.5(postcss@8.5.6) - postcss-discard-duplicates: 7.0.2(postcss@8.5.6) - postcss-discard-empty: 7.0.1(postcss@8.5.6) - postcss-discard-overridden: 7.0.1(postcss@8.5.6) - postcss-merge-longhand: 7.0.5(postcss@8.5.6) - postcss-merge-rules: 7.0.7(postcss@8.5.6) - postcss-minify-font-values: 7.0.1(postcss@8.5.6) - postcss-minify-gradients: 7.0.1(postcss@8.5.6) - postcss-minify-params: 7.0.5(postcss@8.5.6) - postcss-minify-selectors: 7.0.5(postcss@8.5.6) - postcss-normalize-charset: 7.0.1(postcss@8.5.6) - postcss-normalize-display-values: 7.0.1(postcss@8.5.6) - postcss-normalize-positions: 7.0.1(postcss@8.5.6) - postcss-normalize-repeat-style: 7.0.1(postcss@8.5.6) - postcss-normalize-string: 7.0.1(postcss@8.5.6) - postcss-normalize-timing-functions: 7.0.1(postcss@8.5.6) - postcss-normalize-unicode: 7.0.5(postcss@8.5.6) - postcss-normalize-url: 7.0.1(postcss@8.5.6) - postcss-normalize-whitespace: 7.0.1(postcss@8.5.6) - postcss-ordered-values: 7.0.2(postcss@8.5.6) - postcss-reduce-initial: 7.0.5(postcss@8.5.6) - postcss-reduce-transforms: 7.0.1(postcss@8.5.6) - postcss-svgo: 7.1.0(postcss@8.5.6) - postcss-unique-selectors: 7.0.4(postcss@8.5.6) - - cssnano-utils@5.0.1(postcss@8.5.6): + css-declaration-sorter: 7.3.1(postcss@8.5.8) + cssnano-utils: 5.0.1(postcss@8.5.8) + postcss: 8.5.8 + postcss-calc: 10.1.1(postcss@8.5.8) + postcss-colormin: 7.0.6(postcss@8.5.8) + postcss-convert-values: 7.0.9(postcss@8.5.8) + postcss-discard-comments: 7.0.6(postcss@8.5.8) + postcss-discard-duplicates: 7.0.2(postcss@8.5.8) + postcss-discard-empty: 7.0.1(postcss@8.5.8) + postcss-discard-overridden: 7.0.1(postcss@8.5.8) + postcss-merge-longhand: 7.0.5(postcss@8.5.8) + postcss-merge-rules: 7.0.8(postcss@8.5.8) + postcss-minify-font-values: 7.0.1(postcss@8.5.8) + postcss-minify-gradients: 7.0.1(postcss@8.5.8) + postcss-minify-params: 7.0.6(postcss@8.5.8) + postcss-minify-selectors: 7.0.6(postcss@8.5.8) + postcss-normalize-charset: 7.0.1(postcss@8.5.8) + postcss-normalize-display-values: 7.0.1(postcss@8.5.8) + postcss-normalize-positions: 7.0.1(postcss@8.5.8) + postcss-normalize-repeat-style: 7.0.1(postcss@8.5.8) + postcss-normalize-string: 7.0.1(postcss@8.5.8) + postcss-normalize-timing-functions: 7.0.1(postcss@8.5.8) + postcss-normalize-unicode: 7.0.6(postcss@8.5.8) + postcss-normalize-url: 7.0.1(postcss@8.5.8) + postcss-normalize-whitespace: 7.0.1(postcss@8.5.8) + postcss-ordered-values: 7.0.2(postcss@8.5.8) + postcss-reduce-initial: 7.0.6(postcss@8.5.8) + postcss-reduce-transforms: 7.0.1(postcss@8.5.8) + postcss-svgo: 7.1.1(postcss@8.5.8) + postcss-unique-selectors: 7.0.5(postcss@8.5.8) + + cssnano-utils@5.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - cssnano@7.1.2(postcss@8.5.6): + cssnano@7.1.3(postcss@8.5.8): dependencies: - cssnano-preset-default: 7.0.10(postcss@8.5.6) + cssnano-preset-default: 7.0.11(postcss@8.5.8) lilconfig: 3.1.3 - postcss: 8.5.6 + postcss: 8.5.8 csso@5.0.5: dependencies: @@ -3729,11 +3737,11 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-loader@6.2.0(webpack@5.105.3): + file-loader@6.2.0(webpack@5.105.4): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.105.3(webpack-cli@6.0.1) + webpack: 5.105.4(webpack-cli@6.0.1) fill-range@7.1.1: dependencies: @@ -3882,7 +3890,7 @@ snapshots: jest-util@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 25.2.3 + '@types/node': 25.3.5 chalk: 4.1.2 ci-info: 4.4.0 graceful-fs: 4.2.11 @@ -3890,13 +3898,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 25.2.3 + '@types/node': 25.3.5 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@30.2.0: dependencies: - '@types/node': 25.2.3 + '@types/node': 25.3.5 '@ungap/structured-clone': 1.3.0 jest-util: 30.2.0 merge-stream: 2.0.0 @@ -3985,6 +3993,8 @@ snapshots: mdn-data@2.12.2: {} + mdn-data@2.27.1: {} + meow@14.1.0: {} merge-stream@2.0.0: {} @@ -4002,21 +4012,21 @@ snapshots: dependencies: mime-db: 1.52.0 - mini-css-extract-plugin@2.10.0(webpack@5.105.3): + mini-css-extract-plugin@2.10.0(webpack@5.105.4): dependencies: schema-utils: 4.3.3 tapable: 2.3.0 - webpack: 5.105.3(webpack-cli@6.0.1) + webpack: 5.105.4(webpack-cli@6.0.1) minimatch@10.2.4: dependencies: brace-expansion: 5.0.4 - moment-locales-webpack-plugin@1.2.0(moment@2.30.1)(webpack@5.105.3): + moment-locales-webpack-plugin@1.2.0(moment@2.30.1)(webpack@5.105.4): dependencies: lodash.difference: 4.5.0 moment: 2.30.1 - webpack: 5.105.3(webpack-cli@6.0.1) + webpack: 5.105.4(webpack-cli@6.0.1) moment-timezone@0.6.0: dependencies: @@ -4037,7 +4047,7 @@ snapshots: neo-async@2.6.2: {} - node-releases@2.0.27: {} + node-releases@2.0.36: {} normalize-path@3.0.0: {} @@ -4099,80 +4109,80 @@ snapshots: dependencies: find-up: 4.1.0 - postcss-calc@10.1.1(postcss@8.5.6): + postcss-calc@10.1.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-colormin@7.0.5(postcss@8.5.6): + postcss-colormin@7.0.6(postcss@8.5.8): dependencies: browserslist: 4.28.1 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-convert-values@7.0.8(postcss@8.5.6): + postcss-convert-values@7.0.9(postcss@8.5.8): dependencies: browserslist: 4.28.1 - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-discard-comments@7.0.5(postcss@8.5.6): + postcss-discard-comments@7.0.6(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 7.1.1 - postcss-discard-duplicates@7.0.2(postcss@8.5.6): + postcss-discard-duplicates@7.0.2(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - postcss-discard-empty@7.0.1(postcss@8.5.6): + postcss-discard-empty@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - postcss-discard-overridden@7.0.1(postcss@8.5.6): + postcss-discard-overridden@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - postcss-merge-longhand@7.0.5(postcss@8.5.6): + postcss-merge-longhand@7.0.5(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - stylehacks: 7.0.7(postcss@8.5.6) + stylehacks: 7.0.8(postcss@8.5.8) - postcss-merge-rules@7.0.7(postcss@8.5.6): + postcss-merge-rules@7.0.8(postcss@8.5.8): dependencies: browserslist: 4.28.1 caniuse-api: 3.0.0 - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 5.0.1(postcss@8.5.8) + postcss: 8.5.8 postcss-selector-parser: 7.1.1 - postcss-minify-font-values@7.0.1(postcss@8.5.6): + postcss-minify-font-values@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-minify-gradients@7.0.1(postcss@8.5.6): + postcss-minify-gradients@7.0.1(postcss@8.5.8): dependencies: colord: 2.9.3 - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 5.0.1(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-minify-params@7.0.5(postcss@8.5.6): + postcss-minify-params@7.0.6(postcss@8.5.8): dependencies: browserslist: 4.28.1 - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 5.0.1(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-minify-selectors@7.0.5(postcss@8.5.6): + postcss-minify-selectors@7.0.6(postcss@8.5.8): dependencies: cssesc: 3.0.0 - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 7.1.1 postcss-modules-extract-imports@3.1.0(postcss@8.5.6): @@ -4196,66 +4206,66 @@ snapshots: icss-utils: 5.1.0(postcss@8.5.6) postcss: 8.5.6 - postcss-normalize-charset@7.0.1(postcss@8.5.6): + postcss-normalize-charset@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - postcss-normalize-display-values@7.0.1(postcss@8.5.6): + postcss-normalize-display-values@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-positions@7.0.1(postcss@8.5.6): + postcss-normalize-positions@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@7.0.1(postcss@8.5.6): + postcss-normalize-repeat-style@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-string@7.0.1(postcss@8.5.6): + postcss-normalize-string@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@7.0.1(postcss@8.5.6): + postcss-normalize-timing-functions@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@7.0.5(postcss@8.5.6): + postcss-normalize-unicode@7.0.6(postcss@8.5.8): dependencies: browserslist: 4.28.1 - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-url@7.0.1(postcss@8.5.6): + postcss-normalize-url@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@7.0.1(postcss@8.5.6): + postcss-normalize-whitespace@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-ordered-values@7.0.2(postcss@8.5.6): + postcss-ordered-values@7.0.2(postcss@8.5.8): dependencies: - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 5.0.1(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-reduce-initial@7.0.5(postcss@8.5.6): + postcss-reduce-initial@7.0.6(postcss@8.5.8): dependencies: browserslist: 4.28.1 caniuse-api: 3.0.0 - postcss: 8.5.6 + postcss: 8.5.8 - postcss-reduce-transforms@7.0.1(postcss@8.5.6): + postcss-reduce-transforms@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 postcss-safe-parser@7.0.1(postcss@8.5.8): @@ -4267,15 +4277,15 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@7.1.0(postcss@8.5.6): + postcss-svgo@7.1.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 svgo: 4.0.1 - postcss-unique-selectors@7.0.4(postcss@8.5.6): + postcss-unique-selectors@7.0.5(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 7.1.1 postcss-value-parser@4.2.0: {} @@ -4374,8 +4384,6 @@ snapshots: semver@7.7.4: {} - serialize-javascript@7.0.3: {} - serialize-javascript@7.0.4: {} shallow-clone@3.0.1: @@ -4439,10 +4447,10 @@ snapshots: dependencies: ansi-regex: 6.2.2 - stylehacks@7.0.7(postcss@8.5.6): + stylehacks@7.0.8(postcss@8.5.8): dependencies: browserslist: 4.28.1 - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 7.1.1 stylelint@17.4.0: @@ -4511,7 +4519,7 @@ snapshots: dependencies: commander: 11.1.0 css-select: 5.2.2 - css-tree: 3.1.0 + css-tree: 3.2.1 css-what: 6.2.2 csso: 5.0.5 picocolors: 1.1.1 @@ -4527,14 +4535,13 @@ snapshots: tapable@2.3.0: {} - terser-webpack-plugin@5.3.16(webpack@5.105.3): + terser-webpack-plugin@5.3.17(webpack@5.105.4): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - serialize-javascript: 7.0.3 terser: 5.46.0 - webpack: 5.105.3(webpack-cli@6.0.1) + webpack: 5.105.4(webpack-cli@6.0.1) terser@5.46.0: dependencies: @@ -4556,7 +4563,7 @@ snapshots: dependencies: prelude-ls: 1.2.1 - undici-types@7.16.0: {} + undici-types@7.18.2: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -4581,14 +4588,14 @@ snapshots: dependencies: punycode: 2.3.1 - url-loader@4.1.1(file-loader@6.2.0(webpack@5.105.3))(webpack@5.105.3): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.105.4))(webpack@5.105.4): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.105.3(webpack-cli@6.0.1) + webpack: 5.105.4(webpack-cli@6.0.1) optionalDependencies: - file-loader: 6.2.0(webpack@5.105.3) + file-loader: 6.2.0(webpack@5.105.4) util-deprecate@1.0.2: {} @@ -4597,12 +4604,12 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 - webpack-cli@6.0.1(webpack@5.105.3): + webpack-cli@6.0.1(webpack@5.105.4): dependencies: '@discoveryjs/json-ext': 0.6.3 - '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.3) - '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.3) - '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.3) + '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) + '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) + '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) colorette: 2.0.20 commander: 12.1.0 cross-spawn: 7.0.6 @@ -4611,22 +4618,22 @@ snapshots: import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.105.3(webpack-cli@6.0.1) + webpack: 5.105.4(webpack-cli@6.0.1) webpack-merge: 6.0.1 - webpack-license-plugin@4.5.1(webpack@5.105.3): + webpack-license-plugin@4.5.1(webpack@5.105.4): dependencies: chalk: 5.6.2 lodash: 4.17.23 needle: 3.3.1 spdx-expression-validate: 2.0.0 - webpack: 5.105.3(webpack-cli@6.0.1) + webpack: 5.105.4(webpack-cli@6.0.1) webpack-sources: 3.3.4 - webpack-manifest-plugin@6.0.1(webpack@5.105.3): + webpack-manifest-plugin@6.0.1(webpack@5.105.4): dependencies: tapable: 2.3.0 - webpack: 5.105.3(webpack-cli@6.0.1) + webpack: 5.105.4(webpack-cli@6.0.1) webpack-sources: 3.3.4 webpack-merge@6.0.1: @@ -4637,7 +4644,7 @@ snapshots: webpack-sources@3.3.4: {} - webpack@5.105.3(webpack-cli@6.0.1): + webpack@5.105.4(webpack-cli@6.0.1): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -4661,11 +4668,11 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(webpack@5.105.3) + terser-webpack-plugin: 5.3.17(webpack@5.105.4) watchpack: 2.5.1 webpack-sources: 3.3.4 optionalDependencies: - webpack-cli: 6.0.1(webpack@5.105.3) + webpack-cli: 6.0.1(webpack@5.105.4) transitivePeerDependencies: - '@swc/core' - esbuild From a0d6418436d9441aa257d01511ca3b8234f2c57a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:02:04 +0100 Subject: [PATCH 021/280] chore(deps): bump actions/download-artifact from 4.3.0 to 8.0.0 (#63065) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.3.0 to 8.0.0. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/d3f86a106a0bac45b974a628896c90dbdf5c8093...70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-amd-arm.yml | 2 +- .github/workflows/ci-image-checks.yml | 2 +- .github/workflows/finalize-tests.yml | 2 +- .github/workflows/prod-image-build.yml | 12 ++++++------ .github/workflows/publish-docs-to-s3.yml | 2 +- .github/workflows/push-image-cache.yml | 2 +- .github/workflows/release_single_dockerhub_image.yml | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-amd-arm.yml b/.github/workflows/ci-amd-arm.yml index c06506f552030..fe125c2c2dfde 100644 --- a/.github/workflows/ci-amd-arm.yml +++ b/.github/workflows/ci-amd-arm.yml @@ -978,7 +978,7 @@ jobs: 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-* diff --git a/.github/workflows/ci-image-checks.yml b/.github/workflows/ci-image-checks.yml index 585ac16a5b88f..1af065e80f664 100644 --- a/.github/workflows/ci-image-checks.yml +++ b/.github/workflows/ci-image-checks.yml @@ -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' diff --git a/.github/workflows/finalize-tests.yml b/.github/workflows/finalize-tests.yml index 6a6ae7d6b2e99..f56f8192b2fc2 100644 --- a/.github/workflows/finalize-tests.yml +++ b/.github/workflows/finalize-tests.yml @@ -114,7 +114,7 @@ jobs: 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 diff --git a/.github/workflows/prod-image-build.yml b/.github/workflows/prod-image-build.yml index e66caf80a1ce7..f8620fd2b84aa 100644 --- a/.github/workflows/prod-image-build.yml +++ b/.github/workflows/prod-image-build.yml @@ -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 963181cd25622..715b0a9163f7d 100644 --- a/.github/workflows/publish-docs-to-s3.yml +++ b/.github/workflows/publish-docs-to-s3.yml @@ -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 diff --git a/.github/workflows/push-image-cache.yml b/.github/workflows/push-image-cache.yml index 1ac5253a0305a..ccd59d1388654 100644 --- a/.github/workflows/push-image-cache.yml +++ b/.github/workflows/push-image-cache.yml @@ -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/release_single_dockerhub_image.yml b/.github/workflows/release_single_dockerhub_image.yml index cc42773396292..35dae34d3cdc8 100644 --- a/.github/workflows/release_single_dockerhub_image.yml +++ b/.github/workflows/release_single_dockerhub_image.yml @@ -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 }}-* From abac721d5f6fa027a8dddd202bfc328ec2ffcd06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:02:52 +0100 Subject: [PATCH 022/280] chore(deps): bump actions/setup-node from 4.4.0 to 6.3.0 (#63131) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 6.3.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/49933ea5288caeca8642d1e84afbd3f7d6820020...53b83947a5a98c8d113130e565377fae1a50d02f) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 6.3.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/basic-tests.yml | 2 +- .github/workflows/registry-build.yml | 2 +- .github/workflows/ui-e2e-tests.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/basic-tests.yml b/.github/workflows/basic-tests.yml index 1eba352225001..7039cc36ff2ad 100644 --- a/.github/workflows/basic-tests.yml +++ b/.github/workflows/basic-tests.yml @@ -164,7 +164,7 @@ jobs: 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' diff --git a/.github/workflows/registry-build.yml b/.github/workflows/registry-build.yml index f75f5e935fe91..7b96061003945 100644 --- a/.github/workflows/registry-build.yml +++ b/.github/workflows/registry-build.yml @@ -211,7 +211,7 @@ jobs: 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' diff --git a/.github/workflows/ui-e2e-tests.yml b/.github/workflows/ui-e2e-tests.yml index e51eb95d0b661..f440caddf6319 100644 --- a/.github/workflows/ui-e2e-tests.yml +++ b/.github/workflows/ui-e2e-tests.yml @@ -126,7 +126,7 @@ jobs: 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)" From 8ec5b80e066bd8f4cb318e9cf672476b9bb86260 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:03:38 +0100 Subject: [PATCH 023/280] chore(deps): bump actions/setup-python from 5.6.0 to 6.2.0 (#63122) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.6.0 to 6.2.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/a26af69be951a213d495a4c3e4e4022e16d87065...a309ff8b426b58ec0e2a45f0f869d46889d02405) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 6.2.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-amd-arm.yml | 2 +- .github/workflows/registry-tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-amd-arm.yml b/.github/workflows/ci-amd-arm.yml index fe125c2c2dfde..fb71401b686ac 100644 --- a/.github/workflows/ci-amd-arm.yml +++ b/.github/workflows/ci-amd-arm.yml @@ -983,7 +983,7 @@ jobs: 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/registry-tests.yml b/.github/workflows/registry-tests.yml index e305d088655c9..760a1ec3e8cb6 100644 --- a/.github/workflows/registry-tests.yml +++ b/.github/workflows/registry-tests.yml @@ -50,7 +50,7 @@ jobs: persist-credentials: false - name: "Setup Python" - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.12" # 3.11+ required for stdlib tomllib From 63f35b0d54f9e1cf948b670a3337c3d0e2a89c68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:10:37 +0100 Subject: [PATCH 024/280] chore(deps): bump actions/checkout from 4.2.2 to 6.0.2 (#63121) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 6.0.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/11bd71901bbe5b1630ceea73d27597364c9af683...de0fac2e4500dabe0009e67214ff5f5447ce83dd) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.2 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../workflows/additional-ci-image-checks.yml | 2 +- .../workflows/additional-prod-image-tests.yml | 8 +++---- .../workflows/airflow-distributions-tests.yml | 2 +- .github/workflows/airflow-e2e-tests.yml | 2 +- .github/workflows/backport-cli.yml | 2 +- .github/workflows/basic-tests.yml | 22 +++++++++---------- .github/workflows/ci-amd-arm.yml | 10 ++++----- .github/workflows/ci-image-build.yml | 2 +- .github/workflows/ci-image-checks.yml | 12 +++++----- .github/workflows/ci-notification.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/finalize-tests.yml | 6 ++--- .github/workflows/generate-constraints.yml | 2 +- .github/workflows/helm-tests.yml | 4 ++-- .../workflows/integration-system-tests.yml | 6 ++--- .github/workflows/k8s-tests.yml | 2 +- .github/workflows/milestone-tag-assistant.yml | 2 +- .github/workflows/prod-image-build.yml | 4 ++-- .github/workflows/publish-docs-to-s3.yml | 6 ++--- .github/workflows/push-image-cache.yml | 4 ++-- .github/workflows/registry-build.yml | 2 +- .github/workflows/registry-tests.yml | 2 +- .github/workflows/release_dockerhub_image.yml | 2 +- .../release_single_dockerhub_image.yml | 4 ++-- .github/workflows/run-unit-tests.yml | 2 +- .github/workflows/test-providers.yml | 4 ++-- .github/workflows/ui-e2e-tests.yml | 2 +- 27 files changed, 60 insertions(+), 60 deletions(-) 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..35c93a3344f5c 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 @@ -277,7 +277,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..5009987bc67ff 100644 --- a/.github/workflows/airflow-e2e-tests.yml +++ b/.github/workflows/airflow-e2e-tests.yml @@ -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/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 7039cc36ff2ad..eda01ad5c1a17 100644 --- a/.github/workflows/basic-tests.yml +++ b/.github/workflows/basic-tests.yml @@ -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 @@ -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,7 +155,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: Setup 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" @@ -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 fb71401b686ac..ae9f58ba545c7 100644 --- a/.github/workflows/ci-amd-arm.yml +++ b/.github/workflows/ci-amd-arm.yml @@ -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,7 +830,7 @@ 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/ @@ -971,7 +971,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/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 1af065e80f664..7d23dce54a7d9 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" @@ -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 }}" @@ -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..81a9bf0d9a23a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,7 +47,7 @@ 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 diff --git a/.github/workflows/finalize-tests.yml b/.github/workflows/finalize-tests.yml index f56f8192b2fc2..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,7 +107,7 @@ 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 }} @@ -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 8f5467fa7cde8..dd902b8e17da8 100644 --- a/.github/workflows/milestone-tag-assistant.yml +++ b/.github/workflows/milestone-tag-assistant.yml @@ -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 f8620fd2b84aa..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" diff --git a/.github/workflows/publish-docs-to-s3.yml b/.github/workflows/publish-docs-to-s3.yml index 715b0a9163f7d..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 diff --git a/.github/workflows/push-image-cache.yml b/.github/workflows/push-image-cache.yml index ccd59d1388654..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" diff --git a/.github/workflows/registry-build.yml b/.github/workflows/registry-build.yml index 7b96061003945..a5d92fa955ac6 100644 --- a/.github/workflows/registry-build.yml +++ b/.github/workflows/registry-build.yml @@ -89,7 +89,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 diff --git a/.github/workflows/registry-tests.yml b/.github/workflows/registry-tests.yml index 760a1ec3e8cb6..19d5932e54cc7 100644 --- a/.github/workflows/registry-tests.yml +++ b/.github/workflows/registry-tests.yml @@ -45,7 +45,7 @@ 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 diff --git a/.github/workflows/release_dockerhub_image.yml b/.github/workflows/release_dockerhub_image.yml index 7fac56a7d194a..db02fa19a5a08 100644 --- a/.github/workflows/release_dockerhub_image.yml +++ b/.github/workflows/release_dockerhub_image.yml @@ -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 35dae34d3cdc8..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" 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 f440caddf6319..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 From da7edf86e9bcaf72bc7fd4db79c27d6de4e5cab2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:16:39 +0100 Subject: [PATCH 025/280] chore(deps-dev): bump @types/node (#63125) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.19.11 to 25.3.3. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.3.3 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../react_plugin_template/package.json | 2 +- .../react_plugin_template/pnpm-lock.yaml | 108 +++++++++--------- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/dev/react-plugin-tools/react_plugin_template/package.json b/dev/react-plugin-tools/react_plugin_template/package.json index 933b8e6d88c2b..3be38ec734391 100644 --- a/dev/react-plugin-tools/react_plugin_template/package.json +++ b/dev/react-plugin-tools/react_plugin_template/package.json @@ -45,7 +45,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0", - "@types/node": "^22.15.3", + "@types/node": "^25.3.3", "@types/react": "^18.3.19", "@types/react-dom": "^19.0.1", "@typescript-eslint/eslint-plugin": "8.56.1", diff --git a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml index e922a87562c63..c7da095c4f860 100644 --- a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml +++ b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml @@ -51,8 +51,8 @@ importers: specifier: ^4.3.0 version: 4.3.0(prettier@3.8.1) '@types/node': - specifier: ^22.15.3 - version: 22.19.11 + specifier: ^25.3.3 + version: 25.3.3 '@types/react': specifier: ^18.3.19 version: 18.3.28 @@ -70,10 +70,10 @@ importers: version: 8.56.1(eslint@9.39.3)(typescript@5.9.3) '@vitejs/plugin-react-swc': specifier: ^4.0.1 - version: 4.2.3(@swc/helpers@0.5.19)(vite@7.3.1(@types/node@22.19.11)) + version: 4.2.3(@swc/helpers@0.5.19)(vite@7.3.1(@types/node@25.3.3)) '@vitest/coverage-v8': specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@22.19.11)(happy-dom@20.8.3)) + version: 3.2.4(vitest@3.2.4(@types/node@25.3.3)(happy-dom@20.8.3)) eslint: specifier: ^9.25.1 version: 9.39.3 @@ -118,16 +118,16 @@ importers: version: 8.56.1(eslint@9.39.3)(typescript@5.9.3) vite: specifier: ^7.1.11 - version: 7.3.1(@types/node@22.19.11) + version: 7.3.1(@types/node@25.3.3) vite-plugin-css-injected-by-js: specifier: ^4.0.1 - version: 4.0.1(vite@7.3.1(@types/node@22.19.11)) + version: 4.0.1(vite@7.3.1(@types/node@25.3.3)) vite-plugin-dts: specifier: ^4.3.0 - version: 4.5.4(@types/node@22.19.11)(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.11)) + version: 4.5.4(@types/node@25.3.3)(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.19.11)(happy-dom@20.8.3) + version: 3.2.4(@types/node@25.3.3)(happy-dom@20.8.3) packages: @@ -859,8 +859,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.19.11': - resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} + '@types/node@25.3.3': + resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2731,8 +2731,8 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} @@ -3358,23 +3358,23 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@microsoft/api-extractor-model@7.33.1(@types/node@22.19.11)': + '@microsoft/api-extractor-model@7.33.1(@types/node@25.3.3)': dependencies: '@microsoft/tsdoc': 0.16.0 '@microsoft/tsdoc-config': 0.18.0 - '@rushstack/node-core-library': 5.20.1(@types/node@22.19.11) + '@rushstack/node-core-library': 5.20.1(@types/node@25.3.3) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.57.2(@types/node@22.19.11)': + '@microsoft/api-extractor@7.57.2(@types/node@25.3.3)': dependencies: - '@microsoft/api-extractor-model': 7.33.1(@types/node@22.19.11) + '@microsoft/api-extractor-model': 7.33.1(@types/node@25.3.3) '@microsoft/tsdoc': 0.16.0 '@microsoft/tsdoc-config': 0.18.0 - '@rushstack/node-core-library': 5.20.1(@types/node@22.19.11) + '@rushstack/node-core-library': 5.20.1(@types/node@25.3.3) '@rushstack/rig-package': 0.7.1 - '@rushstack/terminal': 0.22.1(@types/node@22.19.11) - '@rushstack/ts-command-line': 5.3.1(@types/node@22.19.11) + '@rushstack/terminal': 0.22.1(@types/node@25.3.3) + '@rushstack/ts-command-line': 5.3.1(@types/node@25.3.3) diff: 8.0.3 lodash: 4.17.23 minimatch: 10.2.4 @@ -3486,7 +3486,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true - '@rushstack/node-core-library@5.20.1(@types/node@22.19.11)': + '@rushstack/node-core-library@5.20.1(@types/node@25.3.3)': dependencies: ajv: 8.18.0 ajv-draft-04: 1.0.0(ajv@8.18.0) @@ -3497,28 +3497,28 @@ snapshots: resolve: 1.22.11 semver: 7.5.4 optionalDependencies: - '@types/node': 22.19.11 + '@types/node': 25.3.3 - '@rushstack/problem-matcher@0.2.1(@types/node@22.19.11)': + '@rushstack/problem-matcher@0.2.1(@types/node@25.3.3)': optionalDependencies: - '@types/node': 22.19.11 + '@types/node': 25.3.3 '@rushstack/rig-package@0.7.1': dependencies: resolve: 1.22.11 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.22.1(@types/node@22.19.11)': + '@rushstack/terminal@0.22.1(@types/node@25.3.3)': dependencies: - '@rushstack/node-core-library': 5.20.1(@types/node@22.19.11) - '@rushstack/problem-matcher': 0.2.1(@types/node@22.19.11) + '@rushstack/node-core-library': 5.20.1(@types/node@25.3.3) + '@rushstack/problem-matcher': 0.2.1(@types/node@25.3.3) supports-color: 8.1.1 optionalDependencies: - '@types/node': 22.19.11 + '@types/node': 25.3.3 - '@rushstack/ts-command-line@5.3.1(@types/node@22.19.11)': + '@rushstack/ts-command-line@5.3.1(@types/node@25.3.3)': dependencies: - '@rushstack/terminal': 0.22.1(@types/node@22.19.11) + '@rushstack/terminal': 0.22.1(@types/node@25.3.3) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -3651,9 +3651,9 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@22.19.11': + '@types/node@25.3.3': dependencies: - undici-types: 6.21.0 + undici-types: 7.18.2 '@types/normalize-package-data@2.4.4': {} @@ -3674,7 +3674,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 22.19.11 + '@types/node': 25.3.3 '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3)': dependencies: @@ -3769,15 +3769,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.19)(vite@7.3.1(@types/node@22.19.11))': + '@vitejs/plugin-react-swc@4.2.3(@swc/helpers@0.5.19)(vite@7.3.1(@types/node@25.3.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 '@swc/core': 1.15.11(@swc/helpers@0.5.19) - vite: 7.3.1(@types/node@22.19.11) + vite: 7.3.1(@types/node@25.3.3) transitivePeerDependencies: - '@swc/helpers' - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.19.11)(happy-dom@20.8.3))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@25.3.3)(happy-dom@20.8.3))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -3792,7 +3792,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.19.11)(happy-dom@20.8.3) + vitest: 3.2.4(@types/node@25.3.3)(happy-dom@20.8.3) transitivePeerDependencies: - supports-color @@ -3804,13 +3804,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.11))': + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.3.3))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@22.19.11) + vite: 7.3.1(@types/node@25.3.3) '@vitest/pretty-format@3.2.4': dependencies: @@ -5182,7 +5182,7 @@ snapshots: happy-dom@20.8.3: dependencies: - '@types/node': 22.19.11 + '@types/node': 25.3.3 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 @@ -6133,7 +6133,7 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@6.21.0: {} + undici-types@7.18.2: {} universalify@2.0.1: {} @@ -6154,13 +6154,13 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite-node@3.2.4(@types/node@22.19.11): + vite-node@3.2.4(@types/node@25.3.3): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@22.19.11) + vite: 7.3.1(@types/node@25.3.3) transitivePeerDependencies: - '@types/node' - jiti @@ -6175,13 +6175,13 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@4.0.1(vite@7.3.1(@types/node@22.19.11)): + vite-plugin-css-injected-by-js@4.0.1(vite@7.3.1(@types/node@25.3.3)): dependencies: - vite: 7.3.1(@types/node@22.19.11) + vite: 7.3.1(@types/node@25.3.3) - vite-plugin-dts@4.5.4(@types/node@22.19.11)(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.11)): + vite-plugin-dts@4.5.4(@types/node@25.3.3)(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)): dependencies: - '@microsoft/api-extractor': 7.57.2(@types/node@22.19.11) + '@microsoft/api-extractor': 7.57.2(@types/node@25.3.3) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) '@volar/typescript': 2.4.28 '@vue/language-core': 2.2.0(typescript@5.9.3) @@ -6192,13 +6192,13 @@ snapshots: magic-string: 0.30.21 typescript: 5.9.3 optionalDependencies: - vite: 7.3.1(@types/node@22.19.11) + vite: 7.3.1(@types/node@25.3.3) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite@7.3.1(@types/node@22.19.11): + vite@7.3.1(@types/node@25.3.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -6207,14 +6207,14 @@ snapshots: rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.19.11 + '@types/node': 25.3.3 fsevents: 2.3.3 - vitest@3.2.4(@types/node@22.19.11)(happy-dom@20.8.3): + vitest@3.2.4(@types/node@25.3.3)(happy-dom@20.8.3): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.11)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.3.3)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -6232,11 +6232,11 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@22.19.11) - vite-node: 3.2.4(@types/node@22.19.11) + vite: 7.3.1(@types/node@25.3.3) + vite-node: 3.2.4(@types/node@25.3.3) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.19.11 + '@types/node': 25.3.3 happy-dom: 20.8.3 transitivePeerDependencies: - jiti From 243b8413275be1fb87b3296f8bf669e641b75af6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:17:21 +0100 Subject: [PATCH 026/280] chore(deps): bump the core-ui-package-updates group across 1 directory with 2 updates (#63068) Bumps the core-ui-package-updates group with 2 updates in the /airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui directory: [@chakra-ui/react](https://github.com/chakra-ui/chakra-ui/tree/HEAD/packages/react) and [happy-dom](https://github.com/capricorn86/happy-dom). Updates `@chakra-ui/react` from 3.33.0 to 3.34.0 - [Release notes](https://github.com/chakra-ui/chakra-ui/releases) - [Changelog](https://github.com/chakra-ui/chakra-ui/blob/main/packages/react/CHANGELOG.md) - [Commits](https://github.com/chakra-ui/chakra-ui/commits/@chakra-ui/react@3.34.0/packages/react) Updates `happy-dom` from 20.7.0 to 20.8.3 - [Release notes](https://github.com/capricorn86/happy-dom/releases) - [Commits](https://github.com/capricorn86/happy-dom/compare/v20.7.0...v20.8.3) --- updated-dependencies: - dependency-name: "@chakra-ui/react" dependency-version: 3.34.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: happy-dom dependency-version: 20.8.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../auth/managers/simple/ui/package.json | 4 +- .../auth/managers/simple/ui/pnpm-lock.yaml | 1447 +++++++++-------- 2 files changed, 727 insertions(+), 724 deletions(-) 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..c6b9fc2381ba7 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,7 +18,7 @@ "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", "@tanstack/react-query": "^5.90.21", @@ -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", 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..d1ded3d02e464 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 @@ -16,8 +16,8 @@ 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) @@ -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==} @@ -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==} @@ -679,79 +679,66 @@ 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==} @@ -815,28 +802,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.15.11': resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-x64-gnu@1.15.11': resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.15.11': resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.15.11': resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==} @@ -868,8 +851,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 +931,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 +1054,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 +1933,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: @@ -2992,72 +2978,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 +3210,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,16 +3409,16 @@ 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)': dependencies: @@ -3507,13 +3494,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 +3527,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 +3650,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 +3665,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 +3754,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 +3772,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 +3865,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 +3885,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 +3896,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 +3926,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: @@ -5290,9 +5293,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 @@ -6221,11 +6224,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 +6237,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 +6261,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 From 8c42004febef027546bae819bcd9c57ae380f7ed Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Sun, 8 Mar 2026 20:35:00 +0100 Subject: [PATCH 027/280] Add clarifications to airflow security model (#63140) Several of our reporters are confused by custom RBAC permissions and permissions that are needed to trigger a Dag via Assets. This change clarifies it. --- airflow-core/docs/security/security_model.rst | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) 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 --------------------------------------- From 8c61320fe12844b583af38b2dd426ce501aa3f5b Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Sun, 8 Mar 2026 20:35:58 +0100 Subject: [PATCH 028/280] Add `breeze pr auto-triage` command (#62682) Add a new Breeze CLI command that helps maintainers efficiently triage open PRs from non-collaborators that don't meet minimum quality criteria. The command fetches open PRs via GitHub GraphQL API with optimized chunked queries, runs deterministic CI checks (failures, merge conflicts, missing test workflows), optionally runs LLM-based quality assessment, and presents flagged PRs interactively for maintainer review with author profiles and contribution history. Key features: - Optimized GraphQL queries with chunking to avoid GitHub timeout errors - Deterministic CI failure detection with categorized fix instructions - LLM assessment via `claude` or `codex` CLI for content quality - Interactive review with Rich panels, clickable links, and author context - "maintainer-accepted" label to skip PRs on future runs - Workflow approval support for first-time contributor PRs awaiting CI runs - Merge conflict and commits-behind detection with rebase guidance Update dev/breeze/src/airflow_breeze/commands/pr_commands.py Update dev/breeze/src/airflow_breeze/utils/llm_utils.py Update contributing-docs/05_pull_requests.rst Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/basic-tests.yml | 6 +- .github/workflows/ci-image-checks.yml | 2 +- AGENTS.md | 8 +- INSTALL | 2 +- airflow-core/hatch_build.py | 2 +- contributing-docs/05_pull_requests.rst | 62 +- contributing-docs/08_static_code_checks.rst | 14 +- dev/breeze/doc/11_registry_tasks.rst | 3 +- dev/breeze/doc/12_issues_tasks.rst | 4 +- dev/breeze/doc/13_pr_tasks.rst | 117 ++ ...pics.rst => 14_advanced_breeze_topics.rst} | 0 dev/breeze/doc/README.rst | 3 +- dev/breeze/doc/images/output-commands.svg | 22 +- dev/breeze/doc/images/output_pr.svg | 103 + dev/breeze/doc/images/output_pr.txt | 1 + .../doc/images/output_pr_auto-triage.svg | 320 +++ .../doc/images/output_pr_auto-triage.txt | 1 + ...utput_setup_check-all-params-in-groups.svg | 6 +- ...utput_setup_check-all-params-in-groups.txt | 2 +- dev/breeze/doc/images/output_setup_config.svg | 48 +- dev/breeze/doc/images/output_setup_config.txt | 2 +- ...output_setup_regenerate-command-images.svg | 18 +- ...output_setup_regenerate-command-images.txt | 2 +- dev/breeze/src/airflow_breeze/breeze.py | 2 + .../airflow_breeze/commands/ci_commands.py | 6 +- .../airflow_breeze/commands/common_options.py | 10 + .../airflow_breeze/commands/pr_commands.py | 1806 +++++++++++++++++ .../commands/pr_commands_config.py | 46 + .../airflow_breeze/commands/setup_commands.py | 4 + .../commands/setup_commands_config.py | 1 + .../airflow_breeze/configure_rich_click.py | 7 + .../src/airflow_breeze/global_constants.py | 20 + .../src/airflow_breeze/utils/confirm.py | 85 +- dev/breeze/src/airflow_breeze/utils/github.py | 232 +++ .../src/airflow_breeze/utils/llm_utils.py | 295 +++ .../src/airflow_breeze/utils/run_utils.py | 2 +- scripts/ci/prek/mypy_folder.py | 4 +- 37 files changed, 3208 insertions(+), 60 deletions(-) create mode 100644 dev/breeze/doc/13_pr_tasks.rst rename dev/breeze/doc/{13_advanced_breeze_topics.rst => 14_advanced_breeze_topics.rst} (100%) create mode 100644 dev/breeze/doc/images/output_pr.svg create mode 100644 dev/breeze/doc/images/output_pr.txt create mode 100644 dev/breeze/doc/images/output_pr_auto-triage.svg create mode 100644 dev/breeze/doc/images/output_pr_auto-triage.txt create mode 100644 dev/breeze/src/airflow_breeze/commands/pr_commands.py create mode 100644 dev/breeze/src/airflow_breeze/commands/pr_commands_config.py create mode 100644 dev/breeze/src/airflow_breeze/utils/llm_utils.py diff --git a/.github/workflows/basic-tests.yml b/.github/workflows/basic-tests.yml index eda01ad5c1a17..ffe5f18f2b1f2 100644 --- a/.github/workflows/basic-tests.yml +++ b/.github/workflows/basic-tests.yml @@ -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 diff --git a/.github/workflows/ci-image-checks.yml b/.github/workflows/ci-image-checks.yml index 7d23dce54a7d9..126bf9fd1f748 100644 --- a/.github/workflows/ci-image-checks.yml +++ b/.github/workflows/ci-image-checks.yml @@ -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" diff --git a/AGENTS.md b/AGENTS.md index da68ce911b199..d707e4c7a79aa 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 ` @@ -122,9 +122,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/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/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/contributing-docs/05_pull_requests.rst b/contributing-docs/05_pull_requests.rst index f936210a8e2fb..639bb3659c94f 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 @@ -75,6 +75,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 +156,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/08_static_code_checks.rst b/contributing-docs/08_static_code_checks.rst index 1ddb14c4f9383..69e12196c50e0 100644 --- a/contributing-docs/08_static_code_checks.rst +++ b/contributing-docs/08_static_code_checks.rst @@ -175,13 +175,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 +272,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 +287,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 +309,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/dev/breeze/doc/11_registry_tasks.rst b/dev/breeze/doc/11_registry_tasks.rst index acbd90ffeacd6..c64828a8b7b07 100644 --- a/dev/breeze/doc/11_registry_tasks.rst +++ b/dev/breeze/doc/11_registry_tasks.rst @@ -81,5 +81,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/images/output-commands.svg b/dev/breeze/doc/images/output-commands.svg index 263431946505b..2b7dad7dbfb01 100644 --- a/dev/breeze/doc/images/output-commands.svg +++ b/dev/breeze/doc/images/output-commands.svg @@ -1,4 +1,4 @@ - + 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/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..a408f8589c9c1 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx @@ -27,8 +27,8 @@ 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 { getTimezoneOffsetString, getTimezoneTooltipLabel } from "src/utils/datetimeUtils"; import type { NavItemResponse } from "src/utils/types"; @@ -157,7 +157,7 @@ export const Nav = () => { - { - + 404 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/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 = { From d69e4617fdb3b0a0f1b48a1efdc9927e0ba54f64 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Mon, 9 Mar 2026 21:49:16 +0800 Subject: [PATCH 046/280] fix(Asset-Partition): use timetable_partitioned to distinguish whether a Dag is partitioned (#62864) --- airflow-core/src/airflow/assets/manager.py | 9 ++------- airflow-core/src/airflow/jobs/scheduler_job_runner.py | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) 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/jobs/scheduler_job_runner.py b/airflow-core/src/airflow/jobs/scheduler_job_runner.py index 9f9aa78cd74a1..24dbf8d1e98c0 100644 --- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py +++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py @@ -2090,7 +2090,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, From 65de97a22ce3874c45456f98fa2777d7714ac9ca Mon Sep 17 00:00:00 2001 From: ANKIT KUMAR Date: Mon, 9 Mar 2026 19:25:27 +0530 Subject: [PATCH 047/280] Add missing HTTP timeout to FAB JWKS fetching (#63058) Adds a 30s timeout to JWKS request (from Authentik or Microsoft) to prevent indefinite hangs. This is a network call for auth metadata which should fail fast if the remote server is unresponsive. --- .../providers/fab/auth_manager/security_manager/override.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py index 0bd9a38983c06..80800aa25c22f 100644 --- a/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py +++ b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py @@ -396,7 +396,7 @@ def __init__(self, appbuilder): def _get_authentik_jwks(self, jwks_url) -> dict: import requests - resp = requests.get(jwks_url) + resp = requests.get(jwks_url, timeout=30) if resp.status_code == 200: return resp.json() return {} @@ -2326,7 +2326,7 @@ def _rotate_session_id(self) -> None: def _get_microsoft_jwks(self) -> list[dict[str, Any]]: import requests - return requests.get(MICROSOFT_KEY_SET_URL).json() + return requests.get(MICROSOFT_KEY_SET_URL, timeout=30).json() def _decode_and_validate_azure_jwt(self, id_token: str) -> dict[str, str]: verify_signature = self.oauth_remotes["azure"].client_kwargs.get("verify_signature", False) From 05d3723570f1fb835df2394face42457fd5240bd Mon Sep 17 00:00:00 2001 From: Justin Pakzad <114518232+justinpakzad@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:06:26 -0400 Subject: [PATCH 048/280] Fix invalid RequestPayer usage in S3Hook.select_key() method (#63148) --- .../src/airflow/providers/amazon/aws/hooks/s3.py | 7 ++++--- .../amazon/tests/unit/amazon/aws/hooks/test_s3.py | 12 ++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py b/providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py index d71a8383e8ad7..f9beb0d040003 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py @@ -1107,14 +1107,16 @@ def select_key( """ expression = expression or "SELECT * FROM S3Object" expression_type = expression_type or "SQL" - extra_args = {} if input_serialization is None: input_serialization = {"CSV": {}} if output_serialization is None: output_serialization = {"CSV": {}} if self._requester_pays: - extra_args["RequestPayer"] = "requester" + raise ValueError( + "select_key cannot be used with requester_pays=True. " + "S3 Select does not support the RequestPayer parameter." + ) response = self.get_conn().select_object_content( Bucket=bucket_name, @@ -1123,7 +1125,6 @@ def select_key( ExpressionType=expression_type, InputSerialization=input_serialization, OutputSerialization=output_serialization, - ExtraArgs=extra_args, ) return b"".join( diff --git a/providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py b/providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py index f84f7b8ba0adc..14371da9cb4b3 100644 --- a/providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py +++ b/providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py @@ -474,18 +474,26 @@ def test_select_key(self, mock_get_client_type, s3_bucket): mock_get_client_type.return_value.select_object_content.return_value = { "Payload": [{"Records": {"Payload": b"Cont\xc3"}}, {"Records": {"Payload": b"\xa9nt"}}] } - hook = S3Hook(requester_pays=True) + hook = S3Hook(requester_pays=False) assert hook.select_key("my_key", s3_bucket) == "Contént" mock_get_client_type.return_value.select_object_content.assert_called_with( Bucket="airflow-test-s3-bucket", Expression="SELECT * FROM S3Object", ExpressionType="SQL", - ExtraArgs={"RequestPayer": "requester"}, InputSerialization={"CSV": {}}, Key="my_key", OutputSerialization={"CSV": {}}, ) + @mock.patch("airflow.providers.amazon.aws.hooks.base_aws.AwsBaseHook.get_client_type") + def test_select_key_with_requester_pay(self, mock_get_client_type, s3_bucket): + mock_get_client_type.return_value.select_object_content.return_value = { + "Payload": [{"Records": {"Payload": b"Cont\xc3"}}, {"Records": {"Payload": b"\xa9nt"}}] + } + hook = S3Hook(requester_pays=True) + with pytest.raises(ValueError, match="select_key cannot be used with requester_pays"): + hook.select_key("my_key", s3_bucket) + def test_check_for_wildcard_key(self, s3_bucket): hook = S3Hook() bucket = hook.get_bucket(s3_bucket) From e824e1de7a5189ad5c624345b76babe5668e96b6 Mon Sep 17 00:00:00 2001 From: Pradeep Kalluri <128097794+kalluripradeep@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:18:00 +0000 Subject: [PATCH 049/280] fix: ensure Celery tasks are registered at worker startup (main) (#63110) * fix: ensure Celery tasks are registered at worker startup Fixes: #63043 * fix: add test for celery task registration on import * remove temporary fix scripts * chore: add TODO comment for execute_command removal when min Airflow >= 3 * fix: correct indentation of TODO comment in test --- .../celery/executors/celery_executor.py | 3 +++ .../celery/executors/test_celery_executor.py | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/providers/celery/src/airflow/providers/celery/executors/celery_executor.py b/providers/celery/src/airflow/providers/celery/executors/celery_executor.py index cd142acc7a182..53e7f06948286 100644 --- a/providers/celery/src/airflow/providers/celery/executors/celery_executor.py +++ b/providers/celery/src/airflow/providers/celery/executors/celery_executor.py @@ -39,6 +39,9 @@ from airflow.exceptions import AirflowProviderDeprecationWarning from airflow.executors.base_executor import BaseExecutor +from airflow.providers.celery.executors import ( + celery_executor_utils as _celery_executor_utils, # noqa: F401 # Needed to register Celery tasks at worker startup, see #63043 +) from airflow.providers.celery.version_compat import AIRFLOW_V_3_0_PLUS, AIRFLOW_V_3_2_PLUS from airflow.providers.common.compat.sdk import AirflowTaskTimeout, Stats from airflow.utils.state import TaskInstanceState diff --git a/providers/celery/tests/unit/celery/executors/test_celery_executor.py b/providers/celery/tests/unit/celery/executors/test_celery_executor.py index 94bb8670666d9..1a7bef2523e8e 100644 --- a/providers/celery/tests/unit/celery/executors/test_celery_executor.py +++ b/providers/celery/tests/unit/celery/executors/test_celery_executor.py @@ -687,3 +687,26 @@ def test_task_routing_through_team_specific_app(self, mock_send_tasks, monkeypat # Critical: task belongs to team A's app, not module-level app assert task_from_call.app is team_a_executor.celery_app assert task_from_call.name == "execute_command" + + +def test_celery_tasks_registered_on_import(): + """ + Ensure execute_workload (and execute_command for Airflow 2.x) are registered + with the Celery app when celery_executor is imported. + + Regression test for https://github.com/apache/airflow/issues/63043 + Celery provider 3.17.0 exposed that celery_executor_utils was never imported + at module level, so tasks were never registered at worker startup. + """ + from airflow.providers.celery.executors.celery_executor_utils import app + + registered_tasks = list(app.tasks.keys()) + assert "execute_workload" in registered_tasks, ( + "execute_workload must be registered with the Celery app at import time. " + "Workers need this to receive tasks without KeyError." + ) + # TODO: remove this block when min supported Airflow version is >= 3.0 + if not AIRFLOW_V_3_0_PLUS: + assert "execute_command" in registered_tasks, ( + "execute_command must be registered for Airflow 2.x compatibility." + ) From eff43ec994d0079c54520437213b4161419bfd30 Mon Sep 17 00:00:00 2001 From: Sam Dumont Date: Mon, 9 Mar 2026 15:27:43 +0100 Subject: [PATCH 050/280] Fix CloudwatchTaskHandler not deleting local logs after streaming (#62985) * Fix CloudwatchTaskHandler not deleting local logs after streaming In Airflow 3, `CloudwatchTaskHandler` streams logs to CloudWatch in real-time via structlog processors, so `upload()` was a no-op. However, `delete_local_copy` was never honoured, causing local log files to accumulate indefinitely on shared storage (e.g. EFS). Changes: - `CloudWatchRemoteLogIO.upload()` now deletes the local log directory when `delete_local_copy` is True. - `CloudwatchTaskHandler.close()` calls `upload()` with the rendered log path stored during `set_context()`. - Path traversal guard prevents deletion outside `base_log_folder`. * chore: rename newsfragment to PR #62985 * Remove provider newsfragment (not used for providers) * fix: align close() guard and tests with S3/GCS/HDFS convention Use hasattr(self, "ti") guard in close() like S3/GCS/HDFS handlers. Fix tests to be version-agnostic by asserting against handler's own log_relative_path instead of hardcoded filename format. --- .../amazon/aws/log/cloudwatch_task_handler.py | 34 +++++- .../aws/log/test_cloudwatch_task_handler.py | 112 +++++++++++++++++- 2 files changed, 138 insertions(+), 8 deletions(-) diff --git a/providers/amazon/src/airflow/providers/amazon/aws/log/cloudwatch_task_handler.py b/providers/amazon/src/airflow/providers/amazon/aws/log/cloudwatch_task_handler.py index ab32a5ffae759..055cfd4493306 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/log/cloudwatch_task_handler.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/log/cloudwatch_task_handler.py @@ -22,6 +22,7 @@ import json import logging import os +import shutil from collections.abc import Generator from datetime import date, datetime, timedelta, timezone from functools import cached_property @@ -166,10 +167,27 @@ def close(self): self.handler.flush() def upload(self, path: os.PathLike | str, ti: RuntimeTI): - # No-op, as we upload via the processor as we go - # But we need to give the handler time to finish off its business + """Upload the given log path to the remote storage.""" + # No batch upload — logs stream in real-time. Flush pending events and clean up. self.close() - return + if self.delete_local_copy: + base = self.base_log_folder.resolve() + raw = Path(path) + local_path = (raw if raw.is_absolute() else base / raw).resolve() + try: + local_path.relative_to(base) + except ValueError: + self.log.warning( + "Skipping deletion: path %s is outside base_log_folder %s", + local_path, + base, + ) + return + parent = local_path.parent + if parent.exists(): + shutil.rmtree(parent, ignore_errors=True) + if parent.exists(): + self.log.warning("Failed to delete local log dir: %s", parent) def read(self, relative_path: str, ti: RuntimeTI) -> LogResponse: messages, logs = self.stream(relative_path, ti) @@ -261,10 +279,12 @@ def __init__( self.log_group = split_arn[6] self.region_name = split_arn[3] self.closed = False + self.log_relative_path: str = "" self.io = CloudWatchRemoteLogIO( base_log_folder=base_log_folder, log_group_arn=log_group_arn, + delete_local_copy=conf.getboolean("logging", "delete_local_logs"), ) @cached_property @@ -281,8 +301,9 @@ def _render_filename(self, ti, try_number): def set_context(self, ti: TaskInstance, *, identifier: str | None = None): super().set_context(ti) self.io.log_stream_name = self._render_filename(ti, ti.try_number) - self.handler = self.io.handler + self.ti = ti + self.log_relative_path = self.io.log_stream_name def close(self): """Close the handler responsible for the upload of the local log file to Cloudwatch.""" @@ -295,6 +316,11 @@ def close(self): if self.handler is not None: self.handler.close() + if hasattr(self, "ti"): + try: + self.io.upload(self.log_relative_path, self.ti) + except Exception: + self.log.exception("Failed to delete local log after streaming to CloudWatch") # Mark closed so we don't double write if close is called twice self.closed = True diff --git a/providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py b/providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py index 9cf1b0be6ed1a..cb5830b36cc5c 100644 --- a/providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py +++ b/providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py @@ -21,6 +21,7 @@ import textwrap import time from datetime import datetime as dt, timedelta, timezone +from pathlib import Path from unittest import mock from unittest.mock import ANY, call @@ -111,6 +112,8 @@ def setup_tests(self, create_runtime_ti, tmp_path, monkeypatch): airflow.sdk.log._ActiveLoggingConfig.set(self.subject, None) else: monkeypatch.setattr(airflow.logging_config, "REMOTE_TASK_LOG", self.subject) + # Clear @cache'd processors so this test picks up self.subject. + airflow.sdk.log.logging_processors.cache_clear() try: procs = airflow.sdk.log.logging_processors(colors=False, json_output=False) except TypeError: @@ -137,6 +140,57 @@ def drop(*args): processors.clear() processors.extend(old_processors) structlog.configure(processors=old_processors, logger_factory=logger_factory) + # Clear @cache to avoid cross-test contamination. + airflow.sdk.log.logging_processors.cache_clear() + + def test_upload_deletes_local_log_dir_when_delete_local_copy_true(self): + """upload() with delete_local_copy=True deletes the local log parent directory.""" + assert self.subject.delete_local_copy is True # attrs default + dag_dir = self.local_log_location / "dag_id=a" + assert dag_dir.exists() + + with mock.patch.object(self.subject, "close"): + self.subject.upload(self.task_log_path, self.ti) + + assert not dag_dir.exists() + # Close watchtower handler; conf_vars needed because base_log_folder differs from tmp_path. + with conf_vars({("logging", "base_log_folder"): self.local_log_location.as_posix()}): + self.subject.handler.close() + + def test_upload_does_not_delete_when_delete_local_copy_false(self): + """upload() with delete_local_copy=False leaves local files untouched.""" + self.subject.delete_local_copy = False + dag_dir = self.local_log_location / "dag_id=a" + assert dag_dir.exists() + + with mock.patch.object(self.subject, "close"): + self.subject.upload(self.task_log_path, self.ti) + + assert dag_dir.exists() + with conf_vars({("logging", "base_log_folder"): self.local_log_location.as_posix()}): + self.subject.handler.close() + + def test_upload_handles_absolute_path(self): + """upload() resolves an absolute path to the correct parent directory.""" + dag_dir = self.local_log_location / "dag_id=a" + assert dag_dir.exists() + absolute_path = self.local_log_location / self.task_log_path + + with mock.patch.object(self.subject, "close"): + self.subject.upload(absolute_path, self.ti) + + assert not dag_dir.exists() + with conf_vars({("logging", "base_log_folder"): self.local_log_location.as_posix()}): + self.subject.handler.close() + + def test_upload_skips_deletion_outside_base_log_folder(self): + """upload() logs a warning and skips deletion for paths outside base_log_folder.""" + outside_path = "/tmp/evil/../../../etc/passwd" + with conf_vars({("logging", "base_log_folder"): self.local_log_location.as_posix()}): + with mock.patch.object(self.subject, "close"): + self.subject.upload(outside_path, self.ti) + dag_dir = self.local_log_location / "dag_id=a" + assert dag_dir.exists() @time_machine.travel(datetime(2025, 3, 27, 21, 58, 1, 2345), tick=False) def test_log_message(self): @@ -404,11 +458,61 @@ def __repr__(self): def test_close_prevents_duplicate_calls(self): with mock.patch("watchtower.CloudWatchLogHandler.close") as mock_log_handler_close: with mock.patch("airflow.utils.log.file_task_handler.FileTaskHandler.set_context"): - self.cloudwatch_task_handler.set_context(self.ti) - for _ in range(5): - self.cloudwatch_task_handler.close() + with mock.patch.object(self.cloudwatch_task_handler.io, "upload"): + self.cloudwatch_task_handler.set_context(self.ti) + for _ in range(5): + self.cloudwatch_task_handler.close() + + mock_log_handler_close.assert_called_once() + + def test_set_context_stores_ti_and_log_relative_path(self): + """set_context() stores the task instance and rendered log path for use in close().""" + self.cloudwatch_task_handler.set_context(self.ti) + + assert self.cloudwatch_task_handler.ti is self.ti + assert self.cloudwatch_task_handler.log_relative_path != "" + assert ( + self.cloudwatch_task_handler.log_relative_path == self.cloudwatch_task_handler.io.log_stream_name + ) + + def test_close_calls_upload_once(self): + """close() calls io.upload() exactly once, even when called multiple times.""" + with mock.patch("airflow.utils.log.file_task_handler.FileTaskHandler.set_context"): + with mock.patch.object(self.cloudwatch_task_handler.io, "upload") as mock_upload: + with mock.patch("watchtower.CloudWatchLogHandler.close"): + self.cloudwatch_task_handler.set_context(self.ti) + for _ in range(3): + self.cloudwatch_task_handler.close() + + mock_upload.assert_called_once_with( + self.cloudwatch_task_handler.log_relative_path, self.ti + ) + + def test_close_skips_upload_without_set_context(self): + """close() without a prior set_context() should not call io.upload().""" + with mock.patch.object(self.cloudwatch_task_handler.io, "upload") as mock_upload: + self.cloudwatch_task_handler.close() + mock_upload.assert_not_called() + + @pytest.mark.skipif(not AIRFLOW_V_3_0_PLUS, reason="delete_local_copy only relevant for Airflow 3") + def test_close_deletes_local_log_dir(self): + """close() after set_context() deletes the local log directory.""" + log_file_path = Path(self.local_log_location) / self.remote_log_stream + log_file_path.parent.mkdir(parents=True, exist_ok=True) + log_file_path.write_text("some log content") + assert log_file_path.parent.exists() + + with conf_vars({("logging", "delete_local_logs"): "True"}): + handler = CloudwatchTaskHandler( + self.local_log_location, + f"arn:aws:logs:{self.region_name}:11111111:log-group:{self.remote_log_group}", + ) + with mock.patch("airflow.utils.log.file_task_handler.FileTaskHandler.set_context"): + with mock.patch("watchtower.CloudWatchLogHandler.close"): + handler.set_context(self.ti) + handler.close() - mock_log_handler_close.assert_called_once() + assert not log_file_path.parent.exists() def test_filename_template_for_backward_compatibility(self): # filename_template arg support for running the latest provider on airflow 2 From 689a97c6d7821aa12b134be2ef6c6d56f4869049 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:36:16 -0400 Subject: [PATCH 051/280] chore(deps-dev): bump eslint-plugin-perfectionist (#63156) Bumps [eslint-plugin-perfectionist](https://github.com/azat-io/eslint-plugin-perfectionist) from 4.15.1 to 5.6.0. - [Release notes](https://github.com/azat-io/eslint-plugin-perfectionist/releases) - [Changelog](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/changelog.md) - [Commits](https://github.com/azat-io/eslint-plugin-perfectionist/compare/v4.15.1...v5.6.0) --- updated-dependencies: - dependency-name: eslint-plugin-perfectionist dependency-version: 5.6.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../react_plugin_template/package.json | 2 +- .../react_plugin_template/pnpm-lock.yaml | 21 +++++++------------ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/dev/react-plugin-tools/react_plugin_template/package.json b/dev/react-plugin-tools/react_plugin_template/package.json index 3be38ec734391..d826666ddc7e1 100644 --- a/dev/react-plugin-tools/react_plugin_template/package.json +++ b/dev/react-plugin-tools/react_plugin_template/package.json @@ -56,7 +56,7 @@ "eslint": "^9.25.1", "eslint-config-prettier": "^10.1.2", "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-perfectionist": "^4.12.3", + "eslint-plugin-perfectionist": "^5.6.0", "eslint-plugin-prettier": "^5.2.6", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", diff --git a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml index c7da095c4f860..7f3224c26bee9 100644 --- a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml +++ b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml @@ -84,8 +84,8 @@ importers: specifier: ^6.10.2 version: 6.10.2(eslint@9.39.3) eslint-plugin-perfectionist: - specifier: ^4.12.3 - version: 4.15.1(eslint@9.39.3)(typescript@5.9.3) + specifier: ^5.6.0 + version: 5.6.0(eslint@9.39.3)(typescript@5.9.3) eslint-plugin-prettier: specifier: ^5.2.6 version: 5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.3))(eslint@9.39.3)(prettier@3.8.1) @@ -921,10 +921,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.56.0': - resolution: {integrity: sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.56.1': resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1622,11 +1618,11 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-perfectionist@4.15.1: - resolution: {integrity: sha512-MHF0cBoOG0XyBf7G0EAFCuJJu4I18wy0zAoT1OHfx2o6EOx1EFTIzr2HGeuZa1kDcusoX0xJ9V7oZmaeFd773Q==} - engines: {node: ^18.0.0 || >=20.0.0} + eslint-plugin-perfectionist@5.6.0: + resolution: {integrity: sha512-pxrLrfRp5wl1Vol1fAEa/G5yTXxefTPJjz07qC7a8iWFXcOZNuWBItMQ2OtTzfQIvMq6bMyYcrzc3Wz++na55Q==} + engines: {node: ^20.0.0 || >=22.0.0} peerDependencies: - eslint: '>=8.45.0' + eslint: ^8.45.0 || ^9.0.0 || ^10.0.0 eslint-plugin-prettier@5.5.5: resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} @@ -3734,8 +3730,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.56.0': {} - '@typescript-eslint/types@8.56.1': {} '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': @@ -4909,9 +4903,8 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-perfectionist@4.15.1(eslint@9.39.3)(typescript@5.9.3): + eslint-plugin-perfectionist@5.6.0(eslint@9.39.3)(typescript@5.9.3): dependencies: - '@typescript-eslint/types': 8.56.0 '@typescript-eslint/utils': 8.56.1(eslint@9.39.3)(typescript@5.9.3) eslint: 9.39.3 natural-orderby: 5.0.0 From ce10543e6cbc15e1f2a1b08fad736c71bbc5c788 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:36:34 -0400 Subject: [PATCH 052/280] chore(deps-dev): bump @trivago/prettier-plugin-sort-imports (#63155) Bumps [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) from 4.3.0 to 6.0.2. - [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases) - [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/main/CHANGELOG.md) - [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v4.3.0...v6.0.2) --- updated-dependencies: - dependency-name: "@trivago/prettier-plugin-sort-imports" dependency-version: 6.0.2 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../react_plugin_template/package.json | 2 +- .../react_plugin_template/pnpm-lock.yaml | 157 ++++++------------ 2 files changed, 49 insertions(+), 110 deletions(-) diff --git a/dev/react-plugin-tools/react_plugin_template/package.json b/dev/react-plugin-tools/react_plugin_template/package.json index d826666ddc7e1..769c88fc4a4db 100644 --- a/dev/react-plugin-tools/react_plugin_template/package.json +++ b/dev/react-plugin-tools/react_plugin_template/package.json @@ -44,7 +44,7 @@ "@stylistic/eslint-plugin": "^2.13.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", - "@trivago/prettier-plugin-sort-imports": "^4.3.0", + "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/node": "^25.3.3", "@types/react": "^18.3.19", "@types/react-dom": "^19.0.1", diff --git a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml index 7f3224c26bee9..75ab592a50297 100644 --- a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml +++ b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml @@ -48,8 +48,8 @@ importers: specifier: ^16.3.0 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.0.1)(@types/react@18.3.28)(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.8.1) + specifier: ^6.0.2 + version: 6.0.2(prettier@3.8.1) '@types/node': specifier: ^25.3.3 version: 25.3.3 @@ -148,38 +148,18 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.17.7': - resolution: {integrity: sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==} - 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-environment-visitor@7.24.7': - resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-function-name@7.24.7': - resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} - engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.28.0': resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} - '@babel/helper-hoist-variables@7.24.7': - resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} - engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} - '@babel/helper-split-export-declaration@7.24.7': - resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -201,18 +181,10 @@ packages: resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.23.2': - resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} - 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'} - '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -832,14 +804,24 @@ packages: '@types/react-dom': optional: true - '@trivago/prettier-plugin-sort-imports@4.3.0': - resolution: {integrity: sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==} + '@trivago/prettier-plugin-sort-imports@6.0.2': + resolution: {integrity: sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==} + engines: {node: '>= 20'} peerDependencies: '@vue/compiler-sfc': 3.x prettier: 2.x - 3.x + prettier-plugin-ember-template-tag: '>= 2.0.0' + prettier-plugin-svelte: 3.x + svelte: 4.x || 5.x peerDependenciesMeta: '@vue/compiler-sfc': optional: true + prettier-plugin-ember-template-tag: + optional: true + prettier-plugin-svelte: + optional: true + svelte: + optional: true '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -1370,18 +1352,18 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} - 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 - 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} browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} @@ -1818,10 +1800,6 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -2076,11 +2054,6 @@ packages: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true - jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -2140,6 +2113,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -2290,10 +2266,16 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-imports-exports@0.2.4: + resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-statements@1.0.11: + resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2662,10 +2644,6 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -2989,12 +2967,6 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/generator@7.17.7': - dependencies: - '@babel/types': 7.17.0 - jsesc: 2.5.2 - source-map: 0.5.7 - '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.0 @@ -3003,21 +2975,8 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-environment-visitor@7.24.7': - dependencies: - '@babel/types': 7.29.0 - - '@babel/helper-function-name@7.24.7': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - '@babel/helper-globals@7.28.0': {} - '@babel/helper-hoist-variables@7.24.7': - dependencies: - '@babel/types': 7.29.0 - '@babel/helper-module-imports@7.28.6': dependencies: '@babel/traverse': 7.29.0 @@ -3025,10 +2984,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-split-export-declaration@7.24.7': - dependencies: - '@babel/types': 7.29.0 - '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -3045,21 +3000,6 @@ snapshots: '@babel/parser': 7.29.0 '@babel/types': 7.29.0 - '@babel/traverse@7.23.2': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-hoist-variables': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - debug: 4.4.3 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.29.0': dependencies: '@babel/code-frame': 7.29.0 @@ -3072,11 +3012,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/types@7.17.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - to-fast-properties: 2.0.0 - '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -3620,14 +3555,16 @@ snapshots: '@types/react': 18.3.28 '@types/react-dom': 19.0.1 - '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.1)': + '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.1)': dependencies: - '@babel/generator': 7.17.7 + '@babel/generator': 7.29.1 '@babel/parser': 7.29.0 - '@babel/traverse': 7.23.2 - '@babel/types': 7.17.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 javascript-natural-sort: 0.7.1 - lodash: 4.17.23 + lodash-es: 4.17.23 + minimatch: 10.2.4 + parse-imports-exports: 0.2.4 prettier: 3.8.1 transitivePeerDependencies: - supports-color @@ -4570,13 +4507,13 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.11 - balanced-match@4.0.3: {} + balanced-match@4.0.4: {} baseline-browser-mapping@2.10.0: {} - brace-expansion@5.0.2: + brace-expansion@5.0.4: dependencies: - balanced-match: 4.0.3 + balanced-match: 4.0.4 browserslist@4.28.1: dependencies: @@ -5156,8 +5093,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - globals@11.12.0: {} - globals@14.0.0: {} globals@15.15.0: {} @@ -5408,8 +5343,6 @@ snapshots: jsesc@0.5.0: {} - jsesc@2.5.2: {} - jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -5468,6 +5401,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.23: {} + lodash.merge@4.6.2: {} lodash@4.17.23: {} @@ -5506,7 +5441,7 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.2 + brace-expansion: 5.0.4 minipass@7.1.3: {} @@ -5623,6 +5558,10 @@ snapshots: dependencies: callsites: 3.1.0 + parse-imports-exports@0.2.4: + dependencies: + parse-statements: 1.0.11 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.0 @@ -5630,6 +5569,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-statements@1.0.11: {} + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -6053,8 +5994,6 @@ snapshots: tinyspy@4.0.4: {} - to-fast-properties@2.0.0: {} - ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 From 229af9ea4ea7a7740a6e0b07c528a9c75bca32f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:36:51 -0400 Subject: [PATCH 053/280] chore(deps-dev): bump @types/react (#63154) Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.3.28 to 19.2.14. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) --- updated-dependencies: - dependency-name: "@types/react" dependency-version: 19.2.14 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../react_plugin_template/package.json | 2 +- .../react_plugin_template/pnpm-lock.yaml | 36 ++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/dev/react-plugin-tools/react_plugin_template/package.json b/dev/react-plugin-tools/react_plugin_template/package.json index 769c88fc4a4db..c88916131822b 100644 --- a/dev/react-plugin-tools/react_plugin_template/package.json +++ b/dev/react-plugin-tools/react_plugin_template/package.json @@ -46,7 +46,7 @@ "@testing-library/react": "^16.3.0", "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/node": "^25.3.3", - "@types/react": "^18.3.19", + "@types/react": "^19.2.14", "@types/react-dom": "^19.0.1", "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", diff --git a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml index 75ab592a50297..8eea8f1a8332b 100644 --- a/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml +++ b/dev/react-plugin-tools/react_plugin_template/pnpm-lock.yaml @@ -18,10 +18,10 @@ importers: version: 2.3.4 '@chakra-ui/react': specifier: ^3.34.0 - version: 3.34.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + 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@18.3.28)(react@19.2.4) + version: 11.14.0(@types/react@19.2.14)(react@19.2.4) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -46,7 +46,7 @@ importers: version: 6.9.1 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.0.1)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.0.1)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@trivago/prettier-plugin-sort-imports': specifier: ^6.0.2 version: 6.0.2(prettier@3.8.1) @@ -54,8 +54,8 @@ importers: specifier: ^25.3.3 version: 25.3.3 '@types/react': - specifier: ^18.3.19 - version: 18.3.28 + specifier: ^19.2.14 + version: 19.2.14 '@types/react-dom': specifier: ^19.0.1 version: 19.0.1 @@ -850,14 +850,11 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - '@types/prop-types@15.7.15': - resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/react-dom@19.0.1': resolution: {integrity: sha512-hljHij7MpWPKF6u5vojuyfV0YA4YURsQG7KT6SzV0Zs2BXAtgdTxG6A229Ub/xiWV4w/7JL8fi6aAyjshH4meA==} - '@types/react@18.3.28': - resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} @@ -3021,11 +3018,11 @@ snapshots: '@chakra-ui/anatomy@2.3.4': {} - '@chakra-ui/react@3.34.0(@emotion/react@11.14.0(@types/react@18.3.28)(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.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@18.3.28)(react@19.2.4) + '@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 @@ -3066,7 +3063,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@18.3.28)(react@19.2.4)': + '@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 @@ -3078,7 +3075,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.2.4 optionalDependencies: - '@types/react': 18.3.28 + '@types/react': 19.2.14 transitivePeerDependencies: - supports-color @@ -3545,14 +3542,14 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.0.1)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.0.1)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@babel/runtime': 7.28.6 '@testing-library/dom': 10.4.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 18.3.28 + '@types/react': 19.2.14 '@types/react-dom': 19.0.1 '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.1)': @@ -3592,15 +3589,12 @@ snapshots: '@types/parse-json@4.0.2': {} - '@types/prop-types@15.7.15': {} - '@types/react-dom@19.0.1': dependencies: - '@types/react': 18.3.28 + '@types/react': 19.2.14 - '@types/react@18.3.28': + '@types/react@19.2.14': dependencies: - '@types/prop-types': 15.7.15 csstype: 3.2.3 '@types/whatwg-mimetype@3.0.2': {} From 65b5d9fb79a20398b13156693f54b0338c68b3fc Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Mon, 9 Mar 2026 22:48:30 +0800 Subject: [PATCH 054/280] Fix PowerBIDatasetRefreshOperator to properly respect wait_for_completion flag (#62842) --- .../microsoft/azure/triggers/powerbi.py | 29 ++++++---- .../microsoft/azure/triggers/test_powerbi.py | 55 +++++++++++++++++++ 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/triggers/powerbi.py b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/triggers/powerbi.py index c9198474cbb0b..354e84b855349 100644 --- a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/triggers/powerbi.py +++ b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/triggers/powerbi.py @@ -152,8 +152,23 @@ async def run(self) -> AsyncIterator[TriggerEvent]: request_body=self.request_body, ) - if dataset_refresh_id: - self.log.info("Triggered dataset refresh %s", dataset_refresh_id) + if not dataset_refresh_id: + yield TriggerEvent( + { + "status": "error", + "dataset_refresh_status": None, + "message": "Failed to trigger the dataset refresh.", + "dataset_refresh_id": None, + } + ) + return + + self.log.info("Triggered dataset refresh %s", dataset_refresh_id) + # Set the dataset_refresh_id for polling + self.dataset_refresh_id = dataset_refresh_id + + # If wait_for_termination is False, return immediately after triggering + if not self.wait_for_termination: yield TriggerEvent( { "status": "success", @@ -164,16 +179,6 @@ async def run(self) -> AsyncIterator[TriggerEvent]: ) return - yield TriggerEvent( - { - "status": "error", - "dataset_refresh_status": None, - "message": "Failed to trigger the dataset refresh.", - "dataset_refresh_id": None, - } - ) - return - # The dataset refresh is already triggered. Poll for the dataset refresh status. @tenacity.retry( stop=tenacity.stop_after_attempt(3), diff --git a/providers/microsoft/azure/tests/unit/microsoft/azure/triggers/test_powerbi.py b/providers/microsoft/azure/tests/unit/microsoft/azure/triggers/test_powerbi.py index 9648fa47387fc..4b6b4f9c98880 100644 --- a/providers/microsoft/azure/tests/unit/microsoft/azure/triggers/test_powerbi.py +++ b/providers/microsoft/azure/tests/unit/microsoft/azure/triggers/test_powerbi.py @@ -178,6 +178,7 @@ async def test_powerbi_trigger_run_failed( async def test_powerbi_trigger_run_trigger_refresh(self, mock_trigger_dataset_refresh, powerbi_trigger): """Assert event is triggered upon successful new refresh trigger.""" powerbi_trigger.dataset_refresh_id = None + powerbi_trigger.wait_for_termination = False mock_trigger_dataset_refresh.return_value = DATASET_REFRESH_ID task = [i async for i in powerbi_trigger.run()] @@ -355,3 +356,57 @@ async def test_powerbi_trigger_run_timeout( ) assert expected == actual + + @pytest.mark.asyncio + @mock.patch.object(PowerBIHook, "get_refresh_details_by_refresh_id") + @mock.patch.object(PowerBIHook, "trigger_dataset_refresh") + async def test_powerbi_trigger_waits_for_completion_when_wait_for_termination_true( + self, mock_trigger_dataset_refresh, mock_get_refresh_details_by_refresh_id + ): + # Create trigger without dataset_refresh_id (simulating first-time trigger) + trigger = PowerBITrigger( + conn_id=POWERBI_CONN_ID, + proxies=None, + api_version=API_VERSION, + dataset_id=DATASET_ID, + dataset_refresh_id=None, # No refresh ID - will trigger new refresh + group_id=GROUP_ID, + check_interval=CHECK_INTERVAL, + wait_for_termination=True, + timeout=TIMEOUT, + request_body=REQUEST_BODY, + ) + + # Mock trigger_dataset_refresh to return a new refresh ID + mock_trigger_dataset_refresh.return_value = DATASET_REFRESH_ID + + # Mock get_refresh_details to simulate the refresh completing + mock_get_refresh_details_by_refresh_id.return_value = { + "status": PowerBIDatasetRefreshStatus.COMPLETED, + "error": None, + } + + # Run the trigger + generator = trigger.run() + actual = await generator.asend(None) + + # Verify trigger was called + mock_trigger_dataset_refresh.assert_called_once_with( + dataset_id=DATASET_ID, + group_id=GROUP_ID, + request_body=REQUEST_BODY, + ) + + # Verify get_refresh_details was called (proving it's polling, not returning immediately) + assert mock_get_refresh_details_by_refresh_id.call_count >= 1 + + # Verify the final event shows COMPLETED status (not None) + expected = TriggerEvent( + { + "status": "success", + "dataset_refresh_status": PowerBIDatasetRefreshStatus.COMPLETED, + "message": f"The dataset refresh {DATASET_REFRESH_ID} has {PowerBIDatasetRefreshStatus.COMPLETED}.", + "dataset_refresh_id": DATASET_REFRESH_ID, + } + ) + assert expected == actual From 1b204132103c145c80f1d4f01ee559acc3604fe0 Mon Sep 17 00:00:00 2001 From: olegkachur-e Date: Mon, 9 Mar 2026 16:11:48 +0100 Subject: [PATCH 055/280] Delete google provider deprecated items sheduled for Jan 2026 (#62802) Co-authored-by: Oleg Kachur --- .../unit/always/test_project_structure.py | 38 - providers/google/docs/changelog.rst | 40 + .../integration-logos/Google-Data-Catalog.png | Bin 15195 -> 0 bytes .../docs/operators/cloud/datacatalog.rst | 526 ---- .../google/docs/operators/cloud/vertex_ai.rst | 91 - providers/google/provider.yaml | 6 - .../providers/google/cloud/hooks/bigquery.py | 9 - .../google/cloud/hooks/datacatalog.py | 1172 --------- .../cloud/hooks/vertex_ai/generative_model.py | 292 +- .../google/cloud/links/datacatalog.py | 84 - .../google/cloud/operators/datacatalog.py | 2338 ----------------- .../cloud/operators/vertex_ai/auto_ml.py | 2 +- .../operators/vertex_ai/generative_model.py | 568 +--- .../providers/google/get_provider_info.py | 7 - .../google/cloud/hooks/test_datacatalog.py | 1600 ----------- .../hooks/vertex_ai/test_generative_model.py | 111 - .../cloud/operators/test_datacatalog.py | 994 ------- .../vertex_ai/test_generative_model.py | 259 +- .../run_provider_yaml_files_check.py | 3 - 19 files changed, 44 insertions(+), 8096 deletions(-) delete mode 100644 providers/google/docs/integration-logos/Google-Data-Catalog.png delete mode 100644 providers/google/docs/operators/cloud/datacatalog.rst delete mode 100644 providers/google/src/airflow/providers/google/cloud/hooks/datacatalog.py delete mode 100644 providers/google/src/airflow/providers/google/cloud/links/datacatalog.py delete mode 100644 providers/google/src/airflow/providers/google/cloud/operators/datacatalog.py delete mode 100644 providers/google/tests/unit/google/cloud/hooks/test_datacatalog.py delete mode 100644 providers/google/tests/unit/google/cloud/operators/test_datacatalog.py diff --git a/airflow-core/tests/unit/always/test_project_structure.py b/airflow-core/tests/unit/always/test_project_structure.py index ffd7aa53c5c71..3c402ff27234f 100644 --- a/airflow-core/tests/unit/always/test_project_structure.py +++ b/airflow-core/tests/unit/always/test_project_structure.py @@ -427,37 +427,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 +455,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 +486,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/providers/google/docs/changelog.rst b/providers/google/docs/changelog.rst index b66b95a3c13e1..7257e0fbd3937 100644 --- a/providers/google/docs/changelog.rst +++ b/providers/google/docs/changelog.rst @@ -27,6 +27,46 @@ Changelog --------- +.. warning:: + Deprecated classes, parameters and features have been removed from the Google provider package. + The following breaking changes were introduced: + +* Operators + + * Remove ``CloudDataCatalogCreateEntryOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateEntryOperator`` instead + * Remove ``CloudDataCatalogCreateEntryGroupOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateEntryGroupOperator`` instead + * Remove ``CloudDataCatalogCreateTagOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateEntryOperator``, ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator`` instead + * Remove ``CloudDataCatalogCreateTagTemplateOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateAspectTypeOperator`` instead + * Remove ``CloudDataCatalogCreateTagTemplateFieldOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator``, ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateAspectTypeOperator`` instead + * Remove ``CloudDataCatalogDeleteEntryOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogDeleteEntryOperator`` instead + * Remove ``CloudDataCatalogDeleteEntryGroupOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogDeleteEntryGroupOperator`` instead + * Remove ``CloudDataCatalogDeleteTagOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator`` instead + * Remove ``CloudDataCatalogDeleteTagTemplateOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogDeleteAspectTypeOperator`` instead + * Remove ``CloudDataCatalogDeleteTagTemplateFieldOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator`` instead + * Remove ``CloudDataCatalogGetEntryOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetEntryOperator`` instead + * Remove ``CloudDataCatalogGetEntryGroupOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetEntryGroupOperator`` instead + * Remove ``CloudDataCatalogGetTagTemplateOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetAspectTypeOperator`` instead + * Remove ``CloudDataCatalogListTagsOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetEntryOperator`` instead + * Remove ``CloudDataCatalogLookupEntryOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogLookupEntryOperator`` instead + * Remove ``CloudDataCatalogRenameTagTemplateFieldOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator`` instead + * Remove ``CloudDataCatalogSearchCatalogOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogSearchEntriesOperator`` instead + * Remove ``CloudDataCatalogUpdateEntryOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator`` instead + * Remove ``CloudDataCatalogUpdateTagOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator`` instead + * Remove ``CloudDataCatalogUpdateTagTemplateOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator`` instead + * Remove ``CloudDataCatalogUpdateTagTemplateFieldOperator`` use ``airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator`` instead + * Remove ``airflow.providers.google.cloud.operators.vertex_ai.generative_model.TextEmbeddingModelGetEmbeddingsOperator`` use ``airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAIGenerateEmbeddingsOperator`` instead + * Remove ``airflow.providers.google.cloud.operators.vertex_ai.generative_model.GenerativeModelGenerateContentOperator`` use ``airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAIGenerateContentOperator`` instead + * Remove ``airflow.providers.google.cloud.operators.vertex_ai.generative_model.SupervisedFineTuningTrainOperator`` use ``airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAISupervisedFineTuningTrainOperator`` instead + * Remove ``airflow.providers.google.cloud.operators.vertex_ai.generative_model.CountTokensOperator`` use ``airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAICountTokensOperator`` instead + * Remove ``airflow.providers.google.cloud.operators.vertex_ai.generative_model.CreateCachedContentOperator`` use ``airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAICreateCachedContentOperator`` instead + * Remove ``airflow.providers.google.cloud.operators.vertex_ai.generative_model.GenerateFromCachedContentOperator`` use ``airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAIGenerateContentOperator`` instead + * Remove ``airflow.providers.google.cloud.operators.vertex_ai.generative_model.DeleteExperimentRunOperator`` use ``airflow.providers.google.cloud.operators.vertex_ai.experiment_service.DeleteExperimentRunOperator`` instead + +* Hooks + + * Remove ``CloudDataCatalogHook`` use ``airflow.providers.google.cloud.hooks.dataplex.DataplexHook`` instead + * Remove ``airflow.providers.google.cloud.hooks.vertex_ai.generative_model.ExperimentRunHook`` use ``airflow.providers.google.cloud.hooks.vertex_ai.experiment_service.ExperimentRunHook`` instead + 20.0.0 ...... diff --git a/providers/google/docs/integration-logos/Google-Data-Catalog.png b/providers/google/docs/integration-logos/Google-Data-Catalog.png deleted file mode 100644 index 93aab9f89436cc3fd2bef417f62b217c60209241..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15195 zcma*ObzB@z&^|~KEV#S76Wk$K@ZhkxySpqB2o^NBdw>9oEba~)Y_Z_(!QJil``+*K z-hX%Z$Mp8jY|YMebyq)C_0$iwuX5-pBq(rjaOev1G8%Aj@7Vr3kP%^L`~u^>U|+)R zq@~pqq@}4`-CeBh9IfEsm=XgM#pL_d@WNF!OEe6~j;XeO77FWO;k%`<5cEpfu{Io< zUQSEcPXGMXB8XCrfD}wniNOJF6K+8X-N1~>&Q30hN%1w&u*KdEhdc?M<@E#a-bvMu zXw}=iSK}L%XWTO9=w?AQ-jv1)x~660(p0r3s6PkQ01r#sVlLRux+GXZc*=RVHxdq6RexM~%itA{0ec z$h5WMF2i$1QTExP_Knb`yB$t6{DxGtdAm5O5PG`AP-J2+Ew%7)q7xEkhG|cVyK_vD zSG-nBckP}(;BO>a^6`|>t2ofJFYY+)rZ8 z)Al=`38A|cj}#9mHFBsLye;v2Ni3OK`2W-N;=p?U+rTSf!At&cgUu$11^>Uz|0DB1 zz5kW@AL;*<`A<4*3zAO#CsqqNBm`;3M>O6K0rgt%#^FPpbF9S zGosGKB~iLeMRm~;X!L;}YrgutU+dlh`1I7?M3sO)1WXj-GhtVlu^JAQ1`j1@vE`n+ zt)q>HALFJA@wr(4}{NGqk&9>%DBoFBSIoex7C#PtD+azlzMl!s*DSs}-P>z6al=ty7n zaz3mlb74E@Xdzpv13nFI&!6692x@o>V}r=R7e=EJ)#$C0TT%Bo#W;xn7=%9|I=3ra z86*IeDLFHxX#jfh(x6p`he$qZqrk5q7PHBgAOSZ|!;9{~Ih$7}8vRA{g@m2b{H;#k#BWmM(biigp(4gg)(=_bu307O&^;2ZeO3Qd&OBEKBCrP%l&K{8L+m-G+oc4}LM6nqjNI+9HizZeRqbx9#0+*V)2#9(9Ac(MLak zBg*4@U$69C!x(@b4Pd^2@twn6n4ztTi6uc;0M^xJXMI}SS*gHRN%*7aV{tpbeK7?L z{L6&1-k4>!GSb2SZunj$H(px=y;C8QvAW==jVOARQ9NN2Nm58`B=VeGEk+YL_*mF_ z5FZaEA`0o~&R|W9HK-7FrDfsJHz=m}if)nv4?{5o98X!rb4_O@DpULDXBD~Yn4HXu zSa?p5A8`i-%pIrES!1(ptn$RqdlMIFI2bUgiK2He*bA<+yt_AJ2h8;*%n?7nXN+?S z3-w0N79nR9kY{rMpw4B9Mfgk*xgmrXnLdGkl0#%0B1%lXu7&fKEqAOt)W~AgB;^D; zS_ZM?1s(1`&W*%HC^>!2%0U_2Cb1U`fL><7Z$ze>l8c@B?*CL-Crr6x#Y;||MRhr; zH?&WOd#=R-$-qsxaPj&G9?&!4k*iZ;rqa?ns@l?J}gq?5ZG*Iz3j1{|Qw4N0eUGP0|i7+0u0 zS8rwm(IcTSp`=i+(&39oXCJ);24-%ORTI; zg!4Gcj%Df<6SdV6nB5>1p^7PbEb#y78|FM5iBnv2PP$(sn2pIi| zBVFl>;4u5O|A$i26bBmSZGMBJD3gwkI?7xo17vjhmuKuJQ8kn#>W-H2$rbugZO`Xp zCuxU-Wzv za-pmx6J&?o&k%k6J9{L$K>`M5cdaQxWg}|dqU}H(PS_Dv*ue<>uo^rory$aVBeLC5 zT3&tZ zY7xW1=R?N*U{oAi!Z5>4y9Hn$cyo1H-s4ZUX>N<9pItjOLv;bwj(4Ji@1B#ihwcT^ z#uW_!+B>n<C@)BVf!tADKq4 z@XRsnZOvnqDJ1_y8Z0cLI@}5_`ODN+E0}KVlS(bM{UVVr-}p93bsC5b8;k24vm0nF zQ~MXpOw$uV>qW0Vb5IUi+WSHU!q{v<4K5ap@}Fk2ll2*^eKK%I>t3>c6Rz@g1xrA( zN*`kV_Qd2~^X3Xe{hY%}eAZK~6@i&zfdgNM{N3C;wm{~cEbS27o6Q~iQPL-Nfq7tx ztZc3L5X>Y6o*A^R7{1p@)p31e8>jW)^I6>r^i|r7&lV&2%Gc30T6^UCcpKVr_TKnj zIB>>t6K^obAZA~7*RU4KD>8S=cU}4ubW77zkwiU~17~8m!)22s*KxTW6sS$imVMv` z8P7VALx)+@9ES*7I-^q=CN|OBQ<4|hw5zhfyNKTMRT>dW=aJ**t#!BfBhhUYbcD*QP!Xexh|O*3jbpz)!j5@ zQzk39U#pS*UCH(=kj&G2sMNb&gW+dYMop0TL!C{^id;r4LzJKk+-GxIwN{|&CH=wW z1k>-9ynpca`k)A4h+rvea6jjsU&DEY5+pDTQ=GWjgQ^6&>(^I1==tR< zVoWdnCxW3kHMWeR`)Qnnsm9hoJGlviAn;=CT}olACiX>Qr9OhG5oc!M!%G^0;CY-ydLS7Citw}*VhMMl*aD2+OKhd^Q6U*w9lW^h{niL1d-^JrRG388DT`_>Fky4NlbZ9y z)zDejJaHe}!R&B6OX9&lEbFo_UHS+dHUZAX*ilt0Mm#kz2(y`3u&NTFHSeiw<}?#lmz(t!#mZkMjj)JgbF z{{EU2Zo&U=ooj_vuTwa_)>yPDpVxnz%4*=ju`RWSB(XX|_g%_F3*wFXw)r_jLj<`T z?(@sxHyI*nW+OcQFGLJ>zwP#X-ev_E;;-xU+rF?ayan}`bioh#nR^)Zj)O=5+}VAs5=1AcTr+bJ;H;L9fizWpS(w-MZ9`_zkYAJi_cRPutSR-s7ACm ziX(rg$_~w45`inLm1H!##Nj`NC)7ZbYi&(QbP-jP4xbOLf9DQ*`JtjEsX7g|PcJO5 zcg=H93!Tjl6S#%#cESpeqyy2(zQg{1b@7T}CxDhlFfoh>%8?jca?+!QxiG2K_Raqr7k?wFaG1D0wpabH$?HW8$ zZy)mHFM3IVDlhLjif+{Z1AXR#>-t0-SKkmSJ0TaB-qJRl#OdVN(_C4<%kB@kRoIde zWQ#1_-WMMh1^DTSnN7-N*dk(AseYU*;fJ6ap!AQB{QL_*g6%xs6pb3f*57_f`TE9N z9}04*UbVsDv9%Ouh5KkFD&Ar+%{Jx|StFk@feqQC|v_LArhDxfG>|u6^$J5VLy4>F}tZNO`i8$OpEVC0W=PMn) z@3X`WYIwJ+i{>zOiQnKYWV}Mg{Sbpa64~N#jgC5}w2WRxT17PU!4m=Bb)G#7$jv4J z?4lp!6lmTAlJNE54w?trC`$4cAu(WL;4nixNcA)M%0mw?Nt^8@x*Ouz&%E3&%_~fH z@LS-L?T0jPm}<=LzwUWTqI<)Du>Zv6hLJSsSXZH{h<`75vtWggjA>5NsbniAMs%bo z6MdupNWW!;pkTH}LyYHzIh-%)BH?}b)}Ksx2On?CYd_kGz&t~Jxq*FJ=a#-!xMatn zOydX-<EC%}m9=z+VsY+`)H8#}~~0%7GqPW6PWh z9S#Vr*}N|)#I16G{)Dc1@mbz z*)tto_22UbCRI9`s(bJyoCNj{+ru=H?30img7-2gxGaua!vx^Jf1H(6o@65?oxD_ul5i-$D{3QH<`R1BN*)gZA+80EWKf2nd zk<;?i@+FTW6m43hK_|e z1gSvbZHm6`;BZ=9TBib4PK?bc6K5~N5P^+v{x*1g_?eEN{E{Kgu{klL(0zSIP7%&qN!&2Cs-N<8 zgDtqbA~BHh(KW3@Ut1SvtJo$Pw78g<_-mk2lp>R|jQZ5#xKVcWmp)h$Qk(sx-+w69 zdP4F7E=fe0nI($F-XunbhPWRn7U9ZSl?uyE+*~}y$LH+OSXtb~K1QcOuMc+5-Zt1I zf{K5vCFC&hHzC0ugYaP{-_Z0ak)jRAJ|%CqW^&zNzS~BSCGtHs=q&GGmMu%iD>r7R z+Z|J7feXaOUQ$qx6jaCI^ZpY2^RcoWt&F6qCG>l3_1;Q_p6K}KKTAG*;h>h%%pSE# zS!}T?*UuKfKMC_q=g<(2u2)krC!>heBLh2UOxNEzrRJ6nW?r78L66ov-B*KPfoCAL z>c#o9;FIc&d!bOz-QQVR)Y@VG6pw%MC$;)WNE1Z-3$KMxUm6n@-D=-J2g!u&+W+N0Dzmrm=rRhx0qSl<$g$aFX5I3Z$^d`y@mZ?Hi zv^9P79lR9b5z*%($n&c#u{HXZ@QSxw&zO{!Uo#PKEYbZd-t7ka7~S}?L6VfgmQsfq z6T+e2L^0l#XZuYCWsb6z?tS^trwjyB$ED6uG{`S5FlpYil=u3g)Ak6Wsq8P!D;_@! ze%mubc7w~J621mcVSdVyuGEruxA{#Co{CkKy&DdKzH?yz=J7v|cMV(1330zTPHB_) z!ZVR^X|{))}`n7iCnL9+t{xOXdXR16_MYb(oI};AI zq0uuMJwV~fGYYD9*x6NKaTG`)+a34c)sK*JrFg28ORn$qhy4d%Ah2-Vnx(bKsI`wtP<-#g`VLc zr{Sgb&dEdDvs6^5o(H;^uSNd2uT6Q?<-_9(x=a9{Ax6sO4vH{C?2n5Qs~h6C(}=*_ zW6>ogPqoa>mm+*`^JMlG9PzYymeswkMn)j6zKN}u=gkjOs~No0uRW*+=uX79F6|x~ z2niff$!G(7W4Wfi%@n_Fw4AJUh2nEn&!^M8L8C*xTETfb3*qQl%(FWhs?ru^qxO{2 zf#vK*`FdZG>W@gACH5Bl>3BAo6YMAq)JE^sx!lk_KC`&8X<(ATRsubj)ip1$+w|yT@R}y^1 z)@+-5`u7U92?@E@C2+D=ZdV(y);s}gPHb%^F4j6$UtLQHlO1IsUy8I6`E^|Fz|q!V zqTeRE=(}J~Jrg~mRs&u_eewyHj}LeG-H*#6LDaB7Hh=v-K(*~9Rmc3rdU)jIwBAwZ z?|6F|u+>M0Z#|$kBqEEoJ7%cq+dh9&B8nk}1CSY{u#psZKc!v}quK!a?n|$gYIElD zC@{-xKY|~}%ov?IIzH4}y`455i5_!ye-HMTOEBAgwAOKPC=)oSO>;1Qx!?GE^j)d6 z?*_8-Ve{Z0S4UIlci8fMxV%Kn0}#Cd+Co3*RJ-gcTiE{*c`{s_R>Wc8+)j^+ps8;F zg(tfGt{hntJj_1CpWU_N!*h=;+jJM{Xj%^Pov39r^9!^g?S9ZbyIP6h_s2PvjyWE| z?b#&zs|}2;xVaGh^zz1^6FvfvqrZ}~}YF}N5hk?W@CAZ@GrLHtaMm4s*1o=Az`{-kTj{-umjS3l@g=4PP)V1it+H7s8B@Bx=F%^ED`B8|Cv@} z|BOt7LWOq6X zgZn>an(gaI>Q9ZX{2z@$w&wbUA@no6F?9eIJ)!-T`ZJ=STW@(5JVM$#>$;)}(Sn-x z@Vqq&8gau>Ld-r-&h7);Q9nmX*a`{m5WfdseGCGF!*k_;tt%8*=BXU!E+ zcD%{UEiQK2*U>uDTxa*^vV3^nCJ|A)JYjg}SjDr9QdCm!O~*scBCA;1hWPUBQ9b_3 zEC}J8hJn{j?oICZTo~R?&XsFBP+AH-?n*|#<0KY5+@`owuG|8*Z;xep-r#o6q$h*c zrl1ioMmY}3*m1@NV-l`asK6O-Ci4Cmz5-8>X+Ae`&#?ht@Oi^T6x&d3%8`rJ6NL3c z69xEMHyA$xKD<6KqX3uCJDAD(M~ana?u7K!)`|opG=-FHdwJGop`|-!_GNLg3vq%q zu9-NaVL4#pnuQ{d(q{PjkhEH%ao-oy!P~nwD40+zFrv;D9MdXYPGk(C*i9wODcO~8 zZU@?fA$3kEQtHrq({DQ8c8P3_7>yS7=7>0{CxQ;S&TO?!b`kFMLD4=H&c>tsd6C#HeIh6FY-M8>%H!!!kK80)xAn>ryk5CPpqF{$L+XillgW>p4Hgzt=<7I zt}^HzLaZ7+VTRk!kxoEh=j?Y^73+?iLpGqjGFe6~mb6KlxmyBD;u?nAp4Uy{>&|ai z&~v(`Bi96^xjc%X_9dUG1O2(JKwDtH2Yl9VB8=K~&rthV4?i&zcZ?$J!EK-UJ#7$` z(`I0hIytSw3(d(ul`F@GA4*B%thFiB( z=H&Wb|FhhtnVF~(3)^jbCW?);@+7xqkrnx0?S9>}@lMjx3VHA!MUbi8F2@*`MThl6?(5Hi z=W6C3GefLt9Z)An`$9&zxJI4e2>pcksvA*ft|PihI7=0-ep zz{6ZC`Js!p+T2pev{-y4dP-EbJD`RYZPXBY4A3V42&>d@+!l7?cu42#V1MUiyZn_> zM}^mEt>;+5&aPj*ZZ-C^rw2wep!Mja?(XyHr);*Zuzqz)dgMSJjO#O-J~dldqug(G zn}UtJI4onqbyHfX8U&T6>~2~qZqg9OMwJ4`5CD`X<3BDvXm=Nqx+0J(laxG+JwI#7 z{rRT}nP(OEX^zy&#}326Vz&WHiZIx2e>o)&GBpa^q~NT~b#-R|z^SsGw>ZTn0n(^a zqDrhZ$uqQ;(N|OR#1!1l#-3;MM(&dk1X19DQActX`kigs%ykr-w}+rM-$5@-h; z1?AgPNH2a!8fj;+OIt0c%hbYBM2tii0S~9KwnfhMZeQ^>$BnyKO}q>Am&JqWP}Qro zPw}I#Boi^{ppVW#yi{+l?{~}I@Zf{XhOlXC4zK>jB0|<@7{5eOsiBXfPwOg@8I$0C ziCe|U>NVe~Clo)X=cQh>Gvi7R&Zlh^&c*aSkKu0o?^v5Gp)&q> zC;@BLzeDtYr%Lz(SDx3UD%%07gOkF-{;1pVWyQ;r=?zQ=!Cz?*Sgrk)^8uepmrdV# zqF4s|>-lIaW_E+aD{-gdc~s6NjBRcDUvE3aAg!f%!7qJQbEl_c_TAkfdVU+AtxN%f z`>7>{Hvg;otCIJlO4&>Ik+UidKG~t*`%7yh;?du2?i5B?Wu+aih})V*9*Ea<()U+{ z(re-wK~r@CM&w6$49CU7(?dDK*#Eh!$zL#4K8#sz_+I1^7_YAN4$gg>Y{|KO84aWI zOo8w=$3|a<17~vcBJCMY#OKC&jtupSymzHL+qE0>2v zpS|ezD^lxc&}_J~m|*(X4;3skqJo@I-L!;)E{3GW{Zj=Z^3o%&m z7NaDv%m2+b=w&mj&wy_c4+P71oxM_Rm}e&^r;+00C-?j!0h>k5ML%e=+z(09wmKOS zPf79$I-Mc|I*W5n936{`=Qjv9qdmWI#k-+ncin#epk%G3smbNa+8|)I=u?+YS#7tq z6Hdpp2$0y#B87m48=vRbiIij;;F=#CQ3!gDfM{=Ltc58B!~VA`F*O!e1j`Ur9Ik5wQ}bG zDEH|l4TZ&A;Itxa$n7{FlyKe7YPE|aR<@5omw3M}W5OlSj4T1l$@#}-tj&}dHTXXH zTPI_cax~hCG~q4G`{)T5ER+Am)*Kge(K<*A5bcbf@=A0#i!HR6xS;rN{DwXl3AR{& zUTyW9nT$$54cNM*(#ebP4VP2xo1V3alD+maXN|x7y)}j4MvckS&C{6(ez-|Q5c#4<$o5UzhCKMb_}ecxZXs@L;7oE?z@gM< z>?}{hEd(E}%kvdMB^zx`?{L|`f1aE!rw2RZtGNwcq(zAiHrNP-(heCuKzg55eQ4#f zNXuWG4fM?)W(#MBFUt_QsU<5wV^wbP{^O+n_K|}@*l-VbkXB0M-t2i#j^MUj&=QK% z3Cdz^P4c{t*ZFC=I89d?`n7PHw;u0E{u?K5YoOt#4gvIssnEum3Fb3aUchpnLf<%D?njh#XGqg(GfdjpZG+`Tp= z%tALo)1d)wiX77YO+Po55mC_h8=wfFzViDj#9uxfjfFrk2ma__*W#f${|Di<2P`WK z2Wg*IY)ykrDT-}IUe2}>+DQ|#oLbsr=<sE?(A&<(yI} z@?anhmM^%kOqZg;7K$5QnLd0?u??*KP1{AY#q94wl|Ld>3kCaH^O+c^PgGf0=*Hq# ztuOZm18;F8GnR3U_o)cfpL^$>%`ajPSzjeq10#G?Z!Wh5^aXt@IN3$#Nm#SLdV`|u zaLPTiLK*!~1e8aZUhhp*O&7;5LxbmXc57J>eAe?$QQEIs*nc}Nnty)M-k2WeoH5#X zG|5P>mP~7ypU`NAa=B^6x`=TnwU_B=P;@??`mKXV)|S?X_r8k>ts4?dfXt_^4=Z2q zD%=yoXDTxJ3q$$Zg?(>N2=v?gDvjs9niQ*FmrOIp=h@DpRdXv=8Ai)JmK8_B>xZ zkm)u2%Ym*t$?a@Op8%~XrO^@H7N!Hv9Vu-~c@~RImDWxO5VkO`+CqG&dFEGy6JQT*HPRxl zF81`3cro9ofOx&&m-U>fu}%ZV$$EyZU6t(#vfpMde;YS*e3|L%UzXE+BNdv|-1Jlw z_WaJW(+KVv2}M{c29|WQJ3=XmmeMhjgyp^~0p7Y+Qze0DnWn3}QWt;7^6P#B^ghTH z7B~JQ^~4-C&#F4}s=qMR{X!2KEzW|4uC_}fkOP^Ou-e|eUDKiYD9aOuj?#t9=tE#h zo<*;Q^t#{hHFe>Q@mbNxnB$dJE66qf@`B})(J8Xa{PutD!5E%nPw(0y6zw`}$y+=r zTaOfTG@q}>t@qZAhkq}T3cQ^WnLukKxRb^s+mKS3onyB{{M?)=GW7~xq?DplT^ggT zQmv$lGyELzS*>;P_S14J2c{~)S1r4$`RMWp8ppw((g>iY8teO!hIf`Q z8ekc`5hZ};}J%$f_|*H&%r zA@f;uQ-Y%aq#Hi2*Hv8F=q0s3O1vJPxmySHc{xPZ2l)Ol*XisrZ6=W?5VW+UGr7K( z={0U>i$P~4c4UyfbrFuv`}a3Z{>j6=8Up6Tta7UYt~|FL0Z1qAt4+Dt#Qu(TzIg-1aT16V0n@qZA^p<@REOWzgulSp z+6gmp)N>;P<5?vo7QW!b3>jKz$^CB=tT-l&oMWq(*RNf59n_rK!prT_#*0f|Jmlor zXgI|T0Q;YxL_0z8*`yswkIo31E%%#?Sw;|i;JWJF2yPv%z9IzXzjF;U)aGNcn_OL% z@PY+)N|%4W zoJ#l#&)5&(ztoa5iV9?R1_S@FAueZ2A=MR4&AM_&gni#tew~X+N7Xb^KpYk;t4&&(Nl1J=))Wgh+oz_&(6ZAij)OD3 zpP|~C@74|XwsQt_-0Ag~YGmY&)J6!WSp1JrMvBDW9t#oeS(Sj}c*lAxeImT5&grH2 zLV*uKe8T!jikI@yz$X~t$^qbSqjxZ|C>2G#(WXX!QDCTaZR+%GDjZS+mlCysv4m|4e`rw&Ov zu+e~2x|XLXR5Q3@d}mww{o zCPR?H(sw4;?TucX?XQafs4ph_sA#aY7;QU%>mo+PuFYvDgHmH2<=ezV2#SZviL7+mC8&5`xZjA_P=n6|Ale}iSA+-pjk(^o&PdbP6A5VK{U7>Dv@Lpx*KPsqZu2^^M7PYHve^S% zPXw^6B40=YBB}Y*<$00r83+4LUt2hS{ZVG1gXKa(ILBW4{k6G@v|)u1+RMVzfyStD z$x8Z2BkR9{(z;L=F*+ePS#|)NfPigR!vf<^RuSpW*v}8XQ~nJm97U;0K3Id@4`WP+ zmGsZ**P$hNFc6c;r%)lZWA*HXM_wiOU?s22iezp@t_gv(-a#|azA(f5)ZhAp`vm?n zqHgF{W_HZvN&=0OT-a401F(9~k;jeJ&_Ejtaqs&K9tE&x8~nx_*D=J~+nTp?7M-|ubsMB|7 zXSt_^za~v6WGlPXE8}6~)Q8Ip-5xG!R3>Ae(;i0A8PF8vU$>C@PLpz7&{4&IlDaKR z`dP-|TXn&9^{`voMD;*&7gY%>0516pv(DIbirnDHNM*@x>bBgGx}pp#F33wuN9Mmg z7TBvuho1BQ4$oMnq~Z2=#ONU8d8R|7dTe1xpue-fpt}HBM`Cst7G-ea+_y2*C33r)h;Zi{ro_quS-s^wq$X;Bk19h z{Q0RCm#u1Jp5lC0zU6aT@onYwYDp^F5UUyc%3n2TGi4Pi{|f?)mD|f+JRSb}>lGLR z&`-N*$4o7He^`7g5RsyippY9jD zN-vuR`H$jauTNvLte?(%RuBo46Z-;a054ht8&{}&Ua1=6g#`@8bb0Z1=(>yE@xI#l zCMN3a$*>ZV?XjHSe!lT?8X!<#R=|{?v8P~k8ZEJZz|p3^von^B8vGHij3^+e43Yv? zM0vsgRiZ5d>hiE9t7n+6v;6WS>P21}tIgIu1DQ#BX98gIY#^AZA?B~9G8R%@ zC$#Z2{Hq9mWIrm^8?6_eiQDjZj74TDIu7P#(7&l97jA8~HjcHbM3YCG8bxw7MkCqy zX{FJs08po=D>&_@P+~1CjqqWtsM0Uf)vzj>u-$rI4+cN?$||ZV)3|v40;O8@N)K4* zf+EjkG@$jrrzswwIiGzJ{W8k}u0zpi&eMP&|Y_W(Mj`SQ> zY>=deE<*Q9%fyL*f^1}8ZegOQ@@Neci_+*jj6u-ymn#vI-$fdAM0NG=1R2$HJlNyA zgXm4)o-j&&(%6R!sPvX1C#wi%7ff=h7@#g)0R#hi-{#XDekbCb&;S-o%V#bUY9bv2 zCd}FH-3Kc-WJYgd2oPBGp+ZuXObN2EH1t%!K3H+2_!8z0yUVCfUu@~X7>(#KzHrxj z_C9SukyTQLB`%a6Rk0z~i>R{2&y!DeT_B0^G+xm}5*t)#cIP&#B!UK1)o;Vax)@Yp zW1CVm)fgAd4xXxmCZZKIRpf;8*!#NM7+wu|76_@U5D|+Ka2ihwFJYTf931w<86$~&te1_c5W36DE;^COIT8W_& zukPIvnSo1M{lKtcYu9qJn6&;j&zGA?IF`NvFg{y*eKd?Mk>TYKYyS}xV~UScj>XX| z!qL<_tdypi&R#}B&TcfB?n$RH?Zrt6i3c2ql@w@K-DI@&`__. - Mapping between entities from Data Catalog and Dataplex Universal Catalog presented in table - `Mapping between Data Catalog and Dataplex Universal Catalog `__ - under `Learn more about simultaneous availability of Data Catalog metadata in Dataplex Universal Catalog` block. - -The `Data Catalog `__ is a fully managed and scalable metadata -management service that allows organizations to quickly discover, manage and understand all their data in -Google Cloud. It offers: - -* A simple and easy to use search interface for data discovery, powered by the same Google search technology that - supports Gmail and Drive -* A flexible and powerful cataloging system for capturing technical and business metadata -* An auto-tagging mechanism for sensitive data with DLP API integration - -Prerequisite Tasks -^^^^^^^^^^^^^^^^^^ - -.. include:: /operators/_partials/prerequisite_tasks.rst - - -.. _howto/operator:CloudDataCatalogEntryOperators: - -Managing an entries -^^^^^^^^^^^^^^^^^^^ - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetEntryOperator` or - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogLookupEntryOperator`. - For more information please check this :ref:`section `. - -Operators uses a :class:`~google.cloud.datacatalog_v1beta1.types.Entry` for representing entry - -.. contents:: - :depth: 1 - :local: - -.. _howto/operator:CloudDataCatalogLookupEntryOperator: -.. _howto/operator:CloudDataCatalogGetEntryOperator: - -Getting an entry -"""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetEntryOperator` or - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogLookupEntryOperator`. - For more information please check this :ref:`section `. - -Getting an entry is performed with the -:class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogGetEntryOperator` and -:class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogLookupEntryOperator` -operators. - -The ``CloudDataCatalogGetEntryOperator`` use Project ID, Entry Group ID, Entry ID to get the entry. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogGetEntryOperator` -parameters which allows you to dynamically determine values. - -The result is saved to :ref:`XCom `, which allows it to be used by other operators. - -The ``CloudDataCatalogLookupEntryOperator`` use the resource name to get the entry. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogLookupEntryOperator` -parameters which allows you to dynamically determine values. - -The result is saved to :ref:`XCom `, which allows it to be used by other operators. - -.. _howto/operator:CloudDataCatalogCreateEntryOperator: - -Creating an entry -""""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateEntryOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateEntryOperator` -operator create the entry. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateEntryOperator` -parameters which allows you to dynamically determine values. - -The result is saved to :ref:`XCom `, which allows it to be used by other operators. - -The newly created entry ID can be read with the ``entry_id`` key. - -.. _howto/operator:CloudDataCatalogUpdateEntryOperator: - -Updating an entry -""""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogUpdateEntryOperator` -operator update the entry. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogUpdateEntryOperator` -parameters which allows you to dynamically determine values. - -.. _howto/operator:CloudDataCatalogDeleteEntryOperator: - -Deleting a entry -"""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogDeleteEntryOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteEntryOperator` -operator delete the entry. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteEntryOperator` -parameters which allows you to dynamically determine values. - -.. _howto/operator:CloudDataCatalogEntryGroupOperators: - -Managing a entry groups -^^^^^^^^^^^^^^^^^^^^^^^ - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateEntryGroupOperator`. - For more information please check this :ref:`section `. - -Operators uses a :class:`~google.cloud.datacatalog_v1beta1.types.Entry` for representing a entry groups. - -.. contents:: - :depth: 1 - :local: - -.. _howto/operator:CloudDataCatalogCreateEntryGroupOperator: - -Creating an entry group -""""""""""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateEntryGroupOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateEntryGroupOperator` -operator create the entry group. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateEntryGroupOperator` -parameters which allows you to dynamically determine values. - -The result is saved to :ref:`XCom `, which allows it to be used by other operators. - -The newly created entry group ID can be read with the ``entry_group_id`` key. - -.. _howto/operator:CloudDataCatalogGetEntryGroupOperator: - -Getting an entry group -"""""""""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetEntryGroupOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogGetEntryGroupOperator` -operator get the entry group. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogGetEntryGroupOperator` -parameters which allows you to dynamically determine values. - -The result is saved to :ref:`XCom `, which allows it to be used by other operators. - -.. _howto/operator:CloudDataCatalogDeleteEntryGroupOperator: - -Deleting an entry group -""""""""""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogDeleteEntryGroupOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteEntryGroupOperator` -operator delete the entry group. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteEntryGroupOperator` -parameters which allows you to dynamically determine values. - -.. _howto/operator:CloudDataCatalogTagTemplateOperators: - -Managing tag templates -^^^^^^^^^^^^^^^^^^^^^^ - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateAspectTypeOperator`. - For more information please check this :ref:`section `. - -Operators uses a :class:`~google.cloud.datacatalog_v1beta1.types.TagTemplate` for representing a tag templates. - -.. contents:: - :depth: 1 - :local: - -.. _howto/operator:CloudDataCatalogCreateTagTemplateOperator: - -Creating a tag template -""""""""""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateAspectTypeOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateTagTemplateOperator` -operator get the tag template. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateTagTemplateOperator` -parameters which allows you to dynamically determine values. - -The result is saved to :ref:`XCom `, which allows it to be used by other operators. - -The newly created tag template ID can be read with the ``tag_template_id`` key. - -.. _howto/operator:CloudDataCatalogDeleteTagTemplateOperator: - -Deleting a tag template -""""""""""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogDeleteAspectTypeOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteTagTemplateOperator` -operator delete the tag template. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteTagTemplateOperator` -parameters which allows you to dynamically determine values. - - -.. _howto/operator:CloudDataCatalogGetTagTemplateOperator: - -Getting a tag template -"""""""""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetAspectTypeOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogGetTagTemplateOperator` -operator get the tag template. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogGetTagTemplateOperator` -parameters which allows you to dynamically determine values. - -The result is saved to :ref:`XCom `, which allows it to be used by other operators. - -.. _howto/operator:CloudDataCatalogUpdateTagTemplateOperator: - -Updating a tag template -""""""""""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogUpdateTagTemplateOperator` -operator update the tag template. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogUpdateTagTemplateOperator` -parameters which allows you to dynamically determine values. - -.. _howto/operator:CloudDataCatalogTagOperators: - -Managing tags -^^^^^^^^^^^^^ - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateEntryOperator` or - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator`. - For more information please check this :ref:`section `. - -Operators uses a :class:`~google.cloud.datacatalog_v1beta1.types.Tag` for representing a tag. - -.. contents:: - :depth: 1 - :local: - -.. _howto/operator:CloudDataCatalogCreateTagOperator: - -Creating a tag on an entry -"""""""""""""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateEntryOperator` or - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateTagOperator` -operator get the tag template. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateTagOperator` -parameters which allows you to dynamically determine values. - -The result is saved to :ref:`XCom `, which allows it to be used by other operators. - -The newly created tag ID can be read with the ``tag_id`` key. - -.. _howto/operator:CloudDataCatalogUpdateTagOperator: - -Updating a tag -"""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogUpdateTagOperator` -operator update the tag template. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogUpdateTagOperator` -parameters which allows you to dynamically determine values. - -.. _howto/operator:CloudDataCatalogDeleteTagOperator: - -Deleting a tag -"""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteTagOperator` -operator delete the tag template. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteTagOperator` -parameters which allows you to dynamically determine values. - -.. _howto/operator:CloudDataCatalogListTagsOperator: - -Listing tags on an entry -"""""""""""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetEntryOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogListTagsOperator` -operator get list of the tags on the entry. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogListTagsOperator` -parameters which allows you to dynamically determine values. - -The result is saved to :ref:`XCom `, which allows it to be used by other operators. - -.. _howto/operator:CloudDataCatalogTagTemplateFieldssOperators: - -Managing a tag template fields -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator` or - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateAspectTypeOperator`. - For more information please check this :ref:`section `. - -Operators uses a :class:`~google.cloud.datacatalog_v1beta1.types.TagTemplateField` for representing a tag template fields. - -.. contents:: - :depth: 1 - :local: - -.. _howto/operator:CloudDataCatalogCreateTagTemplateFieldOperator: - -Creating a field -"""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator` or - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateAspectTypeOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateTagTemplateFieldOperator` -operator get the tag template field. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogCreateTagTemplateFieldOperator` -parameters which allows you to dynamically determine values. - -The result is saved to :ref:`XCom `, which allows it to be used by other operators. - -The newly created field ID can be read with the ``tag_template_field_id`` key. - -.. _howto/operator:CloudDataCatalogRenameTagTemplateFieldOperator: - -Renaming a field -"""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogRenameTagTemplateFieldOperator` -operator rename the tag template field. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogRenameTagTemplateFieldOperator` -parameters which allows you to dynamically determine values. - -.. _howto/operator:CloudDataCatalogUpdateTagTemplateFieldOperator: - -Updating a field -"""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogUpdateTagTemplateFieldOperator` -operator get the tag template field. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogUpdateTagTemplateFieldOperator` -parameters which allows you to dynamically determine values. - - -.. _howto/operator:CloudDataCatalogDeleteTagTemplateFieldOperator: - -Deleting a field -"""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteTagTemplateFieldOperator` -operator delete the tag template field. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogDeleteTagTemplateFieldOperator` -parameters which allows you to dynamically determine values. - - -.. _howto/operator:CloudDataCatalogSearchCatalogOperator: - -Search resources -"""""""""""""""" - -.. warning:: - The Data Catalog will be discontinued on January 30, 2026 in favor of Dataplex Universal Catalog. Please use - :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexCatalogSearchEntriesOperator`. - For more information please check this :ref:`section `. - -The :class:`~airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogSearchCatalogOperator` -operator searches Data Catalog for multiple resources like entries, tags that match a query. - -The ``query`` parameters should defined using `search syntax `__. - -You can use :ref:`Jinja templating ` with -:template-fields:`airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogSearchCatalogOperator` -parameters which allows you to dynamically determine values. - -The result is saved to :ref:`XCom `, which allows it to be used by other operators. - -Reference -^^^^^^^^^ - -For further information, look at: - -* `Client Library Documentation `__ -* `Product Documentation `__ diff --git a/providers/google/docs/operators/cloud/vertex_ai.rst b/providers/google/docs/operators/cloud/vertex_ai.rst index 8b8d06e385a8c..dd840ec6750c3 100644 --- a/providers/google/docs/operators/cloud/vertex_ai.rst +++ b/providers/google/docs/operators/cloud/vertex_ai.rst @@ -576,70 +576,6 @@ To get a pipeline job list you can use Interacting with Generative AI ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. warning:: - This operator is deprecated and will be removed after January 3, 2026. Please use - :class:`~airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAIGenerateEmbeddingsOperator`. - -To generate text embeddings you can use -:class:`~airflow.providers.google.cloud.operators.vertex_ai.generative_model.TextEmbeddingModelGetEmbeddingsOperator`. -The operator returns the model's response in :ref:`XCom ` under ``model_response`` key. - -.. exampleinclude:: /../../google/tests/system/google/cloud/gen_ai/example_gen_ai_generative_model.py - :language: python - :dedent: 4 - :start-after: [START how_to_cloud_gen_ai_generate_embeddings_task] - :end-before: [END how_to_cloud_gen_ai_generate_embeddings_task] - -.. warning:: - This operator is deprecated and will be removed after January 3, 2026. Please use - :class:`~airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAIGenerateContentOperator`. - -To generate content with a generative model you can use -:class:`~airflow.providers.google.cloud.operators.vertex_ai.generative_model.GenerativeModelGenerateContentOperator`. -The operator returns the model's response in :ref:`XCom ` under ``model_response`` key. - -.. exampleinclude:: /../../google/tests/system/google/cloud/gen_ai/example_gen_ai_generative_model.py - :language: python - :dedent: 4 - :start-after: [START how_to_cloud_gen_ai_generate_content_operator] - :end-before: [END how_to_cloud_gen_ai_generate_content_operator] - -.. warning:: - This operator is deprecated and will be removed after January 3, 2026. Please use - :class:`~airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAISupervisedFineTuningTrainOperator`. - -To run a supervised fine tuning job you can use -:class:`~airflow.providers.google.cloud.operators.vertex_ai.generative_model.SupervisedFineTuningTrainOperator`. -The operator returns the tuned model's endpoint name in :ref:`XCom ` under ``tuned_model_endpoint_name`` key. - -.. exampleinclude:: /../../google/tests/system/google/cloud/gen_ai/example_gen_ai_generative_model_tuning.py - :language: python - :dedent: 4 - :start-after: [START how_to_cloud_gen_ai_supervised_fine_tuning_train_operator] - :end-before: [END how_to_cloud_gen_ai_supervised_fine_tuning_train_operator] - -You can also use supervised fine tuning job for video tasks: training and tracking - -.. exampleinclude:: /../../google/tests/system/google/cloud/gen_ai/example_gen_ai_generative_model_tuning.py - :language: python - :dedent: 4 - :start-after: [START how_to_cloud_gen_ai_supervised_fine_tuning_train_operator_for_video] - :end-before: [END how_to_cloud_gen_ai_supervised_fine_tuning_train_operator_for_video] - -.. warning:: - This operator is deprecated and will be removed after January 3, 2026. Please use - :class:`~airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAICountTokensOperator`. - -To calculates the number of input tokens before sending a request to the Gemini API you can use: -:class:`~airflow.providers.google.cloud.operators.vertex_ai.generative_model.CountTokensOperator`. -The operator returns the total tokens in :ref:`XCom ` under ``total_tokens`` key. - -.. exampleinclude:: /../../google/tests/system/google/cloud/gen_ai/example_gen_ai_generative_model.py - :language: python - :dedent: 4 - :start-after: [START how_to_cloud_gen_ai_count_tokens_operator] - :end-before: [END how_to_cloud_gen_ai_count_tokens_operator] - To evaluate a model you can use :class:`~airflow.providers.google.cloud.operators.vertex_ai.generative_model.RunEvaluationOperator`. The operator returns the evaluation summary metrics in :ref:`XCom ` under ``summary_metrics`` key. @@ -650,33 +586,6 @@ The operator returns the evaluation summary metrics in :ref:`XCom ` under ``return_value`` key. - -.. exampleinclude:: /../../google/tests/system/google/cloud/gen_ai/example_gen_ai_generative_model.py - :language: python - :dedent: 4 - :start-after: [START how_to_cloud_gen_ai_create_cached_content_operator] - :end-before: [END how_to_cloud_gen_ai_create_cached_content_operator] - -.. warning:: - This operator is deprecated and will be removed after January 3, 2026. Please use - :class:`~airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAIGenerateContentOperator`. - -To generate a response from cached content you can use -:class:`~airflow.providers.google.cloud.operators.vertex_ai.generative_model.GenerateFromCachedContentOperator`. -The operator returns the cached content response in :ref:`XCom ` under ``return_value`` key. - -.. exampleinclude:: /../../google/tests/system/google/cloud/gen_ai/example_gen_ai_generative_model.py - :language: python - :dedent: 4 - :start-after: [START how_to_cloud_gen_ai_generate_from_cached_content_operator] - :end-before: [END how_to_cloud_gen_ai_generate_from_cached_content_operator] Interacting with Vertex AI Feature Store ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/providers/google/provider.yaml b/providers/google/provider.yaml index d73c4ce1dd7d4..4f0d1d90c81c9 100644 --- a/providers/google/provider.yaml +++ b/providers/google/provider.yaml @@ -310,12 +310,6 @@ integrations: external-doc-url: https://cloud.google.com/dataproc/ logo: /docs/integration-logos/Google-Data-Proc.png tags: [gcp] - - integration-name: Google Data Catalog - external-doc-url: https://cloud.google.com/data-catalog/ - how-to-guide: - - /docs/apache-airflow-providers-google/operators/cloud/datacatalog.rst - logo: /docs/integration-logos/Google-Data-Catalog.png - tags: [gcp] - integration-name: Google Dataflow external-doc-url: https://cloud.google.com/dataflow/ how-to-guide: diff --git a/providers/google/src/airflow/providers/google/cloud/hooks/bigquery.py b/providers/google/src/airflow/providers/google/cloud/hooks/bigquery.py index c882fa26dd782..1ee60b524ee19 100644 --- a/providers/google/src/airflow/providers/google/cloud/hooks/bigquery.py +++ b/providers/google/src/airflow/providers/google/cloud/hooks/bigquery.py @@ -68,7 +68,6 @@ from airflow.providers.google.cloud.utils.credentials_provider import _get_scopes from airflow.providers.google.cloud.utils.lineage import send_hook_lineage_for_bq_job from airflow.providers.google.common.consts import CLIENT_INFO -from airflow.providers.google.common.deprecated import deprecated from airflow.providers.google.common.hooks.base_google import ( _UNSET, PROVIDE_PROJECT_ID, @@ -392,14 +391,6 @@ def get_df( ) return result - @deprecated( - planned_removal_date="November 30, 2025", - use_instead="airflow.providers.google.cloud.hooks.bigquery.BigQueryHook.get_df", - category=AirflowProviderDeprecationWarning, - ) - def get_pandas_df(self, sql, parameters=None, dialect=None, **kwargs): - return self._get_pandas_df(sql, parameters, dialect, **kwargs) - @GoogleBaseHook.fallback_to_default_project_id def table_exists(self, dataset_id: str, table_id: str, project_id: str) -> bool: """ diff --git a/providers/google/src/airflow/providers/google/cloud/hooks/datacatalog.py b/providers/google/src/airflow/providers/google/cloud/hooks/datacatalog.py deleted file mode 100644 index 77e5787a9117d..0000000000000 --- a/providers/google/src/airflow/providers/google/cloud/hooks/datacatalog.py +++ /dev/null @@ -1,1172 +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 collections.abc import Sequence -from typing import TYPE_CHECKING - -from google.api_core.gapic_v1.method import DEFAULT, _MethodDefault -from google.cloud import datacatalog -from google.cloud.datacatalog import ( - CreateTagRequest, - DataCatalogClient, - Entry, - EntryGroup, - SearchCatalogRequest, - Tag, - TagTemplate, - TagTemplateField, -) - -from airflow.exceptions import AirflowProviderDeprecationWarning -from airflow.providers.common.compat.sdk import AirflowException -from airflow.providers.google.common.consts import CLIENT_INFO -from airflow.providers.google.common.deprecated import deprecated -from airflow.providers.google.common.hooks.base_google import PROVIDE_PROJECT_ID, GoogleBaseHook - -if TYPE_CHECKING: - from google.api_core.retry import Retry - from google.protobuf.field_mask_pb2 import FieldMask - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.hooks.dataplex.DataplexHook", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogHook(GoogleBaseHook): - """ - Hook for Google Cloud Data Catalog Service. - - :param gcp_conn_id: The connection ID to use when fetching connection info. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account. - """ - - def __init__( - self, - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__( - gcp_conn_id=gcp_conn_id, - impersonation_chain=impersonation_chain, - **kwargs, - ) - self._client: DataCatalogClient | None = None - - def get_conn(self) -> DataCatalogClient: - """Retrieve client library object that allow access to Cloud Data Catalog service.""" - if not self._client: - self._client = DataCatalogClient(credentials=self.get_credentials(), client_info=CLIENT_INFO) - return self._client - - @GoogleBaseHook.fallback_to_default_project_id - def create_entry( - self, - location: str, - entry_group: str, - entry_id: str, - entry: dict | Entry, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> Entry: - """ - Create an entry. - - Currently only entries of 'FILESET' type can be created. - - :param location: Required. The location of the entry to create. - :param entry_group: Required. Entry group ID under which the entry is created. - :param entry_id: Required. The id of the entry to create. - :param entry: Required. The entry to create. - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.Entry` - :param project_id: The ID of the Google Cloud project that owns the entry. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If set to ``None`` or missing, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - parent = f"projects/{project_id}/locations/{location}/entryGroups/{entry_group}" - self.log.info("Creating a new entry: parent=%s", parent) - result = client.create_entry( - request={"parent": parent, "entry_id": entry_id, "entry": entry}, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - self.log.info("Created a entry: name=%s", result.name) - return result - - @GoogleBaseHook.fallback_to_default_project_id - def create_entry_group( - self, - location: str, - entry_group_id: str, - entry_group: dict | EntryGroup, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> EntryGroup: - """ - Create an EntryGroup. - - :param location: Required. The location of the entry group to create. - :param entry_group_id: Required. The id of the entry group to create. The id must begin with a letter - or underscore, contain only English letters, numbers and underscores, and be at most 64 - characters. - :param entry_group: The entry group to create. Defaults to an empty entry group. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.EntryGroup` - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - parent = f"projects/{project_id}/locations/{location}" - self.log.info("Creating a new entry group: parent=%s", parent) - - result = client.create_entry_group( - request={"parent": parent, "entry_group_id": entry_group_id, "entry_group": entry_group}, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - self.log.info("Created a entry group: name=%s", result.name) - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def create_tag( - self, - location: str, - entry_group: str, - entry: str, - tag: dict | Tag, - project_id: str = PROVIDE_PROJECT_ID, - template_id: str | None = None, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> Tag: - """ - Create a tag on an entry. - - :param location: Required. The location of the tag to create. - :param entry_group: Required. Entry group ID under which the tag is created. - :param entry: Required. Entry group ID under which the tag is created. - :param tag: Required. The tag to create. - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.Tag` - :param template_id: Required. Template ID used to create tag - :param project_id: The ID of the Google Cloud project that owns the tag. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - if template_id: - template_path = f"projects/{project_id}/locations/{location}/tagTemplates/{template_id}" - if isinstance(tag, Tag): - tag.template = template_path - else: - tag["template"] = template_path - parent = f"projects/{project_id}/locations/{location}/entryGroups/{entry_group}/entries/{entry}" - - self.log.info("Creating a new tag: parent=%s", parent) - # HACK: google-cloud-datacatalog has problems with mapping messages where the value is not a - # primitive type, so we need to convert it manually. - # See: https://github.com/googleapis/python-datacatalog/issues/84 - if isinstance(tag, dict): - tag = Tag( - name=tag.get("name"), - template=tag.get("template"), - template_display_name=tag.get("template_display_name"), - column=tag.get("column"), - fields={ - k: datacatalog.TagField(**v) if isinstance(v, dict) else v - for k, v in tag.get("fields", {}).items() - }, - ) - request = CreateTagRequest( - parent=parent, - tag=tag, - ) - - result = client.create_tag(request=request, retry=retry, timeout=timeout, metadata=metadata or ()) - self.log.info("Created a tag: name=%s", result.name) - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def create_tag_template( - self, - location, - tag_template_id: str, - tag_template: dict | TagTemplate, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> TagTemplate: - """ - Create a tag template. - - :param location: Required. The location of the tag template to create. - :param tag_template_id: Required. The id of the tag template to create. - :param tag_template: Required. The tag template to create. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.TagTemplate` - :param project_id: The ID of the Google Cloud project that owns the tag template. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - parent = f"projects/{project_id}/locations/{location}" - - self.log.info("Creating a new tag template: parent=%s", parent) - # HACK: google-cloud-datacatalog has problems with mapping messages where the value is not a - # primitive type, so we need to convert it manually. - # See: https://github.com/googleapis/python-datacatalog/issues/84 - if isinstance(tag_template, dict): - tag_template = datacatalog.TagTemplate( - name=tag_template.get("name"), - display_name=tag_template.get("display_name"), - fields={ - k: datacatalog.TagTemplateField(**v) if isinstance(v, dict) else v - for k, v in tag_template.get("fields", {}).items() - }, - ) - - request = datacatalog.CreateTagTemplateRequest( - parent=parent, tag_template_id=tag_template_id, tag_template=tag_template - ) - result = client.create_tag_template( - request=request, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - self.log.info("Created a tag template: name=%s", result.name) - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def create_tag_template_field( - self, - location: str, - tag_template: str, - tag_template_field_id: str, - tag_template_field: dict | TagTemplateField, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> TagTemplateField: - r""" - Create a field in a tag template. - - :param location: Required. The location of the tag template field to create. - :param tag_template: Required. The id of the tag template to create. - :param tag_template_field_id: Required. The ID of the tag template field to create. Field ids can - contain letters (both uppercase and lowercase), numbers (0-9), underscores (\_) and dashes (-). - Field IDs must be at least 1 character long and at most 128 characters long. Field IDs must also - be unique within their template. - :param tag_template_field: Required. The tag template field to create. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.TagTemplateField` - :param project_id: The ID of the Google Cloud project that owns the tag template field. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - parent = f"projects/{project_id}/locations/{location}/tagTemplates/{tag_template}" - - self.log.info("Creating a new tag template field: parent=%s", parent) - - result = client.create_tag_template_field( - request={ - "parent": parent, - "tag_template_field_id": tag_template_field_id, - "tag_template_field": tag_template_field, - }, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - - self.log.info("Created a tag template field: name=%s", result.name) - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def delete_entry( - self, - location: str, - entry_group: str, - entry: str, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> None: - """ - Delete an existing entry. - - :param location: Required. The location of the entry to delete. - :param entry_group: Required. Entry group ID for entries that is deleted. - :param entry: Entry ID that is deleted. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - name = f"projects/{project_id}/locations/{location}/entryGroups/{entry_group}/entries/{entry}" - self.log.info("Deleting a entry: name=%s", name) - client.delete_entry(request={"name": name}, retry=retry, timeout=timeout, metadata=metadata or ()) - self.log.info("Deleted a entry: name=%s", name) - - @GoogleBaseHook.fallback_to_default_project_id - def delete_entry_group( - self, - location, - entry_group, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> None: - """ - Delete an EntryGroup. - - Only entry groups that do not contain entries can be deleted. - - :param location: Required. The location of the entry group to delete. - :param entry_group: Entry group ID that is deleted. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - name = f"projects/{project_id}/locations/{location}/entryGroups/{entry_group}" - - self.log.info("Deleting a entry group: name=%s", name) - client.delete_entry_group( - request={"name": name}, retry=retry, timeout=timeout, metadata=metadata or () - ) - self.log.info("Deleted a entry group: name=%s", name) - - @GoogleBaseHook.fallback_to_default_project_id - def delete_tag( - self, - location: str, - entry_group: str, - entry: str, - tag: str, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> None: - """ - Delete a tag. - - :param location: Required. The location of the tag to delete. - :param entry_group: Entry group ID for tag that is deleted. - :param entry: Entry ID for tag that is deleted. - :param tag: Identifier for TAG that is deleted. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - name = ( - f"projects/{project_id}/locations/{location}/entryGroups/{entry_group}/entries/{entry}/tags/{tag}" - ) - - self.log.info("Deleting a tag: name=%s", name) - client.delete_tag(request={"name": name}, retry=retry, timeout=timeout, metadata=metadata or ()) - self.log.info("Deleted a tag: name=%s", name) - - @GoogleBaseHook.fallback_to_default_project_id - def delete_tag_template( - self, - location, - tag_template, - force: bool, - project_id: str, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> None: - """ - Delete a tag template and all tags using the template. - - :param location: Required. The location of the tag template to delete. - :param tag_template: ID for tag template that is deleted. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param force: Required. Currently, this field must always be set to ``true``. This confirms the - deletion of any possible tags using this template. ``force = false`` will be supported in the - future. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - name = f"projects/{project_id}/locations/{location}/tagTemplates/{tag_template}" - - self.log.info("Deleting a tag template: name=%s", name) - client.delete_tag_template( - request={"name": name, "force": force}, retry=retry, timeout=timeout, metadata=metadata or () - ) - self.log.info("Deleted a tag template: name=%s", name) - - @GoogleBaseHook.fallback_to_default_project_id - def delete_tag_template_field( - self, - location: str, - tag_template: str, - field: str, - force: bool, - project_id: str, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> None: - """ - Delete a field in a tag template and all uses of that field. - - :param location: Required. The location of the tag template to delete. - :param tag_template: Tag Template ID for tag template field that is deleted. - :param field: Name of field that is deleted. - :param force: Required. This confirms the deletion of this field from any tags using this field. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - name = f"projects/{project_id}/locations/{location}/tagTemplates/{tag_template}/fields/{field}" - - self.log.info("Deleting a tag template field: name=%s", name) - client.delete_tag_template_field( - request={"name": name, "force": force}, retry=retry, timeout=timeout, metadata=metadata or () - ) - self.log.info("Deleted a tag template field: name=%s", name) - - @GoogleBaseHook.fallback_to_default_project_id - def get_entry( - self, - location: str, - entry_group: str, - entry: str, - project_id: str, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> Entry: - """ - Get an entry. - - :param location: Required. The location of the entry to get. - :param entry_group: Required. The entry group of the entry to get. - :param entry: The ID of the entry to get. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - name = f"projects/{project_id}/locations/{location}/entryGroups/{entry_group}/entries/{entry}" - - self.log.info("Getting a entry: name=%s", name) - result = client.get_entry( - request={"name": name}, retry=retry, timeout=timeout, metadata=metadata or () - ) - self.log.info("Received a entry: name=%s", result.name) - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def get_entry_group( - self, - location: str, - entry_group: str, - project_id: str, - read_mask: FieldMask | None = None, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> EntryGroup: - """ - Get an entry group. - - :param location: Required. The location of the entry group to get. - :param entry_group: The ID of the entry group to get. - :param read_mask: The fields to return. If not set or empty, all fields are returned. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.protobuf.field_mask_pb2.FieldMask` - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - name = f"projects/{project_id}/locations/{location}/entryGroups/{entry_group}" - - self.log.info("Getting a entry group: name=%s", name) - - result = client.get_entry_group( - request={"name": name, "read_mask": read_mask}, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - - self.log.info("Received a entry group: name=%s", result.name) - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def get_tag_template( - self, - location: str, - tag_template: str, - project_id: str, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> TagTemplate: - """ - Get a tag template. - - :param location: Required. The location of the tag template to get. - :param tag_template: Required. The ID of the tag template to get. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - name = f"projects/{project_id}/locations/{location}/tagTemplates/{tag_template}" - - self.log.info("Getting a tag template: name=%s", name) - - result = client.get_tag_template( - request={"name": name}, retry=retry, timeout=timeout, metadata=metadata or () - ) - - self.log.info("Received a tag template: name=%s", result.name) - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def list_tags( - self, - location: str, - entry_group: str, - entry: str, - project_id: str, - page_size: int = 100, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ): - """ - List the tags on an Entry. - - :param location: Required. The location of the tags to get. - :param entry_group: Required. The entry group of the tags to get. - :param entry_group: Required. The entry of the tags to get. - :param page_size: The maximum number of resources contained in the underlying API response. If page - streaming is performed per- resource, this parameter does not affect the return value. If page - streaming is performed per-page, this determines the maximum number of resources in a page. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - parent = f"projects/{project_id}/locations/{location}/entryGroups/{entry_group}/entries/{entry}" - - self.log.info("Listing tag on entry: entry_name=%s", parent) - - result = client.list_tags( - request={"parent": parent, "page_size": page_size}, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - - self.log.info("Received tags.") - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def get_tag_for_template_name( - self, - location: str, - entry_group: str, - entry: str, - template_name: str, - project_id: str, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> Tag: - """ - Get for a tag with a specific template for a specific entry. - - :param location: Required. The location which contains the entry to search for. - :param entry_group: The entry group ID which contains the entry to search for. - :param entry: The name of the entry to search for. - :param template_name: The name of the template that will be the search criterion. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - tags_list = self.list_tags( - location=location, - entry_group=entry_group, - entry=entry, - project_id=project_id, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - tag = next(t for t in tags_list if t.template == template_name) - return tag - - def lookup_entry( - self, - linked_resource: str | None = None, - sql_resource: str | None = None, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> Entry: - r""" - Get an entry by target resource name. - - This method allows clients to use the resource name from the source Google Cloud service - to get the Data Catalog Entry. - - :param linked_resource: The full name of the Google Cloud resource the Data Catalog entry - represents. See: https://cloud.google.com/apis/design/resource\_names#full\_resource\_name. Full - names are case-sensitive. - - :param sql_resource: The SQL name of the entry. SQL names are case-sensitive. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - if linked_resource and sql_resource: - raise AirflowException("Only one of linked_resource, sql_resource should be set.") - - if not linked_resource and not sql_resource: - raise AirflowException("At least one of linked_resource, sql_resource should be set.") - - if linked_resource: - self.log.info("Getting entry: linked_resource=%s", linked_resource) - result = client.lookup_entry( - request={"linked_resource": linked_resource}, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - else: - self.log.info("Getting entry: sql_resource=%s", sql_resource) - result = client.lookup_entry( - request={"sql_resource": sql_resource}, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - self.log.info("Received entry. name=%s", result.name) - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def rename_tag_template_field( - self, - location: str, - tag_template: str, - field: str, - new_tag_template_field_id: str, - project_id: str, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> TagTemplateField: - """ - Rename a field in a tag template. - - :param location: Required. The location of the tag template field to rename. - :param tag_template: The tag template ID for field that is renamed. - :param field: Required. The old ID of this tag template field. For example, - ``my_old_field``. - :param new_tag_template_field_id: Required. The new ID of this tag template field. For example, - ``my_new_field``. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - name = f"projects/{project_id}/locations/{location}/tagTemplates/{tag_template}/fields/{field}" - - self.log.info( - "Renaming field: old_name=%s, new_tag_template_field_id=%s", name, new_tag_template_field_id - ) - - result = client.rename_tag_template_field( - request={"name": name, "new_tag_template_field_id": new_tag_template_field_id}, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - - self.log.info("Renamed tag template field.") - - return result - - def search_catalog( - self, - scope: dict | SearchCatalogRequest.Scope, - query: str, - page_size: int = 100, - order_by: str | None = None, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ): - r""" - Search Data Catalog for multiple resources like entries, tags that match a query. - - This does not return the complete resource, only the resource identifier and high level fields. - Clients can subsequently call ``Get`` methods. - - Note that searches do not have full recall. There may be results that match your query but are not - returned, even in subsequent pages of results. These missing results may vary across repeated calls to - search. Do not rely on this method if you need to guarantee full recall. - - :param scope: Required. The scope of this search request. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.Scope` - :param query: Required. The query string in search query syntax. The query must be non-empty. - - Query strings can be simple as "x" or more qualified as: - - - name:x - - column:x - - description:y - - Note: Query tokens need to have a minimum of 3 characters for substring matching to work - correctly. See `Data Catalog Search Syntax `__ for more information. - :param page_size: The maximum number of resources contained in the underlying API response. If page - streaming is performed per-resource, this parameter does not affect the return value. If page - streaming is performed per-page, this determines the maximum number of resources in a page. - :param order_by: Specifies the ordering of results, currently supported case-sensitive choices are: - - - ``relevance``, only supports descending - - ``last_access_timestamp [asc|desc]``, defaults to descending if not specified - - ``last_modified_timestamp [asc|desc]``, defaults to descending if not specified - - If not specified, defaults to ``relevance`` descending. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - - self.log.info( - "Searching catalog: scope=%s, query=%s, page_size=%s, order_by=%s", - scope, - query, - page_size, - order_by, - ) - result = client.search_catalog( - request={"scope": scope, "query": query, "page_size": page_size, "order_by": order_by}, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - - self.log.info("Received items.") - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def update_entry( - self, - entry: dict | Entry, - update_mask: dict | FieldMask, - project_id: str, - location: str | None = None, - entry_group: str | None = None, - entry_id: str | None = None, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> Entry: - """ - Update an existing entry. - - :param entry: Required. The updated entry. The "name" field must be set. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.Entry` - :param update_mask: The fields to update on the entry. If absent or empty, all modifiable fields are - updated. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.protobuf.field_mask_pb2.FieldMask` - :param location: Required. The location of the entry to update. - :param entry_group: The entry group ID for the entry that is being updated. - :param entry_id: The entry ID that is being updated. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - if project_id and location and entry_group and entry_id: - full_entry_name = ( - f"projects/{project_id}/locations/{location}/entryGroups/{entry_group}/entries/{entry_id}" - ) - if isinstance(entry, Entry): - entry.name = full_entry_name - elif isinstance(entry, dict): - entry["name"] = full_entry_name - else: - raise AirflowException("Unable to set entry's name.") - elif location and entry_group and entry_id: - raise AirflowException( - "You must provide all the parameters (project_id, location, entry_group, entry_id) " - "contained in the name, or do not specify any parameters and pass the name on the object " - ) - name = entry.name if isinstance(entry, Entry) else entry["name"] - self.log.info("Updating entry: name=%s", name) - - # HACK: google-cloud-datacatalog has a problem with dictionaries for update methods. - if isinstance(entry, dict): - entry = Entry(**entry) - result = client.update_entry( - request={"entry": entry, "update_mask": update_mask}, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - - self.log.info("Updated entry.") - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def update_tag( - self, - tag: dict | Tag, - update_mask: dict | FieldMask, - project_id: str, - location: str | None = None, - entry_group: str | None = None, - entry: str | None = None, - tag_id: str | None = None, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> Tag: - """ - Update an existing tag. - - :param tag: Required. The updated tag. The "name" field must be set. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.Tag` - :param update_mask: The fields to update on the Tag. If absent or empty, all modifiable fields are - updated. Currently the only modifiable field is the field ``fields``. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.FieldMask` - :param location: Required. The location of the tag to rename. - :param entry_group: The entry group ID for the tag that is being updated. - :param entry: The entry ID for the tag that is being updated. - :param tag_id: The tag ID that is being updated. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - if project_id and location and entry_group and entry and tag_id: - full_tag_name = ( - f"projects/{project_id}/locations/{location}/entryGroups/{entry_group}/entries/{entry}" - f"/tags/{tag_id}" - ) - if isinstance(tag, Tag): - tag.name = full_tag_name - elif isinstance(tag, dict): - tag["name"] = full_tag_name - else: - raise AirflowException("Unable to set tag's name.") - elif location and entry_group and entry and tag_id: - raise AirflowException( - "You must provide all the parameters (project_id, location, entry_group, entry, tag_id) " - "contained in the name, or do not specify any parameters and pass the name on the object " - ) - - name = tag.name if isinstance(tag, Tag) else tag["name"] - self.log.info("Updating tag: name=%s", name) - - # HACK: google-cloud-datacatalog has a problem with dictionaries for update methods. - if isinstance(tag, dict): - tag = Tag(**tag) - result = client.update_tag( - request={"tag": tag, "update_mask": update_mask}, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - self.log.info("Updated tag.") - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def update_tag_template( - self, - tag_template: dict | TagTemplate, - update_mask: dict | FieldMask, - project_id: str, - location: str | None = None, - tag_template_id: str | None = None, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ) -> TagTemplate: - """ - Update a tag template. - - This method cannot be used to update the fields of a template. The tag - template fields are represented as separate resources and should be updated using their own - create/update/delete methods. - - :param tag_template: Required. The template to update. The "name" field must be set. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.TagTemplate` - :param update_mask: The field mask specifies the parts of the template to overwrite. - - If absent or empty, all of the allowed fields above will be updated. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.protobuf.field_mask_pb2.FieldMask` - :param location: Required. The location of the tag template to rename. - :param tag_template_id: Optional. The tag template ID for the entry that is being updated. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - if project_id and location and tag_template: - full_tag_template_name = ( - f"projects/{project_id}/locations/{location}/tagTemplates/{tag_template_id}" - ) - if isinstance(tag_template, TagTemplate): - tag_template.name = full_tag_template_name - elif isinstance(tag_template, dict): - tag_template["name"] = full_tag_template_name - else: - raise AirflowException("Unable to set name of tag template.") - elif location and tag_template: - raise AirflowException( - "You must provide all the parameters (project_id, location, tag_template_id) " - "contained in the name, or do not specify any parameters and pass the name on the object " - ) - - name = tag_template.name if isinstance(tag_template, TagTemplate) else tag_template["name"] - self.log.info("Updating tag template: name=%s", name) - - # HACK: google-cloud-datacatalog has a problem with dictionaries for update methods. - if isinstance(tag_template, dict): - tag_template = TagTemplate(**tag_template) - result = client.update_tag_template( - request={"tag_template": tag_template, "update_mask": update_mask}, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - self.log.info("Updated tag template.") - - return result - - @GoogleBaseHook.fallback_to_default_project_id - def update_tag_template_field( - self, - tag_template_field: dict | TagTemplateField, - update_mask: dict | FieldMask, - project_id: str, - tag_template_field_name: str | None = None, - location: str | None = None, - tag_template: str | None = None, - tag_template_field_id: str | None = None, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - ): - """ - Update a field in a tag template. This method cannot be used to update the field type. - - :param tag_template_field: Required. The template to update. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.TagTemplateField` - :param update_mask: The field mask specifies the parts of the template to be updated. Allowed fields: - - - ``display_name`` - - ``type.enum_type`` - - If ``update_mask`` is not set or empty, all of the allowed fields above will be updated. - - When updating an enum type, the provided values will be merged with the existing values. - Therefore, enum values can only be added, existing enum values cannot be deleted nor renamed. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.protobuf.field_mask_pb2.FieldMask` - :param tag_template_field_name: Optional. The name of the tag template field to rename. - :param location: Optional. The location of the tag to rename. - :param tag_template: Optional. The tag template ID for tag template field to rename. - :param tag_template_field_id: Optional. The ID of tag template field to rename. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - """ - client = self.get_conn() - if project_id and location and tag_template and tag_template_field_id: - tag_template_field_name = ( - f"projects/{project_id}/locations/{location}/tagTemplates/{tag_template}" - f"/fields/{tag_template_field_id}" - ) - - self.log.info("Updating tag template field: name=%s", tag_template_field_name) - - result = client.update_tag_template_field( - request={ - "name": tag_template_field_name, - "tag_template_field": tag_template_field, - "update_mask": update_mask, - }, - retry=retry, - timeout=timeout, - metadata=metadata, - ) - self.log.info("Updated tag template field.") - - return result diff --git a/providers/google/src/airflow/providers/google/cloud/hooks/vertex_ai/generative_model.py b/providers/google/src/airflow/providers/google/cloud/hooks/vertex_ai/generative_model.py index 2e846ccd2524e..06854d313248a 100644 --- a/providers/google/src/airflow/providers/google/cloud/hooks/vertex_ai/generative_model.py +++ b/providers/google/src/airflow/providers/google/cloud/hooks/vertex_ai/generative_model.py @@ -19,26 +19,17 @@ from __future__ import annotations -import time -from datetime import timedelta -from typing import TYPE_CHECKING, Any, Literal +from typing import Any import vertexai -from google.cloud import aiplatform from vertexai.generative_models import GenerativeModel from vertexai.language_models import TextEmbeddingModel from vertexai.preview import generative_models as preview_generative_model from vertexai.preview.caching import CachedContent from vertexai.preview.evaluation import EvalResult, EvalTask -from vertexai.preview.tuning import sft -from airflow.exceptions import AirflowProviderDeprecationWarning -from airflow.providers.google.common.deprecated import deprecated from airflow.providers.google.common.hooks.base_google import PROVIDE_PROJECT_ID, GoogleBaseHook -if TYPE_CHECKING: - from google.cloud.aiplatform_v1beta1 import types as types_v1beta1 - class GenerativeModelHook(GoogleBaseHook): """Hook for Google Cloud Vertex AI Generative Model APIs.""" @@ -90,174 +81,6 @@ def get_cached_context_model( cached_context_model = preview_generative_model.GenerativeModel.from_cached_content(cached_content) return cached_context_model - @deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.hooks.gen_ai.generative_model.GenAIGenerativeModelHook.embed_content", - category=AirflowProviderDeprecationWarning, - ) - @GoogleBaseHook.fallback_to_default_project_id - def text_embedding_model_get_embeddings( - self, - prompt: str, - pretrained_model: str, - location: str, - project_id: str = PROVIDE_PROJECT_ID, - ) -> list: - """ - Use the Vertex AI PaLM API to generate text embeddings. - - :param project_id: Required. The ID of the Google Cloud project that the service belongs to. - :param location: Required. The ID of the Google Cloud location that the service belongs to. - :param prompt: Required. Inputs or queries that a user or a program gives - to the Vertex AI PaLM API, in order to elicit a specific response. - :param pretrained_model: A pre-trained model optimized for generating text embeddings. - """ - vertexai.init(project=project_id, location=location, credentials=self.get_credentials()) - model = self.get_text_embedding_model(pretrained_model) - - response = model.get_embeddings([prompt])[0] # single prompt - - return response.values - - @deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.hooks.gen_ai.generative_model.GenAIGenerativeModelHook.generate_content", - category=AirflowProviderDeprecationWarning, - ) - @GoogleBaseHook.fallback_to_default_project_id - def generative_model_generate_content( - self, - contents: list, - location: str, - pretrained_model: str, - tools: list | None = None, - generation_config: dict | None = None, - safety_settings: dict | None = None, - system_instruction: str | None = None, - project_id: str = PROVIDE_PROJECT_ID, - ) -> str: - """ - Use the Vertex AI Gemini Pro foundation model to generate natural language text. - - :param location: Required. The ID of the Google Cloud location that the service belongs to. - :param project_id: Required. The ID of the Google Cloud project that the service belongs to. - :param contents: Required. The multi-part content of a message that a user or a program - gives to the generative model, in order to elicit a specific response. - :param generation_config: Optional. Generation configuration settings. - :param safety_settings: Optional. Per request settings for blocking unsafe content. - :param tools: Optional. A list of tools available to the model during evaluation, such as a data store. - :param system_instruction: Optional. An instruction given to the model to guide its behavior. - :param pretrained_model: Required. Model, - supporting prompts with text-only input, including natural language - tasks, multi-turn text and code chat, and code generation. It can - output text and code. - """ - vertexai.init(project=project_id, location=location, credentials=self.get_credentials()) - - model = self.get_generative_model( - pretrained_model=pretrained_model, system_instruction=system_instruction - ) - response = model.generate_content( - contents=contents, - tools=tools, - generation_config=generation_config, - safety_settings=safety_settings, - ) - - return response.text - - @deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.hooks.gen_ai.generative_model.GenAIGenerativeModelHook.supervised_fine_tuning_train", - category=AirflowProviderDeprecationWarning, - ) - @GoogleBaseHook.fallback_to_default_project_id - def supervised_fine_tuning_train( - self, - source_model: str, - train_dataset: str, - location: str, - tuned_model_display_name: str | None = None, - validation_dataset: str | None = None, - epochs: int | None = None, - adapter_size: Literal[1, 4, 8, 16] | None = None, - learning_rate_multiplier: float | None = None, - project_id: str = PROVIDE_PROJECT_ID, - ) -> Any: - """ - Use the Supervised Fine Tuning API to create a tuning job. - - :param project_id: Required. The ID of the Google Cloud project that the service belongs to. - :param location: Required. The ID of the Google Cloud location that the service belongs to. - :param source_model: Required. A pre-trained model optimized for performing natural - language tasks such as classification, summarization, extraction, content - creation, and ideation. - :param train_dataset: Required. Cloud Storage URI of your training dataset. The dataset - must be formatted as a JSONL file. For best results, provide at least 100 to 500 examples. - :param tuned_model_display_name: Optional. Display name of the TunedModel. The name can be up - to 128 characters long and can consist of any UTF-8 characters. - :param validation_dataset: Optional. Cloud Storage URI of your training dataset. The dataset must be - formatted as a JSONL file. For best results, provide at least 100 to 500 examples. - :param epochs: Optional. To optimize performance on a specific dataset, try using a higher - epoch value. Increasing the number of epochs might improve results. However, be cautious - about over-fitting, especially when dealing with small datasets. If over-fitting occurs, - consider lowering the epoch number. - :param adapter_size: Optional. Adapter size for tuning. - :param learning_rate_multiplier: Optional. Multiplier for adjusting the default learning rate. - """ - vertexai.init(project=project_id, location=location, credentials=self.get_credentials()) - - sft_tuning_job = sft.train( - source_model=source_model, - train_dataset=train_dataset, - validation_dataset=validation_dataset, - epochs=epochs, - adapter_size=adapter_size, - learning_rate_multiplier=learning_rate_multiplier, - tuned_model_display_name=tuned_model_display_name, - ) - - # Polling for job completion - while not sft_tuning_job.has_ended: - time.sleep(60) - sft_tuning_job.refresh() - - return sft_tuning_job - - @deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.hooks.gen_ai.generative_model.GenAIGenerativeModelHook.count_tokens", - category=AirflowProviderDeprecationWarning, - ) - @GoogleBaseHook.fallback_to_default_project_id - def count_tokens( - self, - contents: list, - location: str, - pretrained_model: str, - project_id: str = PROVIDE_PROJECT_ID, - ) -> types_v1beta1.CountTokensResponse: - """ - Use the Vertex AI Count Tokens API to calculate the number of input tokens before sending a request to the Gemini API. - - :param project_id: Required. The ID of the Google Cloud project that the service belongs to. - :param location: Required. The ID of the Google Cloud location that the service belongs to. - :param contents: Required. The multi-part content of a message that a user or a program - gives to the generative model, in order to elicit a specific response. - :param pretrained_model: Required. Model, - supporting prompts with text-only input, including natural language - tasks, multi-turn text and code chat, and code generation. It can - output text and code. - """ - vertexai.init(project=project_id, location=location, credentials=self.get_credentials()) - - model = self.get_generative_model(pretrained_model=pretrained_model) - response = model.count_tokens( - contents=contents, - ) - - return response - @GoogleBaseHook.fallback_to_default_project_id def run_evaluation( self, @@ -315,116 +138,3 @@ def run_evaluation( ) return eval_result - - @deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.hooks.gen_ai.generative_model.GenAIGenerativeModelHook.create_cached_content", - category=AirflowProviderDeprecationWarning, - ) - def create_cached_content( - self, - model_name: str, - location: str, - ttl_hours: float = 1, - system_instruction: Any | None = None, - contents: list[Any] | None = None, - display_name: str | None = None, - project_id: str = PROVIDE_PROJECT_ID, - ) -> str: - """ - Create CachedContent to reduce the cost of requests that contain repeat content with high input token counts. - - :param project_id: Required. The ID of the Google Cloud project that the service belongs to. - :param location: Required. The ID of the Google Cloud location that the service belongs to. - :param model_name: Required. The name of the publisher model to use for cached content. - :param system_instruction: Developer set system instruction. - :param contents: The content to cache. - :param ttl_hours: The TTL for this resource in hours. The expiration time is computed: now + TTL. - Defaults to one hour. - :param display_name: The user-generated meaningful display name of the cached content - """ - vertexai.init(project=project_id, location=location, credentials=self.get_credentials()) - - response = CachedContent.create( - model_name=model_name, - system_instruction=system_instruction, - contents=contents, - ttl=timedelta(hours=ttl_hours), - display_name=display_name, - ) - - return response.name - - @deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.hooks.gen_ai.generative_model.GenAIGenerativeModelHook.generate_content", - category=AirflowProviderDeprecationWarning, - ) - def generate_from_cached_content( - self, - location: str, - cached_content_name: str, - contents: list, - generation_config: dict | None = None, - safety_settings: dict | None = None, - project_id: str = PROVIDE_PROJECT_ID, - ) -> str: - """ - Generate a response from CachedContent. - - :param project_id: Required. The ID of the Google Cloud project that the service belongs to. - :param location: Required. The ID of the Google Cloud location that the service belongs to. - :param cached_content_name: Required. The name of the cached content resource. - :param contents: Required. The multi-part content of a message that a user or a program - gives to the generative model, in order to elicit a specific response. - :param generation_config: Optional. Generation configuration settings. - :param safety_settings: Optional. Per request settings for blocking unsafe content. - """ - # During run of the system test it was found out that names from xcom, e.g. 3402922389 can be - # treated as int and throw an error TypeError: expected string or bytes-like object, got 'int' - cached_content_name = str(cached_content_name) - vertexai.init(project=project_id, location=location, credentials=self.get_credentials()) - - cached_context_model = self.get_cached_context_model(cached_content_name=cached_content_name) - - response = cached_context_model.generate_content( - contents=contents, - generation_config=generation_config, - safety_settings=safety_settings, - ) - - return response.text - - -@deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.hooks.vertex_ai.experiment_service.ExperimentRunHook", - category=AirflowProviderDeprecationWarning, -) -class ExperimentRunHook(GoogleBaseHook): - """Use the Vertex AI SDK for Python to create and manage your experiment runs.""" - - @GoogleBaseHook.fallback_to_default_project_id - def delete_experiment_run( - self, - experiment_run_name: str, - experiment_name: str, - location: str, - project_id: str = PROVIDE_PROJECT_ID, - delete_backing_tensorboard_run: bool = False, - ) -> None: - """ - Delete experiment run from the experiment. - - :param project_id: Required. The ID of the Google Cloud project that the service belongs to. - :param location: Required. The ID of the Google Cloud location that the service belongs to. - :param experiment_name: Required. The name of the evaluation experiment. - :param experiment_run_name: Required. The specific run name or ID for this experiment. - :param delete_backing_tensorboard_run: Whether to delete the backing Vertex AI TensorBoard run - that stores time series metrics for this run. - """ - self.log.info("Next experiment run will be deleted: %s", experiment_run_name) - experiment_run = aiplatform.ExperimentRun( - run_name=experiment_run_name, experiment=experiment_name, project=project_id, location=location - ) - experiment_run.delete(delete_backing_tensorboard_run=delete_backing_tensorboard_run) diff --git a/providers/google/src/airflow/providers/google/cloud/links/datacatalog.py b/providers/google/src/airflow/providers/google/cloud/links/datacatalog.py deleted file mode 100644 index 00b9071d77dcc..0000000000000 --- a/providers/google/src/airflow/providers/google/cloud/links/datacatalog.py +++ /dev/null @@ -1,84 +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. -"""This module contains Google Data Catalog links.""" - -from __future__ import annotations - -from airflow.exceptions import AirflowProviderDeprecationWarning -from airflow.providers.google.cloud.links.base import BaseGoogleLink -from airflow.providers.google.common.deprecated import deprecated - -DATACATALOG_BASE_LINK = "/datacatalog" -ENTRY_GROUP_LINK = ( - DATACATALOG_BASE_LINK - + "/groups/{entry_group_id};container={project_id};location={location_id}?project={project_id}" -) -ENTRY_LINK = ( - DATACATALOG_BASE_LINK - + "/projects/{project_id}/locations/{location_id}/entryGroups/{entry_group_id}/entries/{entry_id}\ - ?project={project_id}" -) -TAG_TEMPLATE_LINK = ( - DATACATALOG_BASE_LINK - + "/projects/{project_id}/locations/{location_id}/tagTemplates/{tag_template_id}?project={project_id}" -) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.links.dataplex.DataplexCatalogEntryGroupLink", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class DataCatalogEntryGroupLink(BaseGoogleLink): - """Helper class for constructing Data Catalog Entry Group Link.""" - - name = "Data Catalog Entry Group" - key = "data_catalog_entry_group" - format_str = ENTRY_GROUP_LINK - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.links.dataplex.DataplexCatalogEntryLink", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class DataCatalogEntryLink(BaseGoogleLink): - """Helper class for constructing Data Catalog Entry Link.""" - - name = "Data Catalog Entry" - key = "data_catalog_entry" - format_str = ENTRY_LINK - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.links.dataplex.DataplexCatalogAspectTypeLink", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class DataCatalogTagTemplateLink(BaseGoogleLink): - """Helper class for constructing Data Catalog Tag Template Link.""" - - name = "Data Catalog Tag Template" - key = "data_catalog_tag_template" - format_str = TAG_TEMPLATE_LINK diff --git a/providers/google/src/airflow/providers/google/cloud/operators/datacatalog.py b/providers/google/src/airflow/providers/google/cloud/operators/datacatalog.py deleted file mode 100644 index 86dced30cc8d7..0000000000000 --- a/providers/google/src/airflow/providers/google/cloud/operators/datacatalog.py +++ /dev/null @@ -1,2338 +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 collections.abc import Sequence -from typing import TYPE_CHECKING - -from google.api_core.exceptions import AlreadyExists, NotFound -from google.api_core.gapic_v1.method import DEFAULT, _MethodDefault -from google.cloud.datacatalog import ( - DataCatalogClient, - Entry, - EntryGroup, - SearchCatalogRequest, - SearchCatalogResult, - Tag, - TagTemplate, - TagTemplateField, -) - -from airflow.exceptions import AirflowProviderDeprecationWarning -from airflow.providers.google.cloud.hooks.datacatalog import CloudDataCatalogHook -from airflow.providers.google.cloud.links.datacatalog import ( - DataCatalogEntryGroupLink, - DataCatalogEntryLink, - DataCatalogTagTemplateLink, -) -from airflow.providers.google.cloud.operators.cloud_base import GoogleCloudBaseOperator -from airflow.providers.google.common.deprecated import deprecated -from airflow.providers.google.common.hooks.base_google import PROVIDE_PROJECT_ID - -if TYPE_CHECKING: - from google.api_core.retry import Retry - from google.protobuf.field_mask_pb2 import FieldMask - - from airflow.providers.common.compat.sdk import Context - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateEntryOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogCreateEntryOperator(GoogleCloudBaseOperator): - """ - Creates an entry. - - Currently only entries of 'FILESET' type can be created. - - The newly created entry ID are saved under the ``entry_id`` key in XCOM. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogCreateEntryOperator` - - :param location: Required. The location of the entry to create. - :param entry_group: Required. Entry group ID under which the entry is created. - :param entry_id: Required. The id of the entry to create. - :param entry: Required. The entry to create. - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.Entry` - :param project_id: The ID of the Google Cloud project that owns the entry. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If set to ``None`` or missing, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "entry_group", - "entry_id", - "entry", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogEntryLink(),) - - def __init__( - self, - *, - location: str, - entry_group: str, - entry_id: str, - entry: dict | Entry, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.entry_group = entry_group - self.entry_id = entry_id - self.entry = entry - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context): - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - try: - result = hook.create_entry( - location=self.location, - entry_group=self.entry_group, - entry_id=self.entry_id, - entry=self.entry, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - except AlreadyExists: - self.log.info("Entry already exists. Skipping create operation.") - result = hook.get_entry( - location=self.location, - entry_group=self.entry_group, - entry=self.entry_id, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - _, _, entry_id = result.name.rpartition("/") - self.log.info("Current entry_id ID: %s", entry_id) - context["ti"].xcom_push(key="entry_id", value=entry_id) - DataCatalogEntryLink.persist( - context=context, - entry_id=self.entry_id, - entry_group_id=self.entry_group, - location_id=self.location, - project_id=self.project_id or hook.project_id, - ) - return Entry.to_dict(result) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateEntryGroupOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogCreateEntryGroupOperator(GoogleCloudBaseOperator): - """ - Creates an EntryGroup. - - The newly created entry group ID are saved under the ``entry_group_id`` key in XCOM. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogCreateEntryGroupOperator` - - :param location: Required. The location of the entry group to create. - :param entry_group_id: Required. The id of the entry group to create. The id must begin with a letter - or underscore, contain only English letters, numbers and underscores, and be at most 64 - characters. - :param entry_group: The entry group to create. Defaults to an empty entry group. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.EntryGroup` - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "entry_group_id", - "entry_group", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogEntryGroupLink(),) - - def __init__( - self, - *, - location: str, - entry_group_id: str, - entry_group: dict | EntryGroup, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.entry_group_id = entry_group_id - self.entry_group = entry_group - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context): - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - try: - result = hook.create_entry_group( - location=self.location, - entry_group_id=self.entry_group_id, - entry_group=self.entry_group, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - except AlreadyExists: - self.log.info("Entry already exists. Skipping create operation.") - result = hook.get_entry_group( - location=self.location, - entry_group=self.entry_group_id, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - - _, _, entry_group_id = result.name.rpartition("/") - self.log.info("Current entry group ID: %s", entry_group_id) - context["ti"].xcom_push(key="entry_group_id", value=entry_group_id) - DataCatalogEntryGroupLink.persist( - context=context, - entry_group_id=self.entry_group_id, - location_id=self.location, - project_id=self.project_id or hook.project_id, - ) - return EntryGroup.to_dict(result) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateEntryOperator, " - "airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogCreateTagOperator(GoogleCloudBaseOperator): - """ - Creates a tag on an entry. - - The newly created tag ID are saved under the ``tag_id`` key in XCOM. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogCreateTagOperator` - - :param location: Required. The location of the tag to create. - :param entry_group: Required. Entry group ID under which the tag is created. - :param entry: Required. Entry group ID under which the tag is created. - :param tag: Required. The tag to create. - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.Tag` - :param template_id: Required. Template ID used to create tag - :param project_id: The ID of the Google Cloud project that owns the tag. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "entry_group", - "entry", - "tag", - "template_id", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogEntryLink(),) - - def __init__( - self, - *, - location: str, - entry_group: str, - entry: str, - tag: dict | Tag, - template_id: str | None = None, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.entry_group = entry_group - self.entry = entry - self.tag = tag - self.template_id = template_id - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context): - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - try: - tag = hook.create_tag( - location=self.location, - entry_group=self.entry_group, - entry=self.entry, - tag=self.tag, - template_id=self.template_id, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - except AlreadyExists: - self.log.info("Tag already exists. Skipping create operation.") - project_id = self.project_id or hook.project_id - if project_id is None: - raise RuntimeError("The project id must be set here") - if self.template_id: - template_name = DataCatalogClient.tag_template_path( - project_id, self.location, self.template_id - ) - else: - if isinstance(self.tag, Tag): - template_name = self.tag.template - else: - template_name = self.tag["template"] - - tag = hook.get_tag_for_template_name( - location=self.location, - entry_group=self.entry_group, - template_name=template_name, - entry=self.entry, - project_id=project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - - _, _, tag_id = tag.name.rpartition("/") - self.log.info("Current Tag ID: %s", tag_id) - context["ti"].xcom_push(key="tag_id", value=tag_id) - DataCatalogEntryLink.persist( - context=context, - entry_id=self.entry, - entry_group_id=self.entry_group, - location_id=self.location, - project_id=self.project_id or hook.project_id, - ) - return Tag.to_dict(tag) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateAspectTypeOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogCreateTagTemplateOperator(GoogleCloudBaseOperator): - """ - Creates a tag template. - - The newly created tag template are saved under the ``tag_template_id`` key in XCOM. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogCreateTagTemplateOperator` - - :param location: Required. The location of the tag template to create. - :param tag_template_id: Required. The id of the tag template to create. - :param tag_template: Required. The tag template to create. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.TagTemplate` - :param project_id: The ID of the Google Cloud project that owns the tag template. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "tag_template_id", - "tag_template", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogTagTemplateLink(),) - - def __init__( - self, - *, - location: str, - tag_template_id: str, - tag_template: dict | TagTemplate, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.tag_template_id = tag_template_id - self.tag_template = tag_template - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context): - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - try: - result = hook.create_tag_template( - location=self.location, - tag_template_id=self.tag_template_id, - tag_template=self.tag_template, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - except AlreadyExists: - self.log.info("Tag Template already exists. Skipping create operation.") - result = hook.get_tag_template( - location=self.location, - tag_template=self.tag_template_id, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - _, _, tag_template = result.name.rpartition("/") - self.log.info("Current Tag ID: %s", tag_template) - context["ti"].xcom_push(key="tag_template_id", value=tag_template) - DataCatalogTagTemplateLink.persist( - context=context, - tag_template_id=self.tag_template_id, - location_id=self.location, - project_id=self.project_id or hook.project_id, - ) - return TagTemplate.to_dict(result) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator, " - "airflow.providers.google.cloud.operators.dataplex.DataplexCatalogCreateAspectTypeOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogCreateTagTemplateFieldOperator(GoogleCloudBaseOperator): - r""" - Creates a field in a tag template. - - The newly created tag template field are saved under the ``tag_template_field_id`` key in XCOM. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogCreateTagTemplateFieldOperator` - - :param location: Required. The location of the tag template field to create. - :param tag_template: Required. The id of the tag template to create. - :param tag_template_field_id: Required. The ID of the tag template field to create. Field ids can - contain letters (both uppercase and lowercase), numbers (0-9), underscores (\_) and dashes (-). - Field IDs must be at least 1 character long and at most 128 characters long. Field IDs must also - be unique within their template. - :param tag_template_field: Required. The tag template field to create. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.TagTemplateField` - :param project_id: The ID of the Google Cloud project that owns the tag template field. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "tag_template", - "tag_template_field_id", - "tag_template_field", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogTagTemplateLink(),) - - def __init__( - self, - *, - location: str, - tag_template: str, - tag_template_field_id: str, - tag_template_field: dict | TagTemplateField, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.tag_template = tag_template - self.tag_template_field_id = tag_template_field_id - self.tag_template_field = tag_template_field - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context): - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - try: - result = hook.create_tag_template_field( - location=self.location, - tag_template=self.tag_template, - tag_template_field_id=self.tag_template_field_id, - tag_template_field=self.tag_template_field, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - except AlreadyExists: - self.log.info("Tag template field already exists. Skipping create operation.") - tag_template = hook.get_tag_template( - location=self.location, - tag_template=self.tag_template, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - result = tag_template.fields[self.tag_template_field_id] - - self.log.info("Current Tag ID: %s", self.tag_template_field_id) - context["ti"].xcom_push(key="tag_template_field_id", value=self.tag_template_field_id) - DataCatalogTagTemplateLink.persist( - context=context, - tag_template_id=self.tag_template, - location_id=self.location, - project_id=self.project_id or hook.project_id, - ) - return TagTemplateField.to_dict(result) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogDeleteEntryOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogDeleteEntryOperator(GoogleCloudBaseOperator): - """ - Deletes an existing entry. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogDeleteEntryOperator` - - :param location: Required. The location of the entry to delete. - :param entry_group: Required. Entry group ID for entries that is deleted. - :param entry: Entry ID that is deleted. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "entry_group", - "entry", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - - def __init__( - self, - *, - location: str, - entry_group: str, - entry: str, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.entry_group = entry_group - self.entry = entry - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> None: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - try: - hook.delete_entry( - location=self.location, - entry_group=self.entry_group, - entry=self.entry, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - except NotFound: - self.log.info("Entry doesn't exists. Skipping.") - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogDeleteEntryGroupOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogDeleteEntryGroupOperator(GoogleCloudBaseOperator): - """ - Deletes an EntryGroup. - - Only entry groups that do not contain entries can be deleted. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogDeleteEntryGroupOperator` - - :param location: Required. The location of the entry group to delete. - :param entry_group: Entry group ID that is deleted. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "entry_group", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - - def __init__( - self, - *, - location: str, - entry_group: str, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.entry_group = entry_group - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> None: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - try: - hook.delete_entry_group( - location=self.location, - entry_group=self.entry_group, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - except NotFound: - self.log.info("Entry doesn't exists. skipping") - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogDeleteTagOperator(GoogleCloudBaseOperator): - """ - Deletes a tag. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogDeleteTagOperator` - - :param location: Required. The location of the tag to delete. - :param entry_group: Entry group ID for tag that is deleted. - :param entry: Entry ID for tag that is deleted. - :param tag: Identifier for TAG that is deleted. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "entry_group", - "entry", - "tag", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - - def __init__( - self, - *, - location: str, - entry_group: str, - entry: str, - tag: str, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.entry_group = entry_group - self.entry = entry - self.tag = tag - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> None: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - try: - hook.delete_tag( - location=self.location, - entry_group=self.entry_group, - entry=self.entry, - tag=self.tag, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - except NotFound: - self.log.info("Entry doesn't exists. skipping") - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogDeleteAspectTypeOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogDeleteTagTemplateOperator(GoogleCloudBaseOperator): - """ - Deletes a tag template and all tags using the template. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogDeleteTagTemplateOperator` - - :param location: Required. The location of the tag template to delete. - :param tag_template: ID for tag template that is deleted. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param force: Required. Currently, this field must always be set to ``true``. This confirms the - deletion of any possible tags using this template. ``force = false`` will be supported in the - future. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "tag_template", - "force", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - - def __init__( - self, - *, - location: str, - tag_template: str, - force: bool, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.tag_template = tag_template - self.force = force - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> None: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - try: - hook.delete_tag_template( - location=self.location, - tag_template=self.tag_template, - force=self.force, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - except NotFound: - self.log.info("Tag Template doesn't exists. skipping") - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogDeleteTagTemplateFieldOperator(GoogleCloudBaseOperator): - """ - Deletes a field in a tag template and all uses of that field. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogDeleteTagTemplateFieldOperator` - - :param location: Required. The location of the tag template to delete. - :param tag_template: Tag Template ID for tag template field that is deleted. - :param field: Name of field that is deleted. - :param force: Required. This confirms the deletion of this field from any tags using this field. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "tag_template", - "field", - "force", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - - def __init__( - self, - *, - location: str, - tag_template: str, - field: str, - force: bool, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.tag_template = tag_template - self.field = field - self.force = force - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> None: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - try: - hook.delete_tag_template_field( - location=self.location, - tag_template=self.tag_template, - field=self.field, - force=self.force, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - except NotFound: - self.log.info("Tag Template field doesn't exists. skipping") - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetEntryOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogGetEntryOperator(GoogleCloudBaseOperator): - """ - Gets an entry. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogGetEntryOperator` - - :param location: Required. The location of the entry to get. - :param entry_group: Required. The entry group of the entry to get. - :param entry: The ID of the entry to get. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "entry_group", - "entry", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogEntryLink(),) - - def __init__( - self, - *, - location: str, - entry_group: str, - entry: str, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.entry_group = entry_group - self.entry = entry - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> dict: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - result = hook.get_entry( - location=self.location, - entry_group=self.entry_group, - entry=self.entry, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - DataCatalogEntryLink.persist( - context=context, - entry_id=self.entry, - entry_group_id=self.entry_group, - location_id=self.location, - project_id=self.project_id or hook.project_id, - ) - return Entry.to_dict(result) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetEntryGroupOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogGetEntryGroupOperator(GoogleCloudBaseOperator): - """ - Gets an entry group. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogGetEntryGroupOperator` - - :param location: Required. The location of the entry group to get. - :param entry_group: The ID of the entry group to get. - :param read_mask: The fields to return. If not set or empty, all fields are returned. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.protobuf.field_mask_pb2.FieldMask` - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "entry_group", - "read_mask", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogEntryGroupLink(),) - - def __init__( - self, - *, - location: str, - entry_group: str, - read_mask: FieldMask, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.entry_group = entry_group - self.read_mask = read_mask - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> dict: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - result = hook.get_entry_group( - location=self.location, - entry_group=self.entry_group, - read_mask=self.read_mask, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - DataCatalogEntryGroupLink.persist( - context=context, - entry_group_id=self.entry_group, - location_id=self.location, - project_id=self.project_id or hook.project_id, - ) - return EntryGroup.to_dict(result) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetAspectTypeOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogGetTagTemplateOperator(GoogleCloudBaseOperator): - """ - Gets a tag template. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogGetTagTemplateOperator` - - :param location: Required. The location of the tag template to get. - :param tag_template: Required. The ID of the tag template to get. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "tag_template", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogTagTemplateLink(),) - - def __init__( - self, - *, - location: str, - tag_template: str, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.tag_template = tag_template - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> dict: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - result = hook.get_tag_template( - location=self.location, - tag_template=self.tag_template, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - DataCatalogTagTemplateLink.persist( - context=context, - tag_template_id=self.tag_template, - location_id=self.location, - project_id=self.project_id or hook.project_id, - ) - return TagTemplate.to_dict(result) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogGetEntryOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogListTagsOperator(GoogleCloudBaseOperator): - """ - Lists the tags on an Entry. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogListTagsOperator` - - :param location: Required. The location of the tags to get. - :param entry_group: Required. The entry group of the tags to get. - :param entry: Required. The entry of the tags to get. - :param page_size: The maximum number of resources contained in the underlying API response. If page - streaming is performed per- resource, this parameter does not affect the return value. If page - streaming is performed per-page, this determines the maximum number of resources in a page. - (Default: 100) - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "entry_group", - "entry", - "page_size", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogEntryLink(),) - - def __init__( - self, - *, - location: str, - entry_group: str, - entry: str, - page_size: int = 100, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.entry_group = entry_group - self.entry = entry - self.page_size = page_size - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> list: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - result = hook.list_tags( - location=self.location, - entry_group=self.entry_group, - entry=self.entry, - page_size=self.page_size, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - DataCatalogEntryLink.persist( - context=context, - entry_id=self.entry, - entry_group_id=self.entry_group, - location_id=self.location, - project_id=self.project_id or hook.project_id, - ) - return [Tag.to_dict(item) for item in result] - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogLookupEntryOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogLookupEntryOperator(GoogleCloudBaseOperator): - r""" - Get an entry by target resource name. - - This method allows clients to use the resource name from the source Google Cloud service - to get the Data Catalog Entry. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogLookupEntryOperator` - - :param linked_resource: The full name of the Google Cloud resource the Data Catalog entry - represents. See: https://cloud.google.com/apis/design/resource\_names#full\_resource\_name. Full - names are case-sensitive. - :param sql_resource: The SQL name of the entry. SQL names are case-sensitive. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "linked_resource", - "sql_resource", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogEntryLink(),) - - def __init__( - self, - *, - linked_resource: str | None = None, - sql_resource: str | None = None, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.linked_resource = linked_resource - self.sql_resource = sql_resource - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> dict: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - result = hook.lookup_entry( - linked_resource=self.linked_resource, - sql_resource=self.sql_resource, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - - project_id, location_id, entry_group_id, entry_id = result.name.split("/")[1::2] - DataCatalogEntryLink.persist( - context=context, - entry_id=entry_id, - entry_group_id=entry_group_id, - location_id=location_id, - project_id=project_id, - ) - return Entry.to_dict(result) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogRenameTagTemplateFieldOperator(GoogleCloudBaseOperator): - """ - Renames a field in a tag template. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogRenameTagTemplateFieldOperator` - - :param location: Required. The location of the tag template field to rename. - :param tag_template: The tag template ID for field that is renamed. - :param field: Required. The old ID of this tag template field. For example, - ``my_old_field``. - :param new_tag_template_field_id: Required. The new ID of this tag template field. For example, - ``my_new_field``. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "location", - "tag_template", - "field", - "new_tag_template_field_id", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogTagTemplateLink(),) - - def __init__( - self, - *, - location: str, - tag_template: str, - field: str, - new_tag_template_field_id: str, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.location = location - self.tag_template = tag_template - self.field = field - self.new_tag_template_field_id = new_tag_template_field_id - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> None: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - hook.rename_tag_template_field( - location=self.location, - tag_template=self.tag_template, - field=self.field, - new_tag_template_field_id=self.new_tag_template_field_id, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - DataCatalogTagTemplateLink.persist( - context=context, - tag_template_id=self.tag_template, - location_id=self.location, - project_id=self.project_id or hook.project_id, - ) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogSearchEntriesOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogSearchCatalogOperator(GoogleCloudBaseOperator): - r""" - Searches Data Catalog for multiple resources like entries, tags that match a query. - - This does not return the complete resource, only the resource identifier and high level fields. - Clients can subsequently call ``Get`` methods. - - Note that searches do not have full recall. There may be results that match your query but are not - returned, even in subsequent pages of results. These missing results may vary across repeated calls to - search. Do not rely on this method if you need to guarantee full recall. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogSearchCatalogOperator` - - :param scope: Required. The scope of this search request. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.Scope` - :param query: Required. The query string in search query syntax. The query must be non-empty. - - Query strings can be simple as "x" or more qualified as: - - - name:x - - column:x - - description:y - - Note: Query tokens need to have a minimum of 3 characters for substring matching to work - correctly. See `Data Catalog Search Syntax `__ for more information. - :param page_size: The maximum number of resources contained in the underlying API response. If page - streaming is performed per-resource, this parameter does not affect the return value. If page - streaming is performed per-page, this determines the maximum number of resources in a page. - :param order_by: Specifies the ordering of results, currently supported case-sensitive choices are: - - - ``relevance``, only supports descending - - ``last_access_timestamp [asc|desc]``, defaults to descending if not specified - - ``last_modified_timestamp [asc|desc]``, defaults to descending if not specified - - If not specified, defaults to ``relevance`` descending. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "scope", - "query", - "page_size", - "order_by", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - - def __init__( - self, - *, - scope: dict | SearchCatalogRequest.Scope, - query: str, - page_size: int = 100, - order_by: str | None = None, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.scope = scope - self.query = query - self.page_size = page_size - self.order_by = order_by - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> list: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - result = hook.search_catalog( - scope=self.scope, - query=self.query, - page_size=self.page_size, - order_by=self.order_by, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - return [SearchCatalogResult.to_dict(item) for item in result] - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogUpdateEntryOperator(GoogleCloudBaseOperator): - """ - Updates an existing entry. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogUpdateEntryOperator` - - :param entry: Required. The updated entry. The "name" field must be set. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.Entry` - :param update_mask: The fields to update on the entry. If absent or empty, all modifiable fields are - updated. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.protobuf.field_mask_pb2.FieldMask` - :param location: Required. The location of the entry to update. - :param entry_group: The entry group ID for the entry that is being updated. - :param entry_id: The entry ID that is being updated. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "entry", - "update_mask", - "location", - "entry_group", - "entry_id", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogEntryLink(),) - - def __init__( - self, - *, - entry: dict | Entry, - update_mask: dict | FieldMask, - location: str | None = None, - entry_group: str | None = None, - entry_id: str | None = None, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.entry = entry - self.update_mask = update_mask - self.location = location - self.entry_group = entry_group - self.entry_id = entry_id - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> None: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - result = hook.update_entry( - entry=self.entry, - update_mask=self.update_mask, - location=self.location, - entry_group=self.entry_group, - entry_id=self.entry_id, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - - location_id, entry_group_id, entry_id = result.name.split("/")[3::2] - DataCatalogEntryLink.persist( - context=context, - entry_id=self.entry_id or entry_id, - entry_group_id=self.entry_group or entry_group_id, - location_id=self.location or location_id, - project_id=self.project_id or hook.project_id, - ) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateEntryOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogUpdateTagOperator(GoogleCloudBaseOperator): - """ - Updates an existing tag. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogUpdateTagOperator` - - :param tag: Required. The updated tag. The "name" field must be set. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.Tag` - :param update_mask: The fields to update on the Tag. If absent or empty, all modifiable fields are - updated. Currently the only modifiable field is the field ``fields``. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.protobuf.field_mask_pb2.FieldMask` - :param location: Required. The location of the tag to rename. - :param entry_group: The entry group ID for the tag that is being updated. - :param entry: The entry ID for the tag that is being updated. - :param tag_id: The tag ID that is being updated. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "tag", - "update_mask", - "location", - "entry_group", - "entry", - "tag_id", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogEntryLink(),) - - def __init__( - self, - *, - tag: dict | Tag, - update_mask: dict | FieldMask, - location: str | None = None, - entry_group: str | None = None, - entry: str | None = None, - tag_id: str | None = None, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.tag = tag - self.update_mask = update_mask - self.location = location - self.entry_group = entry_group - self.entry = entry - self.tag_id = tag_id - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> None: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - result = hook.update_tag( - tag=self.tag, - update_mask=self.update_mask, - location=self.location, - entry_group=self.entry_group, - entry=self.entry, - tag_id=self.tag_id, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - - location_id, entry_group_id, entry_id = result.name.split("/")[3:8:2] - DataCatalogEntryLink.persist( - context=context, - entry_id=self.entry or entry_id, - entry_group_id=self.entry_group or entry_group_id, - location_id=self.location or location_id, - project_id=self.project_id or hook.project_id, - ) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogUpdateTagTemplateOperator(GoogleCloudBaseOperator): - """ - Updates a tag template. - - This method cannot be used to update the fields of a template. The tag - template fields are represented as separate resources and should be updated using their own - create/update/delete methods. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogUpdateTagTemplateOperator` - - :param tag_template: Required. The template to update. The "name" field must be set. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.TagTemplate` - :param update_mask: The field mask specifies the parts of the template to overwrite. - - If absent or empty, all of the allowed fields above will be updated. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.protobuf.field_mask_pb2.FieldMask` - :param location: Required. The location of the tag template to rename. - :param tag_template_id: Optional. The tag template ID for the entry that is being updated. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "tag_template", - "update_mask", - "location", - "tag_template_id", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogTagTemplateLink(),) - - def __init__( - self, - *, - tag_template: dict | TagTemplate, - update_mask: dict | FieldMask, - location: str | None = None, - tag_template_id: str | None = None, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.tag_template = tag_template - self.update_mask = update_mask - self.location = location - self.tag_template_id = tag_template_id - self.project_id = project_id - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> None: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - result = hook.update_tag_template( - tag_template=self.tag_template, - update_mask=self.update_mask, - location=self.location, - tag_template_id=self.tag_template_id, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - - location_id, tag_template_id = result.name.split("/")[3::2] - DataCatalogTagTemplateLink.persist( - context=context, - tag_template_id=self.tag_template_id or tag_template_id, - location_id=self.location or location_id, - project_id=self.project_id or hook.project_id, - ) - - -@deprecated( - planned_removal_date="January 30, 2026", - use_instead="airflow.providers.google.cloud.operators.dataplex.DataplexCatalogUpdateAspectTypeOperator", - reason="The Data Catalog will be discontinued on January 30, 2026 " - "in favor of Dataplex Universal Catalog.", - category=AirflowProviderDeprecationWarning, -) -class CloudDataCatalogUpdateTagTemplateFieldOperator(GoogleCloudBaseOperator): - """ - Updates a field in a tag template. This method cannot be used to update the field type. - - .. seealso:: - For more information on how to use this operator, take a look at the guide: - :ref:`howto/operator:CloudDataCatalogUpdateTagTemplateFieldOperator` - - :param tag_template_field: Required. The template to update. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.cloud.datacatalog_v1beta1.types.TagTemplateField` - :param update_mask: The field mask specifies the parts of the template to be updated. Allowed fields: - - - ``display_name`` - - ``type.enum_type`` - - If ``update_mask`` is not set or empty, all of the allowed fields above will be updated. - - When updating an enum type, the provided values will be merged with the existing values. - Therefore, enum values can only be added, existing enum values cannot be deleted nor renamed. - - If a dict is provided, it must be of the same form as the protobuf message - :class:`~google.protobuf.field_mask_pb2.FieldMask` - :param tag_template_field_name: Optional. The name of the tag template field to rename. - :param location: Optional. The location of the tag to rename. - :param tag_template: Optional. The tag template ID for tag template field to rename. - :param tag_template_field_id: Optional. The ID of tag template field to rename. - :param project_id: The ID of the Google Cloud project that owns the entry group. - If set to ``None`` or missing, the default project_id from the Google Cloud connection is used. - :param retry: A retry object used to retry requests. If ``None`` is specified, requests will be - retried using a default configuration. - :param timeout: The amount of time, in seconds, to wait for the request to complete. Note that if - ``retry`` is specified, the timeout applies to each individual attempt. - :param metadata: Additional metadata that is provided to the method. - :param gcp_conn_id: Optional, The connection ID used to connect to Google Cloud. - Defaults to 'google_cloud_default'. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields: Sequence[str] = ( - "tag_template_field", - "update_mask", - "tag_template_field_name", - "location", - "tag_template", - "tag_template_field_id", - "project_id", - "retry", - "timeout", - "metadata", - "gcp_conn_id", - "impersonation_chain", - ) - operator_extra_links = (DataCatalogTagTemplateLink(),) - - def __init__( - self, - *, - tag_template_field: dict | TagTemplateField, - update_mask: dict | FieldMask, - tag_template_field_name: str | None = None, - location: str | None = None, - tag_template: str | None = None, - tag_template_field_id: str | None = None, - project_id: str = PROVIDE_PROJECT_ID, - retry: Retry | _MethodDefault = DEFAULT, - timeout: float | None = None, - metadata: Sequence[tuple[str, str]] = (), - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.tag_template_field_name = tag_template_field_name - self.location = location - self.tag_template = tag_template - self.tag_template_field_id = tag_template_field_id - self.project_id = project_id - self.tag_template_field = tag_template_field - self.update_mask = update_mask - self.retry = retry - self.timeout = timeout - self.metadata = metadata - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> None: - hook = CloudDataCatalogHook( - gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain - ) - result = hook.update_tag_template_field( - tag_template_field=self.tag_template_field, - update_mask=self.update_mask, - tag_template_field_name=self.tag_template_field_name, - location=self.location, - tag_template=self.tag_template, - tag_template_field_id=self.tag_template_field_id, - project_id=self.project_id, - retry=self.retry, - timeout=self.timeout, - metadata=self.metadata, - ) - - location_id, tag_template_id = result.name.split("/")[3:6:2] - DataCatalogTagTemplateLink.persist( - context=context, - tag_template_id=self.tag_template or tag_template_id, - location_id=self.location or location_id, - project_id=self.project_id or hook.project_id, - ) diff --git a/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/auto_ml.py b/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/auto_ml.py index 07780a2754034..7884bac0c00fd 100644 --- a/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/auto_ml.py +++ b/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/auto_ml.py @@ -477,7 +477,7 @@ def execute(self, context: Context): @deprecated( planned_removal_date="March 24, 2026", - use_instead="airflow.providers.google.cloud.operators.vertex_ai.generative_model.SupervisedFineTuningTrainOperator", + use_instead="airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAISupervisedFineTuningTrainOperator", category=AirflowProviderDeprecationWarning, ) class CreateAutoMLVideoTrainingJobOperator(AutoMLTrainingJobBaseOperator): diff --git a/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/generative_model.py b/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/generative_model.py index 94c9f01e8b215..de500e3388939 100644 --- a/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/generative_model.py +++ b/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/generative_model.py @@ -20,351 +20,17 @@ from __future__ import annotations from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING -from google.api_core import exceptions - -from airflow.exceptions import AirflowProviderDeprecationWarning -from airflow.providers.common.compat.sdk import AirflowException from airflow.providers.google.cloud.hooks.vertex_ai.generative_model import ( - ExperimentRunHook, GenerativeModelHook, ) from airflow.providers.google.cloud.operators.cloud_base import GoogleCloudBaseOperator -from airflow.providers.google.common.deprecated import deprecated if TYPE_CHECKING: from airflow.providers.common.compat.sdk import Context -@deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAIGenerateEmbeddingsOperator", - category=AirflowProviderDeprecationWarning, -) -class TextEmbeddingModelGetEmbeddingsOperator(GoogleCloudBaseOperator): - """ - Uses the Vertex AI Embeddings API to generate embeddings based on prompt. - - :param project_id: Required. The ID of the Google Cloud project that the - service belongs to (templated). - :param location: Required. The ID of the Google Cloud location that the - service belongs to (templated). - :param prompt: Required. Inputs or queries that a user or a program gives - to the Vertex AI Generative Model API, in order to elicit a specific response (templated). - :param pretrained_model: Required. Model, optimized for performing text embeddings. - :param gcp_conn_id: The connection ID to use connecting to Google Cloud. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields = ("location", "project_id", "impersonation_chain", "prompt", "pretrained_model") - - def __init__( - self, - *, - project_id: str, - location: str, - prompt: str, - pretrained_model: str, - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.project_id = project_id - self.location = location - self.prompt = prompt - self.pretrained_model = pretrained_model - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context): - self.hook = GenerativeModelHook( - gcp_conn_id=self.gcp_conn_id, - impersonation_chain=self.impersonation_chain, - ) - - self.log.info("Generating text embeddings") - response = self.hook.text_embedding_model_get_embeddings( - project_id=self.project_id, - location=self.location, - prompt=self.prompt, - pretrained_model=self.pretrained_model, - ) - - self.log.info("Model response: %s", response) - context["ti"].xcom_push(key="model_response", value=response) - - return response - - -@deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAIGenerateContentOperator", - category=AirflowProviderDeprecationWarning, -) -class GenerativeModelGenerateContentOperator(GoogleCloudBaseOperator): - """ - Use the Vertex AI Gemini Pro foundation model to generate content. - - :param project_id: Required. The ID of the Google Cloud project that the - service belongs to (templated). - :param location: Required. The ID of the Google Cloud location that the - service belongs to (templated). - :param contents: Required. The multi-part content of a message that a user or a program - gives to the generative model, in order to elicit a specific response. - :param generation_config: Optional. Generation configuration settings. - :param safety_settings: Optional. Per request settings for blocking unsafe content. - :param tools: Optional. A list of tools available to the model during evaluation, such as a data store. - :param system_instruction: Optional. An instruction given to the model to guide its behavior. - :param pretrained_model: Required. The name of the model to use for content generation, - which can be a text-only or multimodal model. For example, `gemini-pro` or - `gemini-pro-vision`. - :param gcp_conn_id: The connection ID to use connecting to Google Cloud. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields = ("location", "project_id", "impersonation_chain", "contents", "pretrained_model") - - def __init__( - self, - *, - project_id: str, - location: str, - contents: list, - tools: list | None = None, - generation_config: dict | None = None, - safety_settings: dict | None = None, - system_instruction: str | None = None, - pretrained_model: str, - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.project_id = project_id - self.location = location - self.contents = contents - self.tools = tools - self.generation_config = generation_config - self.safety_settings = safety_settings - self.system_instruction = system_instruction - self.pretrained_model = pretrained_model - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context): - self.hook = GenerativeModelHook( - gcp_conn_id=self.gcp_conn_id, - impersonation_chain=self.impersonation_chain, - ) - response = self.hook.generative_model_generate_content( - project_id=self.project_id, - location=self.location, - contents=self.contents, - tools=self.tools, - generation_config=self.generation_config, - safety_settings=self.safety_settings, - system_instruction=self.system_instruction, - pretrained_model=self.pretrained_model, - ) - - self.log.info("Model response: %s", response) - context["ti"].xcom_push(key="model_response", value=response) - - return response - - -@deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAISupervisedFineTuningTrainOperator", - category=AirflowProviderDeprecationWarning, -) -class SupervisedFineTuningTrainOperator(GoogleCloudBaseOperator): - """ - Use the Supervised Fine Tuning API to create a tuning job. - - :param project_id: Required. The ID of the Google Cloud project that the - service belongs to. - :param location: Required. The ID of the Google Cloud location that the service belongs to. - :param source_model: Required. A pre-trained model optimized for performing natural - language tasks such as classification, summarization, extraction, content - creation, and ideation. - :param train_dataset: Required. Cloud Storage URI of your training dataset. The dataset - must be formatted as a JSONL file. For best results, provide at least 100 to 500 examples. - :param tuned_model_display_name: Optional. Display name of the TunedModel. The name can be up - to 128 characters long and can consist of any UTF-8 characters. - :param validation_dataset: Optional. Cloud Storage URI of your training dataset. The dataset must be - formatted as a JSONL file. For best results, provide at least 100 to 500 examples. - :param epochs: Optional. To optimize performance on a specific dataset, try using a higher - epoch value. Increasing the number of epochs might improve results. However, be cautious - about over-fitting, especially when dealing with small datasets. If over-fitting occurs, - consider lowering the epoch number. - :param adapter_size: Optional. Adapter size for tuning. - :param learning_multiplier_rate: Optional. Multiplier for adjusting the default learning rate. - :param gcp_conn_id: The connection ID to use connecting to Google Cloud. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields = ( - "location", - "project_id", - "impersonation_chain", - "train_dataset", - "validation_dataset", - "source_model", - ) - - def __init__( - self, - *, - project_id: str, - location: str, - source_model: str, - train_dataset: str, - tuned_model_display_name: str | None = None, - validation_dataset: str | None = None, - epochs: int | None = None, - adapter_size: Literal[1, 4, 8, 16] | None = None, - learning_rate_multiplier: float | None = None, - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.project_id = project_id - self.location = location - self.source_model = source_model - self.train_dataset = train_dataset - self.tuned_model_display_name = tuned_model_display_name - self.validation_dataset = validation_dataset - self.epochs = epochs - self.adapter_size = adapter_size - self.learning_rate_multiplier = learning_rate_multiplier - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context): - self.hook = GenerativeModelHook( - gcp_conn_id=self.gcp_conn_id, - impersonation_chain=self.impersonation_chain, - ) - response = self.hook.supervised_fine_tuning_train( - project_id=self.project_id, - location=self.location, - source_model=self.source_model, - train_dataset=self.train_dataset, - validation_dataset=self.validation_dataset, - epochs=self.epochs, - adapter_size=self.adapter_size, - learning_rate_multiplier=self.learning_rate_multiplier, - tuned_model_display_name=self.tuned_model_display_name, - ) - - self.log.info("Tuned Model Name: %s", response.tuned_model_name) - self.log.info("Tuned Model Endpoint Name: %s", response.tuned_model_endpoint_name) - - context["ti"].xcom_push(key="tuned_model_name", value=response.tuned_model_name) - context["ti"].xcom_push(key="tuned_model_endpoint_name", value=response.tuned_model_endpoint_name) - - result = { - "tuned_model_name": response.tuned_model_name, - "tuned_model_endpoint_name": response.tuned_model_endpoint_name, - } - - return result - - -@deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAICountTokensOperator", - category=AirflowProviderDeprecationWarning, -) -class CountTokensOperator(GoogleCloudBaseOperator): - """ - Use the Vertex AI Count Tokens API to calculate the number of input tokens before sending a request to the Gemini API. - - :param project_id: Required. The ID of the Google Cloud project that the - service belongs to (templated). - :param location: Required. The ID of the Google Cloud location that the - service belongs to (templated). - :param contents: Required. The multi-part content of a message that a user or a program - gives to the generative model, in order to elicit a specific response. - :param pretrained_model: Required. Model, supporting prompts with text-only input, - including natural language tasks, multi-turn text and code chat, - and code generation. It can output text and code. - :param gcp_conn_id: The connection ID to use connecting to Google Cloud. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields = ("location", "project_id", "impersonation_chain", "contents", "pretrained_model") - - def __init__( - self, - *, - project_id: str, - location: str, - contents: list, - pretrained_model: str, - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.project_id = project_id - self.location = location - self.contents = contents - self.pretrained_model = pretrained_model - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context): - self.hook = GenerativeModelHook( - gcp_conn_id=self.gcp_conn_id, - impersonation_chain=self.impersonation_chain, - ) - response = self.hook.count_tokens( - project_id=self.project_id, - location=self.location, - contents=self.contents, - pretrained_model=self.pretrained_model, - ) - - self.log.info("Total tokens: %s", response.total_tokens) - self.log.info("Total billable characters: %s", response.total_billable_characters) - - context["ti"].xcom_push(key="total_tokens", value=response.total_tokens) - context["ti"].xcom_push(key="total_billable_characters", value=response.total_billable_characters) - - class RunEvaluationOperator(GoogleCloudBaseOperator): """ Use the Rapid Evaluation API to evaluate a model. @@ -462,235 +128,3 @@ def execute(self, context: Context): ) return response.summary_metrics - - -@deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAICreateCachedContentOperator", - category=AirflowProviderDeprecationWarning, -) -class CreateCachedContentOperator(GoogleCloudBaseOperator): - """ - Create CachedContent to reduce the cost of requests that contain repeat content with high input token counts. - - :param project_id: Required. The ID of the Google Cloud project that the service belongs to. - :param location: Required. The ID of the Google Cloud location that the service belongs to. - :param model_name: Required. The name of the publisher model to use for cached content. - :param system_instruction: Developer set system instruction. - :param contents: The content to cache. - :param ttl_hours: The TTL for this resource in hours. The expiration time is computed: now + TTL. - Defaults to one hour. - :param display_name: The user-generated meaningful display name of the cached content - :param gcp_conn_id: The connection ID to use connecting to Google Cloud. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields = ( - "location", - "project_id", - "impersonation_chain", - "model_name", - "contents", - "system_instruction", - ) - - def __init__( - self, - *, - project_id: str, - location: str, - model_name: str, - system_instruction: Any | None = None, - contents: list[Any] | None = None, - ttl_hours: float = 1, - display_name: str | None = None, - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - - self.project_id = project_id - self.location = location - self.model_name = model_name - self.system_instruction = system_instruction - self.contents = contents - self.ttl_hours = ttl_hours - self.display_name = display_name - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context): - self.hook = GenerativeModelHook( - gcp_conn_id=self.gcp_conn_id, - impersonation_chain=self.impersonation_chain, - ) - - cached_content_name = self.hook.create_cached_content( - project_id=self.project_id, - location=self.location, - model_name=self.model_name, - system_instruction=self.system_instruction, - contents=self.contents, - ttl_hours=self.ttl_hours, - display_name=self.display_name, - ) - - self.log.info("Cached Content Name: %s", cached_content_name) - - return cached_content_name - - -@deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.operators.gen_ai.generative_model.GenAIGenerateContentOperator", - category=AirflowProviderDeprecationWarning, -) -class GenerateFromCachedContentOperator(GoogleCloudBaseOperator): - """ - Generate a response from CachedContent. - - :param project_id: Required. The ID of the Google Cloud project that the service belongs to. - :param location: Required. The ID of the Google Cloud location that the service belongs to. - :param cached_content_name: Required. The name of the cached content resource. - :param contents: Required. The multi-part content of a message that a user or a program - gives to the generative model, in order to elicit a specific response. - :param generation_config: Optional. Generation configuration settings. - :param safety_settings: Optional. Per request settings for blocking unsafe content. - :param gcp_conn_id: The connection ID to use connecting to Google Cloud. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields = ( - "location", - "project_id", - "impersonation_chain", - "cached_content_name", - "contents", - ) - - def __init__( - self, - *, - project_id: str, - location: str, - cached_content_name: str, - contents: list, - generation_config: dict | None = None, - safety_settings: dict | None = None, - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - - self.project_id = project_id - self.location = location - self.cached_content_name = cached_content_name - self.contents = contents - self.generation_config = generation_config - self.safety_settings = safety_settings - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context): - self.hook = GenerativeModelHook( - gcp_conn_id=self.gcp_conn_id, - impersonation_chain=self.impersonation_chain, - ) - cached_content_text = self.hook.generate_from_cached_content( - project_id=self.project_id, - location=self.location, - cached_content_name=self.cached_content_name, - contents=self.contents, - generation_config=self.generation_config, - safety_settings=self.safety_settings, - ) - - self.log.info("Cached Content Response: %s", cached_content_text) - - return cached_content_text - - -@deprecated( - planned_removal_date="January 3, 2026", - use_instead="airflow.providers.google.cloud.operators.vertex_ai.experiment_service.DeleteExperimentRunOperator", - category=AirflowProviderDeprecationWarning, -) -class DeleteExperimentRunOperator(GoogleCloudBaseOperator): - """ - Use the Rapid Evaluation API to evaluate a model. - - :param project_id: Required. The ID of the Google Cloud project that the service belongs to. - :param location: Required. The ID of the Google Cloud location that the service belongs to. - :param experiment_name: Required. The name of the evaluation experiment. - :param experiment_run_name: Required. The specific run name or ID for this experiment. - :param gcp_conn_id: The connection ID to use connecting to Google Cloud. - :param impersonation_chain: Optional service account to impersonate using short-term - credentials, or chained list of accounts required to get the access_token - of the last account in the list, which will be impersonated in the request. - If set as a string, the account must grant the originating account - the Service Account Token Creator IAM role. - If set as a sequence, the identities from the list must grant - Service Account Token Creator IAM role to the directly preceding identity, with first - account from the list granting this role to the originating account (templated). - """ - - template_fields = ( - "location", - "project_id", - "impersonation_chain", - "experiment_name", - "experiment_run_name", - ) - - def __init__( - self, - *, - project_id: str, - location: str, - experiment_name: str, - experiment_run_name: str, - gcp_conn_id: str = "google_cloud_default", - impersonation_chain: str | Sequence[str] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.project_id = project_id - self.location = location - self.experiment_name = experiment_name - self.experiment_run_name = experiment_run_name - self.gcp_conn_id = gcp_conn_id - self.impersonation_chain = impersonation_chain - - def execute(self, context: Context) -> None: - self.hook = ExperimentRunHook( - gcp_conn_id=self.gcp_conn_id, - impersonation_chain=self.impersonation_chain, - ) - - try: - self.hook.delete_experiment_run( - project_id=self.project_id, - location=self.location, - experiment_name=self.experiment_name, - experiment_run_name=self.experiment_run_name, - ) - except exceptions.NotFound: - raise AirflowException(f"Experiment Run with name {self.experiment_run_name} not found") - - self.log.info("Deleted experiment run: %s", self.experiment_run_name) diff --git a/providers/google/src/airflow/providers/google/get_provider_info.py b/providers/google/src/airflow/providers/google/get_provider_info.py index 4f45d95ad46bb..cab93aa57c9e1 100644 --- a/providers/google/src/airflow/providers/google/get_provider_info.py +++ b/providers/google/src/airflow/providers/google/get_provider_info.py @@ -268,13 +268,6 @@ def get_provider_info(): "logo": "/docs/integration-logos/Google-Data-Proc.png", "tags": ["gcp"], }, - { - "integration-name": "Google Data Catalog", - "external-doc-url": "https://cloud.google.com/data-catalog/", - "how-to-guide": ["/docs/apache-airflow-providers-google/operators/cloud/datacatalog.rst"], - "logo": "/docs/integration-logos/Google-Data-Catalog.png", - "tags": ["gcp"], - }, { "integration-name": "Google Dataflow", "external-doc-url": "https://cloud.google.com/dataflow/", diff --git a/providers/google/tests/unit/google/cloud/hooks/test_datacatalog.py b/providers/google/tests/unit/google/cloud/hooks/test_datacatalog.py deleted file mode 100644 index f1a93a9aa8178..0000000000000 --- a/providers/google/tests/unit/google/cloud/hooks/test_datacatalog.py +++ /dev/null @@ -1,1600 +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 re -from collections.abc import Sequence -from copy import deepcopy -from typing import TYPE_CHECKING -from unittest import mock - -import pytest -from google.api_core.retry import Retry -from google.cloud.datacatalog import CreateTagRequest, CreateTagTemplateRequest, Entry, Tag, TagTemplate -from google.protobuf.field_mask_pb2 import FieldMask - -from airflow.exceptions import AirflowProviderDeprecationWarning -from airflow.providers.common.compat.sdk import AirflowException -from airflow.providers.google.cloud.hooks.datacatalog import CloudDataCatalogHook - -from unit.google.cloud.utils.base_gcp_mock import ( - mock_base_gcp_hook_default_project_id, - mock_base_gcp_hook_no_default_project_id, -) - -if TYPE_CHECKING: - from google.api_core.gapic_v1.method import _MethodDefault - -TEST_GCP_CONN_ID: str = "test-gcp-conn-id" -TEST_LOCATION: str = "europe-west-3b" -TEST_ENTRY_ID: str = "test-entry-id" -TEST_ENTRY: dict = {} -TEST_RETRY: Retry | _MethodDefault = Retry() -TEST_TIMEOUT: float = 4 -TEST_METADATA: Sequence[tuple[str, str]] = () -TEST_ENTRY_GROUP_ID: str = "test-entry-group-id" -TEST_ENTRY_GROUP: dict = {} -TEST_TAG: dict = {} -TEST_TAG_TEMPLATE_ID: str = "test-tag-template-id" -TEST_TAG_TEMPLATE: dict = {"name": TEST_TAG_TEMPLATE_ID} -TEST_TAG_TEMPLATE_FIELD_ID: str = "test-tag-template-field-id" -TEST_TAG_TEMPLATE_FIELD: dict = {} -TEST_FORCE: bool = False -TEST_READ_MASK: FieldMask = FieldMask(paths=["name"]) -TEST_RESOURCE: str = "test-resource" -TEST_PAGE_SIZE: int = 50 -TEST_LINKED_RESOURCE: str = "test-linked-resource" -TEST_SQL_RESOURCE: str = "test-sql-resource" -TEST_NEW_TAG_TEMPLATE_FIELD_ID: str = "test-new-tag-template-field-id" -TEST_SCOPE: dict = {"include_project_ids": ["example-scope-project"]} -TEST_QUERY: str = "test-query" -TEST_ORDER_BY: str = "test-order-by" -TEST_UPDATE_MASK: dict = {"fields": ["name"]} -TEST_PARENT: str = "test-parent" -TEST_NAME: str = "test-name" -TEST_TAG_ID: str = "test-tag-id" -TEST_LOCATION_PATH: str = f"projects/{{}}/locations/{TEST_LOCATION}" -TEST_ENTRY_PATH: str = ( - f"projects/{{}}/locations/{TEST_LOCATION}/entryGroups/{TEST_ENTRY_GROUP_ID}/entries/{TEST_ENTRY_ID}" -) -TEST_ENTRY_GROUP_PATH: str = f"projects/{{}}/locations/{TEST_LOCATION}/entryGroups/{TEST_ENTRY_GROUP_ID}" -TEST_TAG_TEMPLATE_PATH: str = f"projects/{{}}/locations/{TEST_LOCATION}/tagTemplates/{TEST_TAG_TEMPLATE_ID}" -TEST_TAG_TEMPLATE_FIELD_PATH: str = ( - f"projects/{{}}/locations/{TEST_LOCATION}/tagTemplates/" - f"{TEST_TAG_TEMPLATE_ID}/fields/{TEST_TAG_TEMPLATE_FIELD_ID}" -) -TEST_TAG_PATH: str = ( - f"projects/{{}}/locations/{TEST_LOCATION}/entryGroups/{TEST_ENTRY_GROUP_ID}" - f"/entries/{TEST_ENTRY_ID}/tags/{TEST_TAG_ID}" -) -TEST_PROJECT_ID_1 = "example-project-1" -TEST_PROJECT_ID_2 = "example-project-2" -TEST_CREDENTIALS = mock.MagicMock() - - -class TestCloudDataCatalog: - def setup_method(self): - with pytest.warns(AirflowProviderDeprecationWarning): - with mock.patch( - "airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.__init__", - new=mock_base_gcp_hook_default_project_id, - ): - self.hook = CloudDataCatalogHook(gcp_conn_id="test") - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_lookup_entry_with_linked_resource(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.lookup_entry( - linked_resource=TEST_LINKED_RESOURCE, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.lookup_entry.assert_called_once_with( - request=dict(linked_resource=TEST_LINKED_RESOURCE), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_lookup_entry_with_sql_resource(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.lookup_entry( - sql_resource=TEST_SQL_RESOURCE, retry=TEST_RETRY, timeout=TEST_TIMEOUT, metadata=TEST_METADATA - ) - mock_get_conn.return_value.lookup_entry.assert_called_once_with( - request=dict(sql_resource=TEST_SQL_RESOURCE), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_lookup_entry_without_resource(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises( - AirflowException, match=re.escape("At least one of linked_resource, sql_resource should be set.") - ): - self.hook.lookup_entry(retry=TEST_RETRY, timeout=TEST_TIMEOUT, metadata=TEST_METADATA) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_search_catalog(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.search_catalog( - scope=TEST_SCOPE, - query=TEST_QUERY, - page_size=TEST_PAGE_SIZE, - order_by=TEST_ORDER_BY, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.search_catalog.assert_called_once_with( - request=dict( - scope=TEST_SCOPE, query=TEST_QUERY, page_size=TEST_PAGE_SIZE, order_by=TEST_ORDER_BY - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogWithDefaultProjectIdHook: - def setup_method(self): - with pytest.warns(AirflowProviderDeprecationWarning): - with mock.patch( - "airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.__init__", - new=mock_base_gcp_hook_default_project_id, - ): - self.hook = CloudDataCatalogHook(gcp_conn_id="test") - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_entry(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.create_entry( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - entry=TEST_ENTRY, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.create_entry.assert_called_once_with( - request=dict( - parent=TEST_ENTRY_GROUP_PATH.format(TEST_PROJECT_ID_1), - entry_id=TEST_ENTRY_ID, - entry=TEST_ENTRY, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_entry_group(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.create_entry_group( - location=TEST_LOCATION, - entry_group_id=TEST_ENTRY_GROUP_ID, - entry_group=TEST_ENTRY_GROUP, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.create_entry_group.assert_called_once_with( - request=dict( - parent=TEST_LOCATION_PATH.format(TEST_PROJECT_ID_1), - entry_group_id=TEST_ENTRY_GROUP_ID, - entry_group=TEST_ENTRY_GROUP, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_tag(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.create_tag( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=deepcopy(TEST_TAG), - template_id=TEST_TAG_TEMPLATE_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.create_tag.assert_called_once_with( - request=CreateTagRequest( - parent=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_1), - tag=Tag(template=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_1)), - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_tag_protobuff(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.create_tag( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=Tag(), - template_id=TEST_TAG_TEMPLATE_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.create_tag.assert_called_once_with( - request=CreateTagRequest( - parent=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_1), - tag=Tag(template=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_1)), - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_tag_template(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.create_tag_template( - location=TEST_LOCATION, - tag_template_id=TEST_TAG_TEMPLATE_ID, - tag_template=TEST_TAG_TEMPLATE, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.create_tag_template.assert_called_once_with( - request=CreateTagTemplateRequest( - parent=TEST_LOCATION_PATH.format(TEST_PROJECT_ID_1), - tag_template_id=TEST_TAG_TEMPLATE_ID, - tag_template=TEST_TAG_TEMPLATE, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_tag_template_field(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.create_tag_template_field( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.create_tag_template_field.assert_called_once_with( - request=dict( - parent=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_1), - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_entry(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.delete_entry( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.delete_entry.assert_called_once_with( - request=dict( - name=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_1), - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_entry_group(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.delete_entry_group( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.delete_entry_group.assert_called_once_with( - request=dict( - name=TEST_ENTRY_GROUP_PATH.format(TEST_PROJECT_ID_1), - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_tag(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.delete_tag( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=TEST_TAG_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.delete_tag.assert_called_once_with( - request=dict( - name=TEST_TAG_PATH.format(TEST_PROJECT_ID_1), - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_tag_template(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.delete_tag_template( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - force=TEST_FORCE, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.delete_tag_template.assert_called_once_with( - request=dict(name=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_1), force=TEST_FORCE), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_tag_template_field(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.delete_tag_template_field( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - field=TEST_TAG_TEMPLATE_FIELD_ID, - force=TEST_FORCE, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.delete_tag_template_field.assert_called_once_with( - request=dict( - name=TEST_TAG_TEMPLATE_FIELD_PATH.format(TEST_PROJECT_ID_1), - force=TEST_FORCE, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_get_entry(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.get_entry( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.get_entry.assert_called_once_with( - request=dict( - name=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_1), - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_get_entry_group(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.get_entry_group( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - read_mask=TEST_READ_MASK, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.get_entry_group.assert_called_once_with( - request=dict( - name=TEST_ENTRY_GROUP_PATH.format(TEST_PROJECT_ID_1), - read_mask=TEST_READ_MASK, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_get_tag_template(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.get_tag_template( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.get_tag_template.assert_called_once_with( - request=dict( - name=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_1), - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_list_tags(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.list_tags( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - page_size=TEST_PAGE_SIZE, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.list_tags.assert_called_once_with( - request=dict( - parent=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_1), - page_size=TEST_PAGE_SIZE, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_get_tag_for_template_name(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - tag_1 = mock.MagicMock(template=TEST_TAG_TEMPLATE_PATH.format("invalid-project")) - tag_2 = mock.MagicMock(template=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_1)) - - mock_get_conn.return_value.list_tags.return_value = [tag_1, tag_2] - result = self.hook.get_tag_for_template_name( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - template_name=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_1), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.list_tags.assert_called_once_with( - request=dict( - parent=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_1), - page_size=100, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - assert result == tag_2 - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_rename_tag_template_field(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.rename_tag_template_field( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - field=TEST_TAG_TEMPLATE_FIELD_ID, - new_tag_template_field_id=TEST_NEW_TAG_TEMPLATE_FIELD_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.rename_tag_template_field.assert_called_once_with( - request=dict( - name=TEST_TAG_TEMPLATE_FIELD_PATH.format(TEST_PROJECT_ID_1), - new_tag_template_field_id=TEST_NEW_TAG_TEMPLATE_FIELD_ID, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_update_entry(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.update_entry( - entry=TEST_ENTRY, - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.update_entry.assert_called_once_with( - request=dict( - entry=Entry(name=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_1)), - update_mask=TEST_UPDATE_MASK, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_update_tag(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.update_tag( - tag=deepcopy(TEST_TAG), - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag_id=TEST_TAG_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.update_tag.assert_called_once_with( - request=dict(tag=Tag(name=TEST_TAG_PATH.format(TEST_PROJECT_ID_1)), update_mask=TEST_UPDATE_MASK), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_update_tag_template(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.update_tag_template( - tag_template=TEST_TAG_TEMPLATE, - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - tag_template_id=TEST_TAG_TEMPLATE_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.update_tag_template.assert_called_once_with( - request=dict( - tag_template=TagTemplate(name=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_1)), - update_mask=TEST_UPDATE_MASK, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, TEST_PROJECT_ID_1), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_update_tag_template_field(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.update_tag_template_field( - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - update_mask=TEST_UPDATE_MASK, - tag_template=TEST_TAG_TEMPLATE_ID, - location=TEST_LOCATION, - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.update_tag_template_field.assert_called_once_with( - request=dict( - name=TEST_TAG_TEMPLATE_FIELD_PATH.format(TEST_PROJECT_ID_1), - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - update_mask=TEST_UPDATE_MASK, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogWithoutDefaultProjectIdHook: - def setup_method(self): - with pytest.warns(AirflowProviderDeprecationWarning): - with mock.patch( - "airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.__init__", - new=mock_base_gcp_hook_no_default_project_id, - ): - self.hook = CloudDataCatalogHook(gcp_conn_id="test") - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_entry(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.create_entry( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - entry=TEST_ENTRY, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.create_entry.assert_called_once_with( - request=dict( - parent=TEST_ENTRY_GROUP_PATH.format(TEST_PROJECT_ID_2), - entry_id=TEST_ENTRY_ID, - entry=TEST_ENTRY, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_entry_group(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.create_entry_group( - location=TEST_LOCATION, - entry_group_id=TEST_ENTRY_GROUP_ID, - entry_group=TEST_ENTRY_GROUP, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.create_entry_group.assert_called_once_with( - request=dict( - parent=TEST_LOCATION_PATH.format(TEST_PROJECT_ID_2), - entry_group_id=TEST_ENTRY_GROUP_ID, - entry_group=TEST_ENTRY_GROUP, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_tag(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.create_tag( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=deepcopy(TEST_TAG), - template_id=TEST_TAG_TEMPLATE_ID, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.create_tag.assert_called_once_with( - request=CreateTagRequest( - parent=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_2), - tag=Tag(template=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_2)), - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_tag_protobuff(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.create_tag( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=Tag(), - template_id=TEST_TAG_TEMPLATE_ID, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.create_tag.assert_called_once_with( - request=CreateTagRequest( - parent=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_2), - tag=Tag(template=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_2)), - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_tag_template(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.create_tag_template( - location=TEST_LOCATION, - tag_template_id=TEST_TAG_TEMPLATE_ID, - tag_template=TEST_TAG_TEMPLATE, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.create_tag_template.assert_called_once_with( - request=CreateTagTemplateRequest( - parent=TEST_LOCATION_PATH.format(TEST_PROJECT_ID_2), - tag_template_id=TEST_TAG_TEMPLATE_ID, - tag_template=TEST_TAG_TEMPLATE, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_tag_template_field(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.create_tag_template_field( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.create_tag_template_field.assert_called_once_with( - request=dict( - parent=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_2), - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_entry(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.delete_entry( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.delete_entry.assert_called_once_with( - request=dict(name=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_2)), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_entry_group(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.delete_entry_group( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.delete_entry_group.assert_called_once_with( - request=dict(name=TEST_ENTRY_GROUP_PATH.format(TEST_PROJECT_ID_2)), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_tag(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.delete_tag( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=TEST_TAG_ID, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.delete_tag.assert_called_once_with( - request=dict(name=TEST_TAG_PATH.format(TEST_PROJECT_ID_2)), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_tag_template(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.delete_tag_template( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - force=TEST_FORCE, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.delete_tag_template.assert_called_once_with( - request=dict(name=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_2), force=TEST_FORCE), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_tag_template_field(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.delete_tag_template_field( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - field=TEST_TAG_TEMPLATE_FIELD_ID, - force=TEST_FORCE, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.delete_tag_template_field.assert_called_once_with( - request=dict(name=TEST_TAG_TEMPLATE_FIELD_PATH.format(TEST_PROJECT_ID_2), force=TEST_FORCE), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_get_entry(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.get_entry( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.get_entry.assert_called_once_with( - request=dict(name=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_2)), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_get_entry_group(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.get_entry_group( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - read_mask=TEST_READ_MASK, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.get_entry_group.assert_called_once_with( - request=dict( - name=TEST_ENTRY_GROUP_PATH.format(TEST_PROJECT_ID_2), - read_mask=TEST_READ_MASK, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_get_tag_template(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.get_tag_template( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.get_tag_template.assert_called_once_with( - request=dict(name=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_2)), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_list_tags(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.list_tags( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - page_size=TEST_PAGE_SIZE, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.list_tags.assert_called_once_with( - request=dict(parent=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_2), page_size=TEST_PAGE_SIZE), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_get_tag_for_template_name(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - tag_1 = mock.MagicMock(template=TEST_TAG_TEMPLATE_PATH.format("invalid-project")) - tag_2 = mock.MagicMock(template=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_2)) - - mock_get_conn.return_value.list_tags.return_value = [tag_1, tag_2] - result = self.hook.get_tag_for_template_name( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - template_name=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_2), - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.list_tags.assert_called_once_with( - request=dict(parent=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_2), page_size=100), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - assert result == tag_2 - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_rename_tag_template_field(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.rename_tag_template_field( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - field=TEST_TAG_TEMPLATE_FIELD_ID, - new_tag_template_field_id=TEST_NEW_TAG_TEMPLATE_FIELD_ID, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.rename_tag_template_field.assert_called_once_with( - request=dict( - name=TEST_TAG_TEMPLATE_FIELD_PATH.format(TEST_PROJECT_ID_2), - new_tag_template_field_id=TEST_NEW_TAG_TEMPLATE_FIELD_ID, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_update_entry(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.update_entry( - entry=TEST_ENTRY, - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.update_entry.assert_called_once_with( - request=dict( - entry=Entry(name=TEST_ENTRY_PATH.format(TEST_PROJECT_ID_2)), update_mask=TEST_UPDATE_MASK - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_update_tag(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.update_tag( - tag=deepcopy(TEST_TAG), - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag_id=TEST_TAG_ID, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.update_tag.assert_called_once_with( - request=dict(tag=Tag(name=TEST_TAG_PATH.format(TEST_PROJECT_ID_2)), update_mask=TEST_UPDATE_MASK), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_update_tag_template(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.update_tag_template( - tag_template=TEST_TAG_TEMPLATE, - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - tag_template_id=TEST_TAG_TEMPLATE_ID, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.update_tag_template.assert_called_once_with( - request=dict( - tag_template=TagTemplate(name=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_2)), - update_mask=TEST_UPDATE_MASK, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_update_tag_template_field(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - self.hook.update_tag_template_field( - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - update_mask=TEST_UPDATE_MASK, - tag_template=TEST_TAG_TEMPLATE_ID, - location=TEST_LOCATION, - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - project_id=TEST_PROJECT_ID_2, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_get_conn.return_value.update_tag_template_field.assert_called_once_with( - request=dict( - name=TEST_TAG_TEMPLATE_FIELD_PATH.format(TEST_PROJECT_ID_2), - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - update_mask=TEST_UPDATE_MASK, - ), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -TEST_MESSAGE = re.escape( - "The project id must be passed either as keyword project_id parameter or as project_id extra in " - "Google Cloud connection definition. Both are not set!" -) - - -class TestCloudDataCatalogMissingProjectIdHook: - def setup_method(self): - with pytest.warns(AirflowProviderDeprecationWarning): - with mock.patch( - "airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.__init__", - new=mock_base_gcp_hook_no_default_project_id, - ): - self.hook = CloudDataCatalogHook(gcp_conn_id="test") - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_entry(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.create_entry( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - entry=TEST_ENTRY, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_entry_group(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.create_entry_group( - location=TEST_LOCATION, - entry_group_id=TEST_ENTRY_GROUP_ID, - entry_group=TEST_ENTRY_GROUP, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_tag(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.create_tag( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=deepcopy(TEST_TAG), - template_id=TEST_TAG_TEMPLATE_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_tag_protobuff(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.create_tag( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=Tag(), - template_id=TEST_TAG_TEMPLATE_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_tag_template(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.create_tag_template( - location=TEST_LOCATION, - tag_template_id=TEST_TAG_TEMPLATE_ID, - tag_template=TEST_TAG_TEMPLATE, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_create_tag_template_field(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.create_tag_template_field( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_entry(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.delete_entry( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_entry_group(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.delete_entry_group( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_tag(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.delete_tag( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=TEST_TAG_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_tag_template(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.delete_tag_template( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - force=TEST_FORCE, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_delete_tag_template_field(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.delete_tag_template_field( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - field=TEST_TAG_TEMPLATE_FIELD_ID, - force=TEST_FORCE, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_get_entry(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.get_entry( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_get_entry_group(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.get_entry_group( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - read_mask=TEST_READ_MASK, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_get_tag_template(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.get_tag_template( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_list_tags(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.list_tags( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - page_size=TEST_PAGE_SIZE, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_get_tag_for_template_name(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - tag_1 = mock.MagicMock(template=TEST_TAG_TEMPLATE_PATH.format("invalid-project")) - tag_2 = mock.MagicMock(template=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_2)) - - mock_get_conn.return_value.list_tags.return_value = [tag_1, tag_2] - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.get_tag_for_template_name( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - template_name=TEST_TAG_TEMPLATE_PATH.format(TEST_PROJECT_ID_2), - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_rename_tag_template_field(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.rename_tag_template_field( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - field=TEST_TAG_TEMPLATE_FIELD_ID, - new_tag_template_field_id=TEST_NEW_TAG_TEMPLATE_FIELD_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_update_entry(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.update_entry( - entry=TEST_ENTRY, - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_update_tag(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.update_tag( - tag=deepcopy(TEST_TAG), - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag_id=TEST_TAG_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_update_tag_template(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.update_tag_template( - tag_template=TEST_TAG_TEMPLATE, - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - tag_template_id=TEST_TAG_TEMPLATE_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - @mock.patch( - "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_credentials_and_project_id", - return_value=(TEST_CREDENTIALS, None), - ) - @mock.patch("airflow.providers.google.cloud.hooks.datacatalog.CloudDataCatalogHook.get_conn") - def test_update_tag_template_field(self, mock_get_conn, mock_get_creds_and_project_id) -> None: - with pytest.raises(AirflowException, match=TEST_MESSAGE): - self.hook.update_tag_template_field( - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - update_mask=TEST_UPDATE_MASK, - tag_template=TEST_TAG_TEMPLATE_ID, - location=TEST_LOCATION, - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) diff --git a/providers/google/tests/unit/google/cloud/hooks/vertex_ai/test_generative_model.py b/providers/google/tests/unit/google/cloud/hooks/vertex_ai/test_generative_model.py index 98217fedeaca9..3146992dba599 100644 --- a/providers/google/tests/unit/google/cloud/hooks/vertex_ai/test_generative_model.py +++ b/providers/google/tests/unit/google/cloud/hooks/vertex_ai/test_generative_model.py @@ -21,11 +21,8 @@ import pytest -from airflow.exceptions import AirflowProviderDeprecationWarning - # For no Pydantic environment, we need to skip the tests pytest.importorskip("google.cloud.aiplatform_v1") -from datetime import timedelta from vertexai.generative_models import HarmBlockThreshold, HarmCategory, Part, Tool, grounding from vertexai.preview.evaluation import MetricPromptTemplateExamples @@ -146,77 +143,6 @@ def setup_method(self): self.hook = GenerativeModelHook(gcp_conn_id=TEST_GCP_CONN_ID) self.hook.get_credentials = self.dummy_get_credentials - @mock.patch(GENERATIVE_MODEL_STRING.format("GenerativeModelHook.get_text_embedding_model")) - def test_text_embedding_model_get_embeddings(self, mock_model) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - self.hook.text_embedding_model_get_embeddings( - project_id=GCP_PROJECT, - location=GCP_LOCATION, - prompt=TEST_PROMPT, - pretrained_model=TEST_TEXT_EMBEDDING_MODEL, - ) - mock_model.assert_called_once_with(TEST_TEXT_EMBEDDING_MODEL) - mock_model.return_value.get_embeddings.assert_called_once_with([TEST_PROMPT]) - - @mock.patch(GENERATIVE_MODEL_STRING.format("GenerativeModelHook.get_generative_model")) - def test_generative_model_generate_content(self, mock_model) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - self.hook.generative_model_generate_content( - project_id=GCP_PROJECT, - contents=TEST_CONTENTS, - location=GCP_LOCATION, - tools=TEST_TOOLS, - generation_config=TEST_GENERATION_CONFIG, - safety_settings=TEST_SAFETY_SETTINGS, - pretrained_model=TEST_MULTIMODAL_PRETRAINED_MODEL, - ) - mock_model.assert_called_once_with( - pretrained_model=TEST_MULTIMODAL_PRETRAINED_MODEL, - system_instruction=None, - ) - mock_model.return_value.generate_content.assert_called_once_with( - contents=TEST_CONTENTS, - tools=TEST_TOOLS, - generation_config=TEST_GENERATION_CONFIG, - safety_settings=TEST_SAFETY_SETTINGS, - ) - - @mock.patch("vertexai.preview.tuning.sft.train") - def test_supervised_fine_tuning_train(self, mock_sft_train) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - self.hook.supervised_fine_tuning_train( - project_id=GCP_PROJECT, - location=GCP_LOCATION, - source_model=SOURCE_MODEL, - train_dataset=TRAIN_DATASET, - ) - - mock_sft_train.assert_called_once_with( - source_model=SOURCE_MODEL, - train_dataset=TRAIN_DATASET, - validation_dataset=None, - epochs=None, - adapter_size=None, - learning_rate_multiplier=None, - tuned_model_display_name=None, - ) - - @mock.patch(GENERATIVE_MODEL_STRING.format("GenerativeModelHook.get_generative_model")) - def test_count_tokens(self, mock_model) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - self.hook.count_tokens( - project_id=GCP_PROJECT, - contents=TEST_CONTENTS, - location=GCP_LOCATION, - pretrained_model=TEST_MULTIMODAL_PRETRAINED_MODEL, - ) - mock_model.assert_called_once_with( - pretrained_model=TEST_MULTIMODAL_PRETRAINED_MODEL, - ) - mock_model.return_value.count_tokens.assert_called_once_with( - contents=TEST_CONTENTS, - ) - @mock.patch(GENERATIVE_MODEL_STRING.format("GenerativeModelHook.get_generative_model")) @mock.patch(GENERATIVE_MODEL_STRING.format("GenerativeModelHook.get_eval_task")) def test_run_evaluation(self, mock_eval_task, mock_model) -> None: @@ -248,40 +174,3 @@ def test_run_evaluation(self, mock_eval_task, mock_model) -> None: prompt_template=TEST_PROMPT_TEMPLATE, experiment_run_name=TEST_EXPERIMENT_RUN_NAME, ) - - @mock.patch("vertexai.preview.caching.CachedContent.create") - def test_create_cached_content(self, mock_cached_content_create) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - self.hook.create_cached_content( - project_id=GCP_PROJECT, - location=GCP_LOCATION, - model_name=TEST_CACHED_MODEL, - system_instruction=TEST_CACHED_SYSTEM_INSTRUCTION, - contents=TEST_CACHED_CONTENTS, - ttl_hours=TEST_CACHED_TTL, - display_name=TEST_CACHED_DISPLAY_NAME, - ) - - mock_cached_content_create.assert_called_once_with( - model_name=TEST_CACHED_MODEL, - system_instruction=TEST_CACHED_SYSTEM_INSTRUCTION, - contents=TEST_CACHED_CONTENTS, - ttl=timedelta(hours=TEST_CACHED_TTL), - display_name=TEST_CACHED_DISPLAY_NAME, - ) - - @mock.patch(GENERATIVE_MODEL_STRING.format("GenerativeModelHook.get_cached_context_model")) - def test_generate_from_cached_content(self, mock_cached_context_model) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - self.hook.generate_from_cached_content( - project_id=GCP_PROJECT, - location=GCP_LOCATION, - cached_content_name=TEST_CACHED_CONTENT_NAME, - contents=TEST_CACHED_CONTENT_PROMPT, - ) - - mock_cached_context_model.return_value.generate_content.assert_called_once_with( - contents=TEST_CACHED_CONTENT_PROMPT, - generation_config=None, - safety_settings=None, - ) diff --git a/providers/google/tests/unit/google/cloud/operators/test_datacatalog.py b/providers/google/tests/unit/google/cloud/operators/test_datacatalog.py deleted file mode 100644 index 38e4f33d74d9b..0000000000000 --- a/providers/google/tests/unit/google/cloud/operators/test_datacatalog.py +++ /dev/null @@ -1,994 +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 collections.abc import Sequence -from typing import TYPE_CHECKING -from unittest import mock - -import pytest -from google.api_core.exceptions import AlreadyExists -from google.api_core.retry import Retry -from google.cloud.datacatalog import Entry, EntryGroup, Tag, TagTemplate, TagTemplateField -from google.protobuf.field_mask_pb2 import FieldMask - -from airflow.exceptions import AirflowProviderDeprecationWarning -from airflow.providers.google.cloud.operators.datacatalog import ( - CloudDataCatalogCreateEntryGroupOperator, - CloudDataCatalogCreateEntryOperator, - CloudDataCatalogCreateTagOperator, - CloudDataCatalogCreateTagTemplateFieldOperator, - CloudDataCatalogCreateTagTemplateOperator, - CloudDataCatalogDeleteEntryGroupOperator, - CloudDataCatalogDeleteEntryOperator, - CloudDataCatalogDeleteTagOperator, - CloudDataCatalogDeleteTagTemplateFieldOperator, - CloudDataCatalogDeleteTagTemplateOperator, - CloudDataCatalogGetEntryGroupOperator, - CloudDataCatalogGetEntryOperator, - CloudDataCatalogGetTagTemplateOperator, - CloudDataCatalogListTagsOperator, - CloudDataCatalogLookupEntryOperator, - CloudDataCatalogRenameTagTemplateFieldOperator, - CloudDataCatalogSearchCatalogOperator, - CloudDataCatalogUpdateEntryOperator, - CloudDataCatalogUpdateTagOperator, - CloudDataCatalogUpdateTagTemplateFieldOperator, - CloudDataCatalogUpdateTagTemplateOperator, -) - -from tests_common.test_utils.version_compat import AIRFLOW_V_3_0_PLUS - -if TYPE_CHECKING: - from google.api_core.gapic_v1.method import _MethodDefault - -BASE_PATH = "airflow.providers.google.cloud.operators.datacatalog.{}" -TEST_PROJECT_ID: str = "example_id" -TEST_LOCATION: str = "en-west-3" -TEST_ENTRY_ID: str = "test-entry-id" -TEST_TAG_ID: str = "test-tag-id" -TEST_RETRY: Retry | _MethodDefault = Retry() -TEST_TIMEOUT: float = 0.5 -TEST_METADATA: Sequence[tuple[str, str]] = [] -TEST_GCP_CONN_ID: str = "test-gcp-conn-id" -TEST_IMPERSONATION_CHAIN: Sequence[str] = ["ACCOUNT_1", "ACCOUNT_2", "ACCOUNT_3"] -TEST_ENTRY_GROUP_ID: str = "test-entry-group-id" -TEST_TAG_TEMPLATE_ID: str = "test-tag-template-id" -TEST_TAG_TEMPLATE_FIELD_ID: str = "test-tag-template-field-id" -TEST_TAG_TEMPLATE_NAME: str = "test-tag-template-field-name" -TEST_FORCE: bool = False -TEST_READ_MASK: FieldMask = FieldMask(paths=["name"]) -TEST_RESOURCE: str = "test-resource" -TEST_OPTIONS_: dict = {} -TEST_PAGE_SIZE: int = 50 -TEST_LINKED_RESOURCE: str = "test-linked-resource" -TEST_SQL_RESOURCE: str = "test-sql-resource" -TEST_NEW_TAG_TEMPLATE_FIELD_ID: str = "test-new-tag-template-field-id" -TEST_SCOPE: dict = dict(include_project_ids=["example-scope-project"]) -TEST_QUERY: str = "test-query" -TEST_ORDER_BY: str = "test-order-by" -TEST_UPDATE_MASK: dict = {"fields": ["name"]} -TEST_ENTRY_PATH: str = ( - f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}" - f"/entryGroups/{TEST_ENTRY_GROUP_ID}/entries/{TEST_ENTRY_ID}" -) -TEST_ENTRY_GROUP_PATH: str = ( - f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}/entryGroups/{TEST_ENTRY_GROUP_ID}" -) -TEST_TAG_TEMPLATE_PATH: str = ( - f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}/tagTemplates/{TEST_TAG_TEMPLATE_ID}" -) -TEST_TAG_PATH: str = ( - f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}/entryGroups/" - f"{TEST_ENTRY_GROUP_ID}/entries/{TEST_ENTRY_ID}/tags/{TEST_TAG_ID}" -) - -TEST_ENTRY: Entry = Entry(name=TEST_ENTRY_PATH) -TEST_ENTRY_DICT: dict = { - "description": "", - "display_name": "", - "linked_resource": "", - "fully_qualified_name": "", - "labels": {}, - "name": TEST_ENTRY_PATH, -} -TEST_ENTRY_GROUP: EntryGroup = EntryGroup(name=TEST_ENTRY_GROUP_PATH) -TEST_ENTRY_GROUP_DICT: dict = { - "description": "", - "display_name": "", - "name": TEST_ENTRY_GROUP_PATH, - "transferred_to_dataplex": False, -} -TEST_TAG: Tag = Tag(name=TEST_TAG_PATH) -TEST_TAG_DICT: dict = { - "fields": {}, - "name": TEST_TAG_PATH, - "template": "", - "template_display_name": "", - "dataplex_transfer_status": 0, -} -TEST_TAG_TEMPLATE: TagTemplate = TagTemplate(name=TEST_TAG_TEMPLATE_PATH) -TEST_TAG_TEMPLATE_DICT: dict = { - "dataplex_transfer_status": 0, - "display_name": "", - "fields": {}, - "is_publicly_readable": False, - "name": TEST_TAG_TEMPLATE_PATH, -} -TEST_TAG_TEMPLATE_FIELD: TagTemplateField = TagTemplateField(name=TEST_TAG_TEMPLATE_FIELD_ID) -TEST_TAG_TEMPLATE_FIELD_DICT: dict = { - "description": "", - "display_name": "", - "is_required": False, - "name": TEST_TAG_TEMPLATE_FIELD_ID, - "order": 0, -} -TEST_ENTRY_LINK = "projects/{project_id}/locations/{location}/entryGroups/{entry_group_id}/entries/{entry_id}" -TEST_TAG_TEMPLATE_LINK = "projects/{project_id}/locations/{location}/tagTemplates/{tag_template_id}" -TEST_TAG_TEMPLATE_FIELD_LINK = "projects/{project_id}/locations/{location}/tagTemplates/{tag_template_id}\ - /fields/{tag_template_field_id}" - - -class TestCloudDataCatalogCreateEntryOperator: - @mock.patch( - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook", - **{"return_value.create_entry.return_value": TEST_ENTRY}, - ) - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogCreateEntryOperator( - task_id="task_id", - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - entry=TEST_ENTRY, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_ti = mock.MagicMock() - mock_context = {"ti": mock_ti} - if not AIRFLOW_V_3_0_PLUS: - mock_context["task"] = task # type: ignore[assignment] - result = task.execute(context=mock_context) # type: ignore[arg-type] - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.create_entry.assert_called_once_with( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - entry=TEST_ENTRY, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_ti.xcom_push.assert_any_call( - key="entry_id", - value=TEST_ENTRY_ID, - ) - - assert result == TEST_ENTRY_DICT - - @mock.patch("airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook") - def test_assert_valid_hook_call_when_exists(self, mock_hook) -> None: - mock_hook.return_value.create_entry.side_effect = AlreadyExists(message="message") - mock_hook.return_value.get_entry.return_value = TEST_ENTRY - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogCreateEntryOperator( - task_id="task_id", - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - entry=TEST_ENTRY, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_ti = mock.MagicMock() - mock_context = {"ti": mock_ti} - if not AIRFLOW_V_3_0_PLUS: - mock_context["task"] = task # type: ignore[assignment] - result = task.execute(context=mock_context) # type: ignore[arg-type] - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.create_entry.assert_called_once_with( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - entry=TEST_ENTRY, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_hook.return_value.get_entry.assert_called_once_with( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_ti.xcom_push.assert_any_call( - key="entry_id", - value=TEST_ENTRY_ID, - ) - assert result == TEST_ENTRY_DICT - - -class TestCloudDataCatalogCreateEntryGroupOperator: - @mock.patch( - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook", - **{"return_value.create_entry_group.return_value": TEST_ENTRY_GROUP}, - ) - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogCreateEntryGroupOperator( - task_id="task_id", - location=TEST_LOCATION, - entry_group_id=TEST_ENTRY_GROUP_ID, - entry_group=TEST_ENTRY_GROUP, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_ti = mock.MagicMock() - mock_context = {"ti": mock_ti} - if not AIRFLOW_V_3_0_PLUS: - mock_context["task"] = task # type: ignore[assignment] - result = task.execute(context=mock_context) # type: ignore[arg-type] - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.create_entry_group.assert_called_once_with( - location=TEST_LOCATION, - entry_group_id=TEST_ENTRY_GROUP_ID, - entry_group=TEST_ENTRY_GROUP, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_ti.xcom_push.assert_any_call( - key="entry_group_id", - value=TEST_ENTRY_GROUP_ID, - ) - assert result == TEST_ENTRY_GROUP_DICT - - -class TestCloudDataCatalogCreateTagOperator: - @mock.patch( - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook", - **{"return_value.create_tag.return_value": TEST_TAG}, - ) - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogCreateTagOperator( - task_id="task_id", - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=TEST_TAG, - template_id=TEST_TAG_TEMPLATE_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_ti = mock.MagicMock() - mock_context = {"ti": mock_ti} - if not AIRFLOW_V_3_0_PLUS: - mock_context["task"] = task # type: ignore[assignment] - result = task.execute(context=mock_context) # type: ignore[arg-type] - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.create_tag.assert_called_once_with( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=TEST_TAG, - template_id=TEST_TAG_TEMPLATE_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_ti.xcom_push.assert_any_call( - key="tag_id", - value=TEST_TAG_ID, - ) - assert result == TEST_TAG_DICT - - -class TestCloudDataCatalogCreateTagTemplateOperator: - @mock.patch( - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook", - **{"return_value.create_tag_template.return_value": TEST_TAG_TEMPLATE}, - ) - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogCreateTagTemplateOperator( - task_id="task_id", - location=TEST_LOCATION, - tag_template_id=TEST_TAG_TEMPLATE_ID, - tag_template=TEST_TAG_TEMPLATE, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_ti = mock.MagicMock() - mock_context = {"ti": mock_ti} - if not AIRFLOW_V_3_0_PLUS: - mock_context["task"] = task # type: ignore[assignment] - result = task.execute(context=mock_context) # type: ignore[arg-type] - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.create_tag_template.assert_called_once_with( - location=TEST_LOCATION, - tag_template_id=TEST_TAG_TEMPLATE_ID, - tag_template=TEST_TAG_TEMPLATE, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_ti.xcom_push.assert_any_call( - key="tag_template_id", - value=TEST_TAG_TEMPLATE_ID, - ) - assert result == TEST_TAG_TEMPLATE_DICT - - -class TestCloudDataCatalogCreateTagTemplateFieldOperator: - @mock.patch( - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook", - **{"return_value.create_tag_template_field.return_value": TEST_TAG_TEMPLATE_FIELD}, - ) - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogCreateTagTemplateFieldOperator( - task_id="task_id", - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_ti = mock.MagicMock() - mock_context = {"ti": mock_ti} - if not AIRFLOW_V_3_0_PLUS: - mock_context["task"] = task # type: ignore[assignment] - result = task.execute(context=mock_context) # type: ignore[arg-type] - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.create_tag_template_field.assert_called_once_with( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - mock_ti.xcom_push.assert_any_call( - key="tag_template_field_id", - value=TEST_TAG_TEMPLATE_FIELD_ID, - ) - assert result == TEST_TAG_TEMPLATE_FIELD_DICT - - -class TestCloudDataCatalogDeleteEntryOperator: - @mock.patch("airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook") - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogDeleteEntryOperator( - task_id="task_id", - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.delete_entry.assert_called_once_with( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogDeleteEntryGroupOperator: - @mock.patch("airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook") - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogDeleteEntryGroupOperator( - task_id="task_id", - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.delete_entry_group.assert_called_once_with( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogDeleteTagOperator: - @mock.patch("airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook") - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogDeleteTagOperator( - task_id="task_id", - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=TEST_TAG_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.delete_tag.assert_called_once_with( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag=TEST_TAG_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogDeleteTagTemplateOperator: - @mock.patch("airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook") - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogDeleteTagTemplateOperator( - task_id="task_id", - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - force=TEST_FORCE, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.delete_tag_template.assert_called_once_with( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - force=TEST_FORCE, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogDeleteTagTemplateFieldOperator: - @mock.patch("airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook") - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogDeleteTagTemplateFieldOperator( - task_id="task_id", - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - field=TEST_TAG_TEMPLATE_FIELD_ID, - force=TEST_FORCE, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.delete_tag_template_field.assert_called_once_with( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - field=TEST_TAG_TEMPLATE_FIELD_ID, - force=TEST_FORCE, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogGetEntryOperator: - @mock.patch( - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook", - **{"return_value.get_entry.return_value": TEST_ENTRY}, - ) - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogGetEntryOperator( - task_id="task_id", - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.get_entry.assert_called_once_with( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogGetEntryGroupOperator: - @mock.patch( - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook", - **{"return_value.get_entry_group.return_value": TEST_ENTRY_GROUP}, - ) - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogGetEntryGroupOperator( - task_id="task_id", - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - read_mask=TEST_READ_MASK, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.get_entry_group.assert_called_once_with( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - read_mask=TEST_READ_MASK, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogGetTagTemplateOperator: - @mock.patch( - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook", - **{"return_value.get_tag_template.return_value": TEST_TAG_TEMPLATE}, - ) - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogGetTagTemplateOperator( - task_id="task_id", - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.get_tag_template.assert_called_once_with( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogListTagsOperator: - @mock.patch( - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook", - return_value=mock.MagicMock(list_tags=mock.MagicMock(return_value=[TEST_TAG])), - ) - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogListTagsOperator( - task_id="task_id", - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - page_size=TEST_PAGE_SIZE, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.list_tags.assert_called_once_with( - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - page_size=TEST_PAGE_SIZE, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogLookupEntryOperator: - @mock.patch( - "airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook", - **{"return_value.lookup_entry.return_value": TEST_ENTRY}, - ) - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogLookupEntryOperator( - task_id="task_id", - linked_resource=TEST_LINKED_RESOURCE, - sql_resource=TEST_SQL_RESOURCE, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.lookup_entry.assert_called_once_with( - linked_resource=TEST_LINKED_RESOURCE, - sql_resource=TEST_SQL_RESOURCE, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogRenameTagTemplateFieldOperator: - @mock.patch("airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook") - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogRenameTagTemplateFieldOperator( - task_id="task_id", - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - field=TEST_TAG_TEMPLATE_FIELD_ID, - new_tag_template_field_id=TEST_NEW_TAG_TEMPLATE_FIELD_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.rename_tag_template_field.assert_called_once_with( - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - field=TEST_TAG_TEMPLATE_FIELD_ID, - new_tag_template_field_id=TEST_NEW_TAG_TEMPLATE_FIELD_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogSearchCatalogOperator: - @mock.patch("airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook") - def test_assert_valid_hook_call(self, mock_hook) -> None: - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogSearchCatalogOperator( - task_id="task_id", - scope=TEST_SCOPE, - query=TEST_QUERY, - page_size=TEST_PAGE_SIZE, - order_by=TEST_ORDER_BY, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.search_catalog.assert_called_once_with( - scope=TEST_SCOPE, - query=TEST_QUERY, - page_size=TEST_PAGE_SIZE, - order_by=TEST_ORDER_BY, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogUpdateEntryOperator: - @mock.patch("airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook") - def test_assert_valid_hook_call(self, mock_hook) -> None: - mock_hook.return_value.update_entry.return_value.name = TEST_ENTRY_LINK.format( - project_id=TEST_PROJECT_ID, - location=TEST_LOCATION, - entry_group_id=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - ) - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogUpdateEntryOperator( - task_id="task_id", - entry=TEST_ENTRY, - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.update_entry.assert_called_once_with( - entry=TEST_ENTRY, - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogUpdateTagOperator: - @mock.patch("airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook") - def test_assert_valid_hook_call(self, mock_hook) -> None: - mock_hook.return_value.update_tag.return_value.name = TEST_ENTRY_LINK.format( - project_id=TEST_PROJECT_ID, - location=TEST_LOCATION, - entry_group_id=TEST_ENTRY_GROUP_ID, - entry_id=TEST_ENTRY_ID, - ) - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogUpdateTagOperator( - task_id="task_id", - tag=Tag(name=TEST_TAG_ID), - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag_id=TEST_TAG_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.update_tag.assert_called_once_with( - tag=Tag(name=TEST_TAG_ID), - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - entry_group=TEST_ENTRY_GROUP_ID, - entry=TEST_ENTRY_ID, - tag_id=TEST_TAG_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogUpdateTagTemplateOperator: - @mock.patch("airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook") - def test_assert_valid_hook_call(self, mock_hook) -> None: - mock_hook.return_value.update_tag_template.return_value.name = TEST_TAG_TEMPLATE_LINK.format( - project_id=TEST_PROJECT_ID, - location=TEST_LOCATION, - tag_template_id=TEST_TAG_TEMPLATE_ID, - ) - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogUpdateTagTemplateOperator( - task_id="task_id", - tag_template=TagTemplate(name=TEST_TAG_TEMPLATE_ID), - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - tag_template_id=TEST_TAG_TEMPLATE_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.update_tag_template.assert_called_once_with( - tag_template=TagTemplate(name=TEST_TAG_TEMPLATE_ID), - update_mask=TEST_UPDATE_MASK, - location=TEST_LOCATION, - tag_template_id=TEST_TAG_TEMPLATE_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) - - -class TestCloudDataCatalogUpdateTagTemplateFieldOperator: - @mock.patch("airflow.providers.google.cloud.operators.datacatalog.CloudDataCatalogHook") - def test_assert_valid_hook_call(self, mock_hook) -> None: - mock_hook.return_value.update_tag_template_field.return_value.name = ( - TEST_TAG_TEMPLATE_FIELD_LINK.format( - project_id=TEST_PROJECT_ID, - location=TEST_LOCATION, - tag_template_id=TEST_TAG_TEMPLATE_ID, - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - ) - ) - with pytest.warns(AirflowProviderDeprecationWarning): - task = CloudDataCatalogUpdateTagTemplateFieldOperator( - task_id="task_id", - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - update_mask=TEST_UPDATE_MASK, - tag_template_field_name=TEST_TAG_TEMPLATE_NAME, - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - task.execute(context=mock.MagicMock()) - mock_hook.assert_called_once_with( - gcp_conn_id=TEST_GCP_CONN_ID, - impersonation_chain=TEST_IMPERSONATION_CHAIN, - ) - mock_hook.return_value.update_tag_template_field.assert_called_once_with( - tag_template_field=TEST_TAG_TEMPLATE_FIELD, - update_mask=TEST_UPDATE_MASK, - tag_template_field_name=TEST_TAG_TEMPLATE_NAME, - location=TEST_LOCATION, - tag_template=TEST_TAG_TEMPLATE_ID, - tag_template_field_id=TEST_TAG_TEMPLATE_FIELD_ID, - project_id=TEST_PROJECT_ID, - retry=TEST_RETRY, - timeout=TEST_TIMEOUT, - metadata=TEST_METADATA, - ) diff --git a/providers/google/tests/unit/google/cloud/operators/vertex_ai/test_generative_model.py b/providers/google/tests/unit/google/cloud/operators/vertex_ai/test_generative_model.py index 86a6be37bf82c..e9bd014eac794 100644 --- a/providers/google/tests/unit/google/cloud/operators/vertex_ai/test_generative_model.py +++ b/providers/google/tests/unit/google/cloud/operators/vertex_ai/test_generative_model.py @@ -24,19 +24,11 @@ pytest.importorskip("google.cloud.aiplatform_v1") pytest.importorskip("google.cloud.aiplatform_v1beta1") vertexai = pytest.importorskip("vertexai.generative_models") -from vertexai.generative_models import HarmBlockThreshold, HarmCategory, Part, Tool, grounding +from vertexai.generative_models import HarmBlockThreshold, HarmCategory, Tool, grounding from vertexai.preview.evaluation import MetricPromptTemplateExamples -from airflow.exceptions import AirflowProviderDeprecationWarning from airflow.providers.google.cloud.operators.vertex_ai.generative_model import ( - CountTokensOperator, - CreateCachedContentOperator, - DeleteExperimentRunOperator, - GenerateFromCachedContentOperator, - GenerativeModelGenerateContentOperator, RunEvaluationOperator, - SupervisedFineTuningTrainOperator, - TextEmbeddingModelGetEmbeddingsOperator, ) VERTEX_AI_PATH = "airflow.providers.google.cloud.operators.vertex_ai.{}" @@ -52,146 +44,6 @@ def assert_warning(msg: str, warnings): assert any(msg in str(w) for w in warnings) -class TestVertexAITextEmbeddingModelGetEmbeddingsOperator: - @mock.patch(VERTEX_AI_PATH.format("generative_model.GenerativeModelHook")) - def test_execute(self, mock_hook): - prompt = "In 10 words or less, what is Apache Airflow?" - pretrained_model = "textembedding-gecko" - with pytest.warns(AirflowProviderDeprecationWarning): - op = TextEmbeddingModelGetEmbeddingsOperator( - task_id=TASK_ID, - project_id=GCP_PROJECT, - location=GCP_LOCATION, - prompt=prompt, - pretrained_model=pretrained_model, - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - op.execute(context={"ti": mock.MagicMock()}) - mock_hook.assert_called_once_with( - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - mock_hook.return_value.text_embedding_model_get_embeddings.assert_called_once_with( - project_id=GCP_PROJECT, - location=GCP_LOCATION, - prompt=prompt, - pretrained_model=pretrained_model, - ) - - -class TestVertexAIGenerativeModelGenerateContentOperator: - @mock.patch(VERTEX_AI_PATH.format("generative_model.GenerativeModelHook")) - def test_execute(self, mock_hook): - contents = ["In 10 words or less, what is Apache Airflow?"] - tools = [Tool.from_google_search_retrieval(grounding.GoogleSearchRetrieval())] - pretrained_model = "gemini-pro" - safety_settings = { - HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_ONLY_HIGH, - HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_ONLY_HIGH, - HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_ONLY_HIGH, - HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_ONLY_HIGH, - } - generation_config = {"max_output_tokens": 256, "top_p": 0.8, "temperature": 0.0} - system_instruction = "be concise." - with pytest.warns(AirflowProviderDeprecationWarning): - op = GenerativeModelGenerateContentOperator( - task_id=TASK_ID, - project_id=GCP_PROJECT, - location=GCP_LOCATION, - contents=contents, - tools=tools, - generation_config=generation_config, - safety_settings=safety_settings, - pretrained_model=pretrained_model, - system_instruction=system_instruction, - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - op.execute(context={"ti": mock.MagicMock()}) - mock_hook.assert_called_once_with( - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - mock_hook.return_value.generative_model_generate_content.assert_called_once_with( - project_id=GCP_PROJECT, - location=GCP_LOCATION, - contents=contents, - tools=tools, - generation_config=generation_config, - safety_settings=safety_settings, - pretrained_model=pretrained_model, - system_instruction=system_instruction, - ) - - -class TestVertexAISupervisedFineTuningTrainOperator: - @mock.patch(VERTEX_AI_PATH.format("generative_model.GenerativeModelHook")) - @mock.patch("google.cloud.aiplatform_v1.types.TuningJob.to_dict") - def test_execute( - self, - to_dict_mock, - mock_hook, - ): - source_model = "gemini-1.0-pro-002" - train_dataset = "gs://cloud-samples-data/ai-platform/generative_ai/sft_train_data.jsonl" - with pytest.warns(AirflowProviderDeprecationWarning): - op = SupervisedFineTuningTrainOperator( - task_id=TASK_ID, - project_id=GCP_PROJECT, - location=GCP_LOCATION, - source_model=source_model, - train_dataset=train_dataset, - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - op.execute(context={"ti": mock.MagicMock()}) - mock_hook.assert_called_once_with( - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - mock_hook.return_value.supervised_fine_tuning_train.assert_called_once_with( - project_id=GCP_PROJECT, - location=GCP_LOCATION, - source_model=source_model, - train_dataset=train_dataset, - adapter_size=None, - epochs=None, - learning_rate_multiplier=None, - tuned_model_display_name=None, - validation_dataset=None, - ) - - -class TestVertexAICountTokensOperator: - @mock.patch(VERTEX_AI_PATH.format("generative_model.GenerativeModelHook")) - @mock.patch("google.cloud.aiplatform_v1beta1.types.CountTokensResponse.to_dict") - def test_execute(self, to_dict_mock, mock_hook): - contents = ["In 10 words or less, what is Apache Airflow?"] - pretrained_model = "gemini-pro" - with pytest.warns(AirflowProviderDeprecationWarning): - op = CountTokensOperator( - task_id=TASK_ID, - project_id=GCP_PROJECT, - location=GCP_LOCATION, - contents=contents, - pretrained_model=pretrained_model, - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - op.execute(context={"ti": mock.MagicMock()}) - mock_hook.assert_called_once_with( - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - mock_hook.return_value.count_tokens.assert_called_once_with( - project_id=GCP_PROJECT, - location=GCP_LOCATION, - contents=contents, - pretrained_model=pretrained_model, - ) - - class TestVertexAIRunEvaluationOperator: @mock.patch(VERTEX_AI_PATH.format("generative_model.GenerativeModelHook")) def test_execute( @@ -277,112 +129,3 @@ def test_execute( safety_settings=safety_settings, tools=tools, ) - - -class TestVertexAICreateCachedContentOperator: - @mock.patch(VERTEX_AI_PATH.format("generative_model.GenerativeModelHook")) - def test_execute(self, mock_hook): - model_name = "gemini-1.5-pro-002" - system_instruction = """ - You are an expert researcher. You always stick to the facts in the sources provided, and never make up new facts. - Now look at these research papers, and answer the following questions. - """ - - contents = [ - Part.from_uri( - "gs://cloud-samples-data/generative-ai/pdf/2312.11805v3.pdf", - mime_type="application/pdf", - ), - Part.from_uri( - "gs://cloud-samples-data/generative-ai/pdf/2403.05530.pdf", - mime_type="application/pdf", - ), - ] - ttl_hours = 1 - display_name = "test-example-cache" - with pytest.warns(AirflowProviderDeprecationWarning): - op = CreateCachedContentOperator( - task_id=TASK_ID, - project_id=GCP_PROJECT, - location=GCP_LOCATION, - model_name=model_name, - system_instruction=system_instruction, - contents=contents, - ttl_hours=ttl_hours, - display_name=display_name, - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - op.execute(context={"ti": mock.MagicMock()}) - mock_hook.assert_called_once_with( - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - mock_hook.return_value.create_cached_content.assert_called_once_with( - project_id=GCP_PROJECT, - location=GCP_LOCATION, - model_name=model_name, - system_instruction=system_instruction, - contents=contents, - ttl_hours=ttl_hours, - display_name=display_name, - ) - - -class TestVertexAIGenerateFromCachedContentOperator: - @mock.patch(VERTEX_AI_PATH.format("generative_model.GenerativeModelHook")) - def test_execute(self, mock_hook): - cached_content_name = "test" - contents = ["what are in these papers"] - with pytest.warns(AirflowProviderDeprecationWarning): - op = GenerateFromCachedContentOperator( - task_id=TASK_ID, - project_id=GCP_PROJECT, - location=GCP_LOCATION, - cached_content_name=cached_content_name, - contents=contents, - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - op.execute(context={"ti": mock.MagicMock()}) - mock_hook.assert_called_once_with( - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - mock_hook.return_value.generate_from_cached_content.assert_called_once_with( - project_id=GCP_PROJECT, - location=GCP_LOCATION, - cached_content_name=cached_content_name, - contents=contents, - generation_config=None, - safety_settings=None, - ) - - -class TestVertexAIDeleteExperimentRunOperator: - @mock.patch(VERTEX_AI_PATH.format("generative_model.ExperimentRunHook")) - def test_execute(self, mock_hook): - test_experiment_name = "test_experiment_name" - test_experiment_run_name = "test_experiment_run_name" - - with pytest.warns(AirflowProviderDeprecationWarning): - op = DeleteExperimentRunOperator( - task_id=TASK_ID, - project_id=GCP_PROJECT, - location=GCP_LOCATION, - experiment_name=test_experiment_name, - experiment_run_name=test_experiment_run_name, - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - op.execute(context={"ti": mock.MagicMock()}) - mock_hook.assert_called_once_with( - gcp_conn_id=GCP_CONN_ID, - impersonation_chain=IMPERSONATION_CHAIN, - ) - mock_hook.return_value.delete_experiment_run.assert_called_once_with( - project_id=GCP_PROJECT, - location=GCP_LOCATION, - experiment_name=test_experiment_name, - experiment_run_name=test_experiment_run_name, - ) diff --git a/scripts/in_container/run_provider_yaml_files_check.py b/scripts/in_container/run_provider_yaml_files_check.py index bf6e104db6a28..17fbf527c6123 100755 --- a/scripts/in_container/run_provider_yaml_files_check.py +++ b/scripts/in_container/run_provider_yaml_files_check.py @@ -60,9 +60,6 @@ "airflow.providers.tabular.hooks.tabular", "airflow.providers.yandex.hooks.yandexcloud_dataproc", "airflow.providers.yandex.operators.yandexcloud_dataproc", - "airflow.providers.google.cloud.hooks.datacatalog", - "airflow.providers.google.cloud.operators.datacatalog", - "airflow.providers.google.cloud.links.datacatalog", ] KNOWN_DEPRECATED_CLASSES = [ From 0d5137a3660f9b9518a1f0fde0765473fe71a2ff Mon Sep 17 00:00:00 2001 From: Jake Roach <116606359+jroachgolf84@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:37:17 -0400 Subject: [PATCH 056/280] issue-57891: Adding sftp_remote_host to S3 transfer Operators (#63147) --- .../amazon/aws/transfers/s3_to_sftp.py | 8 ++- .../amazon/aws/transfers/sftp_to_s3.py | 8 ++- .../amazon/aws/transfers/test_s3_to_sftp.py | 55 +++++++++++++++++++ .../amazon/aws/transfers/test_sftp_to_s3.py | 51 +++++++++++++++++ 4 files changed, 120 insertions(+), 2 deletions(-) diff --git a/providers/amazon/src/airflow/providers/amazon/aws/transfers/s3_to_sftp.py b/providers/amazon/src/airflow/providers/amazon/aws/transfers/s3_to_sftp.py index 0b3b5e1cb8417..87d8454af963b 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/transfers/s3_to_sftp.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/transfers/s3_to_sftp.py @@ -42,6 +42,8 @@ class S3ToSFTPOperator(BaseOperator): establishing a connection to the SFTP server. :param sftp_path: The sftp remote path. This is the specified file path for uploading file to the SFTP server. + :param sftp_remote_host: The remote host of the SFTP server. Overrides host in + Connection. :param aws_conn_id: The Airflow connection used for AWS credentials. If this is None or empty then the default boto3 behaviour is used. If running Airflow in a distributed manner and aws_conn_id is None or @@ -65,6 +67,7 @@ def __init__( s3_key: str, sftp_path: str, sftp_conn_id: str = "ssh_default", + sftp_remote_host: str = "", aws_conn_id: str | None = "aws_default", confirm: bool = True, **kwargs, @@ -74,6 +77,7 @@ def __init__( self.sftp_path = sftp_path self.s3_bucket = s3_bucket self.s3_key = s3_key + self.sftp_remote_host = sftp_remote_host self.aws_conn_id = aws_conn_id self.confirm = confirm @@ -85,7 +89,9 @@ def get_s3_key(s3_key: str) -> str: def execute(self, context: Context) -> None: self.s3_key = self.get_s3_key(self.s3_key) - ssh_hook = SSHHook(ssh_conn_id=self.sftp_conn_id) + + # SSHHook will handle a None/"" sftp_remote_host + ssh_hook = SSHHook(ssh_conn_id=self.sftp_conn_id, remote_host=self.sftp_remote_host) s3_hook = S3Hook(self.aws_conn_id) s3_client = s3_hook.get_conn() diff --git a/providers/amazon/src/airflow/providers/amazon/aws/transfers/sftp_to_s3.py b/providers/amazon/src/airflow/providers/amazon/aws/transfers/sftp_to_s3.py index 0b3aada19501d..4897ccca25c91 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/transfers/sftp_to_s3.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/transfers/sftp_to_s3.py @@ -40,6 +40,8 @@ class SFTPToS3Operator(BaseOperator): :param sftp_conn_id: The sftp connection id. The name or identifier for establishing a connection to the SFTP server. + :param sftp_remote_host: The remote host of the SFTP server. Overrides host in + Connection. :param sftp_path: The sftp remote path. This is the specified file path for downloading the file from the SFTP server. :param s3_conn_id: The s3 connection id. The name or identifier for @@ -63,6 +65,7 @@ def __init__( s3_key: str, sftp_path: str, sftp_conn_id: str = "ssh_default", + sftp_remote_host: str = "", s3_conn_id: str = "aws_default", use_temp_file: bool = True, fail_on_file_not_exist: bool = True, @@ -71,6 +74,7 @@ def __init__( super().__init__(**kwargs) self.sftp_conn_id = sftp_conn_id self.sftp_path = sftp_path + self.sftp_remote_host = sftp_remote_host self.s3_bucket = s3_bucket self.s3_key = s3_key self.s3_conn_id = s3_conn_id @@ -85,7 +89,9 @@ def get_s3_key(s3_key: str) -> str: def execute(self, context: Context) -> None: self.s3_key = self.get_s3_key(self.s3_key) - ssh_hook = SSHHook(ssh_conn_id=self.sftp_conn_id) + + # SSHHook will handle a None/"" sftp_remote_host + ssh_hook = SSHHook(ssh_conn_id=self.sftp_conn_id, remote_host=self.sftp_remote_host) s3_hook = S3Hook(self.s3_conn_id) sftp_client = ssh_hook.get_conn().open_sftp() diff --git a/providers/amazon/tests/unit/amazon/aws/transfers/test_s3_to_sftp.py b/providers/amazon/tests/unit/amazon/aws/transfers/test_s3_to_sftp.py index fecf207c6f429..257b898922ccd 100644 --- a/providers/amazon/tests/unit/amazon/aws/transfers/test_s3_to_sftp.py +++ b/providers/amazon/tests/unit/amazon/aws/transfers/test_s3_to_sftp.py @@ -256,5 +256,60 @@ def test_s3_to_sftp_operation_confirm_false(self): conn.delete_bucket(Bucket=self.s3_bucket) assert not s3_hook.check_for_bucket(self.s3_bucket) + @mock_aws + @conf_vars({("core", "enable_xcom_pickling"): "True"}) + def test_s3_to_sftp_operator_sftp_remote_host(self): + """Test that sftp_remote_host overrides the connection host when provided.""" + s3_hook = S3Hook(aws_conn_id=None) + test_remote_file_content = ( + "This is remote file content for sftp_remote_host test \n which is also multiline " + "another line here \n this is last line. EOF" + ) + + # Test for creation of s3 bucket + conn = boto3.client("s3") + conn.create_bucket(Bucket=self.s3_bucket) + assert s3_hook.check_for_bucket(self.s3_bucket) + + with open(LOCAL_FILE_PATH, "w") as file: + file.write(test_remote_file_content) + s3_hook.load_file(LOCAL_FILE_PATH, self.s3_key, bucket_name=BUCKET) + + # Check if object was created in s3 + objects_in_dest_bucket = conn.list_objects(Bucket=self.s3_bucket, Prefix=self.s3_key) + assert len(objects_in_dest_bucket["Contents"]) == 1 + assert objects_in_dest_bucket["Contents"][0]["Key"] == self.s3_key + + # Execute with sftp_remote_host overriding the connection host to the same localhost + run_task = S3ToSFTPOperator( + s3_bucket=BUCKET, + s3_key=S3_KEY, + sftp_path=SFTP_PATH, + sftp_conn_id=SFTP_CONN_ID, + sftp_remote_host="localhost", + task_id=TASK_ID + "_remote_host", + dag=self.dag, + ) + assert run_task is not None + + run_task.execute(None) + + # Check that the file is created remotely with correct content + check_file_task = SSHOperator( + task_id="test_check_file_remote_host", + ssh_hook=self.hook, + command=f"cat {self.sftp_path}", + do_xcom_push=True, + dag=self.dag, + ) + assert check_file_task is not None + result = check_file_task.execute(None) + assert result.strip() == test_remote_file_content.encode("utf-8") + + # Clean up after finishing with test + conn.delete_object(Bucket=self.s3_bucket, Key=self.s3_key) + conn.delete_bucket(Bucket=self.s3_bucket) + assert not s3_hook.check_for_bucket(self.s3_bucket) + def teardown_method(self): self.delete_remote_resource() diff --git a/providers/amazon/tests/unit/amazon/aws/transfers/test_sftp_to_s3.py b/providers/amazon/tests/unit/amazon/aws/transfers/test_sftp_to_s3.py index e8fd3be4905da..feb85e33a3c17 100644 --- a/providers/amazon/tests/unit/amazon/aws/transfers/test_sftp_to_s3.py +++ b/providers/amazon/tests/unit/amazon/aws/transfers/test_sftp_to_s3.py @@ -157,3 +157,54 @@ def test_sftp_to_s3_fail_on_file_not_exist(self, fail_on_file_not_exist): conn.delete_object(Bucket=self.s3_bucket, Key=self.s3_key) conn.delete_bucket(Bucket=self.s3_bucket) assert not s3_hook.check_for_bucket(self.s3_bucket) + + @mock_aws + @conf_vars({("core", "enable_xcom_pickling"): "True"}) + def test_sftp_to_s3_sftp_remote_host(self): + """Test that sftp_remote_host overrides the connection host when provided.""" + test_remote_file_content = ( + "This is remote file content for sftp_remote_host test \n which is also multiline " + "another line here \n this is last line. EOF" + ) + + # Create a test file remotely + create_file_task = SSHOperator( + task_id="test_create_file_remote_host", + ssh_hook=self.hook, + command=f"echo '{test_remote_file_content}' > {self.sftp_path}", + do_xcom_push=True, + dag=self.dag, + ) + assert create_file_task is not None + create_file_task.execute(None) + + # Test for creation of s3 bucket + s3_hook = S3Hook(aws_conn_id=None) + conn = boto3.client("s3") + conn.create_bucket(Bucket=self.s3_bucket) + assert s3_hook.check_for_bucket(self.s3_bucket) + + # Execute with sftp_remote_host overriding the connection host to the same localhost + run_task = SFTPToS3Operator( + s3_bucket=BUCKET, + s3_key=S3_KEY, + sftp_path=SFTP_PATH, + sftp_conn_id=SFTP_CONN_ID, + sftp_remote_host="localhost", + s3_conn_id=S3_CONN_ID, + task_id="test_sftp_to_s3_remote_host", + dag=self.dag, + ) + assert run_task is not None + + run_task.execute(None) + + # Check if object was created in s3 + objects_in_dest_bucket = conn.list_objects(Bucket=self.s3_bucket, Prefix=self.s3_key) + assert len(objects_in_dest_bucket["Contents"]) == 1 + assert objects_in_dest_bucket["Contents"][0]["Key"] == self.s3_key + + # Clean up after finishing with test + conn.delete_object(Bucket=self.s3_bucket, Key=self.s3_key) + conn.delete_bucket(Bucket=self.s3_bucket) + assert not s3_hook.check_for_bucket(self.s3_bucket) From 3124b6ca8fca3d23994d53e19b0114086263e2ce Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Mon, 9 Mar 2026 17:44:25 +0100 Subject: [PATCH 057/280] Further limit setuptools after 82.0.1 is released (until redoc fixes it) (#63202) --- devel-common/pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/devel-common/pyproject.toml b/devel-common/pyproject.toml index 350546df09faf..a5020cebff4b8 100644 --- a/devel-common/pyproject.toml +++ b/devel-common/pyproject.toml @@ -103,7 +103,9 @@ dependencies = [ "sphinxcontrib-redoc>=1.6.0", "sphinxcontrib-serializinghtml>=1.1.5", "sphinxcontrib-spelling>=8.0.0", - "setuptools!=82.0.0", # until https://github.com/sphinx-contrib/redoc/issues/53 is resolved + # setuptools 82.0.0+ causes redoc to fail due to pkg_resources removal + # until https://github.com/sphinx-contrib/redoc/issues/53 is resolved + "setuptools<82.0.0", ] "docs-gen" = [ "diagrams>=0.24.4", From 2145e6cb86181ba2ada22eb80820b25882bdfa7c Mon Sep 17 00:00:00 2001 From: Elad Kalif <45845474+eladkal@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:45:38 +0200 Subject: [PATCH 058/280] make hive cli `zooKeeperNamespace` and `ssl` parameters configurable (#63193) * make hive cli zooKeeperNamespace and Ssl parameters configurable * fix static checks * Update providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py Co-authored-by: GPK * fix tests --------- Co-authored-by: romsharon98 Co-authored-by: Jarek Potiuk Co-authored-by: GPK --- providers/apache/hive/docs/connections/hive_cli.rst | 6 ++++++ .../src/airflow/providers/apache/hive/hooks/hive.py | 10 +++++++++- .../hive/tests/unit/apache/hive/hooks/test_hive.py | 12 ++++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/providers/apache/hive/docs/connections/hive_cli.rst b/providers/apache/hive/docs/connections/hive_cli.rst index 5e88df971d989..00807a07502e6 100644 --- a/providers/apache/hive/docs/connections/hive_cli.rst +++ b/providers/apache/hive/docs/connections/hive_cli.rst @@ -77,6 +77,12 @@ High Availability (optional) Specify as ``True`` if you want to connect to a Hive installation running in high availability mode. Specify host accordingly. +Ssl (optional) + Specify as ``True`` to enable SSL for your high availability connection. + +Zoo Keeper Namespace (optional) + Zoo keeper namespace for high availability. + When specifying the connection in environment variable you should specify it using URI syntax. diff --git a/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py b/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py index 5745c5e4a615f..efa0397c0d7ea 100644 --- a/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py +++ b/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py @@ -140,6 +140,10 @@ def get_connection_form_widgets(cls) -> dict[str, Any]: lazy_gettext("Principal"), widget=BS3TextFieldWidget(), default="hive/_HOST@EXAMPLE.COM" ), "high_availability": BooleanField(lazy_gettext("High Availability mode"), default=False), + "ssl": BooleanField(lazy_gettext("Ssl"), default=True), + "zoo_keeper_namespace": StringField( + lazy_gettext("Zoo Keeper Namespace"), widget=BS3TextFieldWidget(), default="hiveserver2" + ), } @classmethod @@ -188,7 +192,11 @@ def _prepare_cli_cmd(self) -> list[Any]: if self.high_availability: if not jdbc_url.endswith(";"): jdbc_url += ";" - jdbc_url += "serviceDiscoveryMode=zooKeeper;ssl=true;zooKeeperNamespace=hiveserver2" + ssl = conn.extra_dejson.get("ssl", True) + zoo_keeper_namespace = conn.extra_dejson.get("zoo_keeper_namespace", "hiveserver2") + jdbc_url += ( + f"serviceDiscoveryMode=zooKeeper;ssl={ssl};zooKeeperNamespace={zoo_keeper_namespace}" + ) elif self.auth: jdbc_url += ";auth=" + self.auth diff --git a/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py b/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py index beb82a926b072..94a573a2259c8 100644 --- a/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py +++ b/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py @@ -1023,18 +1023,22 @@ def test_get_wrong_principal(self): [ ( {"high_availability": "true"}, - "serviceDiscoveryMode=zooKeeper;ssl=true;zooKeeperNamespace=hiveserver2", + "serviceDiscoveryMode=zooKeeper;ssl=True;zooKeeperNamespace=hiveserver2", ), ( {"high_availability": "false"}, - "serviceDiscoveryMode=zooKeeper;ssl=true;zooKeeperNamespace=hiveserver2", + "serviceDiscoveryMode=zooKeeper;ssl=True;zooKeeperNamespace=hiveserver2", ), - ({}, "serviceDiscoveryMode=zooKeeper;ssl=true;zooKeeperNamespace=hiveserver2"), + ( + {"high_availability": "true", "ssl": "false", "zoo_keeper_namespace": "custom_hive_server"}, + "serviceDiscoveryMode=zooKeeper;ssl=false;zooKeeperNamespace=custom_hive_server", + ), + ({}, "serviceDiscoveryMode=zooKeeper;ssl=True;zooKeeperNamespace=hiveserver2"), # with proxy user ( {"proxy_user": "a_user_proxy", "high_availability": "true"}, "hive.server2.proxy.user=a_user_proxy;" - "serviceDiscoveryMode=zooKeeper;ssl=true;zooKeeperNamespace=hiveserver2", + "serviceDiscoveryMode=zooKeeper;ssl=True;zooKeeperNamespace=hiveserver2", ), ], ) From 064d131d6348d0b2bc749ca010124c0d81becd0b Mon Sep 17 00:00:00 2001 From: jerry Date: Tue, 10 Mar 2026 00:46:10 +0800 Subject: [PATCH 059/280] fix(providers/amazon): S3DagBundle does not delete stale dag recursively (#63104) * Refactor S3Hook's local file synchronization logic to mach GCSHook. Update tests to cover nested directories and ensure proper logging of deleted files and directories. * Update S3Hook logging level for deleted files and directories from info to debug to reduce log verbosity. --- .../airflow/providers/amazon/aws/hooks/s3.py | 34 +++++++------------ .../tests/unit/amazon/aws/hooks/test_s3.py | 14 ++++++++ 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py b/providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py index f9beb0d040003..d0f8ce8b78460 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py @@ -1735,27 +1735,19 @@ def delete_bucket_tagging(self, bucket_name: str | None = None) -> None: s3_client.delete_bucket_tagging(Bucket=bucket_name) def _sync_to_local_dir_delete_stale_local_files(self, current_s3_objects: list[Path], local_dir: Path): - current_s3_keys = {key for key in current_s3_objects} - - for item in local_dir.iterdir(): - item: Path # type: ignore[no-redef] - absolute_item_path = item.resolve() - - if absolute_item_path not in current_s3_keys: - try: - if item.is_file(): - item.unlink(missing_ok=True) - self.log.debug("Deleted stale local file: %s", item) - elif item.is_dir(): - # delete only when the folder is empty - if not os.listdir(item): - item.rmdir() - self.log.debug("Deleted stale empty directory: %s", item) - else: - self.log.debug("Skipping stale item of unknown type: %s", item) - except OSError as e: - self.log.error("Error deleting stale item %s: %s", item, e) - raise e + current_s3_keys = {key.resolve() for key in current_s3_objects} + + for item in local_dir.rglob("*"): + if item.is_file() and item.resolve() not in current_s3_keys: + self.log.debug("Deleted stale local file: %s", item) + item.unlink() + # Clean up empty directories + for root, dirs, _ in os.walk(local_dir, topdown=False): + for d in dirs: + dir_path = os.path.join(root, d) + if not os.listdir(dir_path): + self.log.debug("Deleted stale empty directory: %s", dir_path) + os.rmdir(dir_path) def _sync_to_local_dir_if_changed(self, s3_bucket, s3_object, local_target_path: Path): should_download = False diff --git a/providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py b/providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py index 14371da9cb4b3..99fd45aff8903 100644 --- a/providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py +++ b/providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py @@ -1870,14 +1870,28 @@ def get_logs_string(call_args_list): local_file_that_should_be_deleted.write_text("test dag") local_folder_should_be_deleted = Path(sync_local_dir).joinpath("local_folder_should_be_deleted") local_folder_should_be_deleted.mkdir(exist_ok=True) + nested_stale_file = Path(sync_local_dir).joinpath("subproject1", "stale_nested.py") + nested_stale_file.write_text("stale nested file") + deep_nested_dir = Path(sync_local_dir).joinpath("subproject1", "deep") + deep_nested_dir.mkdir() + deep_stale_file = deep_nested_dir.joinpath("stale_deep.py") + deep_stale_file.write_text("stale deep file") hook.log.debug = MagicMock() hook.sync_to_local_dir( bucket_name=s3_bucket, local_dir=sync_local_dir, s3_prefix="", delete_stale=True ) logs_string = get_logs_string(hook.log.debug.call_args_list) assert f"Deleted stale local file: {local_file_that_should_be_deleted.as_posix()}" in logs_string + assert f"Deleted stale local file: {nested_stale_file.as_posix()}" in logs_string + assert f"Deleted stale local file: {deep_stale_file.as_posix()}" in logs_string assert f"Deleted stale empty directory: {local_folder_should_be_deleted.as_posix()}" in logs_string + assert f"Deleted stale empty directory: {deep_nested_dir.as_posix()}" in logs_string + assert not nested_stale_file.exists() + assert not deep_stale_file.exists() + assert not deep_nested_dir.exists() + assert Path(sync_local_dir).joinpath("dag_01.py").exists() + assert Path(sync_local_dir).joinpath("subproject1", "dag_a.py").exists() s3_client.put_object(Bucket=s3_bucket, Key="dag_03.py", Body=b"test data-changed") hook.log.debug = MagicMock() From 68af9863e58dd5da10dfb3a83a90a22a92a200c7 Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Mon, 9 Mar 2026 18:12:12 +0100 Subject: [PATCH 060/280] Add retry mechanism to airflowctl and remove flaky integration mark (#63016) * tenacity added to include retry over httpx --- .../test_airflowctl_commands.py | 2 - .../docs/cli-and-env-variables-ref.rst | 18 ++++++ airflow-ctl/pyproject.toml | 1 + airflow-ctl/src/airflowctl/api/client.py | 39 ++++++++++++ .../tests/airflow_ctl/api/test_client.py | 62 +++++++++++++++++++ 5 files changed, 120 insertions(+), 2 deletions(-) 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..d9d002e9eda76 100644 --- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py +++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py @@ -131,7 +131,6 @@ def date_param(): ] -@pytest.mark.flaky(reruns=3, reruns_delay=1) @pytest.mark.parametrize( "command", TEST_COMMANDS_DEBUG_MODE, @@ -144,7 +143,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/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/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/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 From 5dfea1a1129044a3e8d5c97a86b09a0da3f66e02 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:00:56 -0400 Subject: [PATCH 061/280] chore(deps): bump @hey-api/openapi-ts (#63211) Bumps the core-ui-package-updates group with 1 update in the /airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui directory: [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts). Updates `@hey-api/openapi-ts` from 0.93.1 to 0.94.0 - [Release notes](https://github.com/hey-api/openapi-ts/releases) - [Changelog](https://github.com/hey-api/openapi-ts/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/hey-api/openapi-ts/compare/@hey-api/openapi-ts@0.93.1...@hey-api/openapi-ts@0.94.0) --- updated-dependencies: - dependency-name: "@hey-api/openapi-ts" dependency-version: 0.94.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../auth/managers/simple/ui/package.json | 2 +- .../auth/managers/simple/ui/pnpm-lock.yaml | 34 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) 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 c6b9fc2381ba7..58189eccfb0cc 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 @@ -20,7 +20,7 @@ "dependencies": { "@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", 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 d1ded3d02e464..a7b8967fc2afb 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 @@ -20,10 +20,10 @@ importers: 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) @@ -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' @@ -3420,14 +3420,14 @@ snapshots: '@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 @@ -3453,11 +3453,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 @@ -3466,9 +3466,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 From f7edbd7f9afe5e36befd3b774e05e6784abfbc84 Mon Sep 17 00:00:00 2001 From: Andrii Roiko <57030016+roykoand@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:06:48 +0200 Subject: [PATCH 062/280] Improve DAG processor timeout logging clarity (#62328) * Add 'seconds' to logging message * Improve DAG processor timeout logging clarity * Use floats in log message --- airflow-core/src/airflow/dag_processing/manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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"}) From a2e1fc59d6d6123e6d5dcd8b4b4437298eab52ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Mirowski?= Date: Mon, 9 Mar 2026 21:09:59 +0100 Subject: [PATCH 063/280] Refactor Git-Sync livenessProbe & deprecate readinessProbe & add startupProbe (#62334) * Deploy Git-Sync liveness service * Add recommendedProbeSetting flag * Refactor git-sync probes tests * Remove usage of GitSync readinessProbe * Refactor git-sync liveness probe * Add git-sync startup probe * Misc * Fix spellcheck --- chart/newsfragments/62334.significant.rst | 5 + chart/templates/NOTES.txt | 16 ++ chart/templates/_helpers.yaml | 30 ++- chart/values.schema.json | 73 +++++++- chart/values.yaml | 27 +++ .../airflow_aux/test_airflow_common.py | 18 ++ .../other/test_git_sync_scheduler.py | 174 ++++++++++++++++-- .../other/test_git_sync_triggerer.py | 155 ++++++++++++++-- .../helm_tests/other/test_git_sync_worker.py | 155 ++++++++++++++-- 9 files changed, 613 insertions(+), 40 deletions(-) create mode 100644 chart/newsfragments/62334.significant.rst 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/templates/NOTES.txt b/chart/templates/NOTES.txt index 79d967919dd9b..ba0ec138db661 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: @@ -501,6 +509,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/values.schema.json b/chart/values.schema.json index 74d931666da40..5a8dfcd9123a1 100644 --- a/chart/values.schema.json +++ b/chart/values.schema.json @@ -10649,13 +10649,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" }, diff --git a/chart/values.yaml b/chart/values.yaml index 255db1700bc83..bab02bdef30c0 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -3605,8 +3605,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/helm-tests/tests/helm_tests/airflow_aux/test_airflow_common.py b/helm-tests/tests/helm_tests/airflow_aux/test_airflow_common.py index c5eac7ce2c726..b3ad40e42abd7 100644 --- a/helm-tests/tests/helm_tests/airflow_aux/test_airflow_common.py +++ b/helm-tests/tests/helm_tests/airflow_aux/test_airflow_common.py @@ -146,6 +146,24 @@ def test_dags_mount(self, dag_values, expected_mount): for doc in docs: assert expected_mount in jmespath.search("spec.template.spec.containers[0].volumeMounts", doc) + def test_git_sync_http_port(self): + docs = render_chart( + values={ + "dags": {"gitSync": {"enabled": True, "httpPort": 10}}, + }, + show_only=[ + "templates/dag-processor/dag-processor-deployment.yaml", + "templates/triggerer/triggerer-deployment.yaml", + "templates/workers/worker-deployment.yaml", + ], + ) + + assert len(docs) == 3 + for doc in docs: + envs = jmespath.search("spec.template.spec.containers[?name=='git-sync'] | [0].env", doc) + assert {"name": "GIT_SYNC_HTTP_BIND", "value": ":10"} in envs + assert {"name": "GITSYNC_HTTP_BIND", "value": ":10"} in envs + @pytest.mark.parametrize( "workers_values", [ diff --git a/helm-tests/tests/helm_tests/other/test_git_sync_scheduler.py b/helm-tests/tests/helm_tests/other/test_git_sync_scheduler.py index 9c007a3f49ccc..71b5f69abd0f6 100644 --- a/helm-tests/tests/helm_tests/other/test_git_sync_scheduler.py +++ b/helm-tests/tests/helm_tests/other/test_git_sync_scheduler.py @@ -108,9 +108,18 @@ def test_validate_the_git_sync_container_spec(self): {"name": "GITSYNC_PERIOD", "value": "66s"}, {"name": "GIT_SYNC_MAX_SYNC_FAILURES", "value": "70"}, {"name": "GITSYNC_MAX_FAILURES", "value": "70"}, + {"name": "GIT_SYNC_HTTP_BIND", "value": ":1234"}, + {"name": "GITSYNC_HTTP_BIND", "value": ":1234"}, ], "volumeMounts": [{"mountPath": "/git", "name": "dags"}], "resources": {}, + "startupProbe": { + "failureThreshold": 10, + "httpGet": {"path": "/", "port": 1234}, + "initialDelaySeconds": 0, + "periodSeconds": 5, + "timeoutSeconds": 1, + }, } def test_validate_the_git_sync_container_spec_if_wait_specified(self): @@ -172,9 +181,18 @@ def test_validate_the_git_sync_container_spec_if_wait_specified(self): {"name": "GITSYNC_PERIOD", "value": "66s"}, {"name": "GIT_SYNC_MAX_SYNC_FAILURES", "value": "70"}, {"name": "GITSYNC_MAX_FAILURES", "value": "70"}, + {"name": "GIT_SYNC_HTTP_BIND", "value": ":1234"}, + {"name": "GITSYNC_HTTP_BIND", "value": ":1234"}, ], "volumeMounts": [{"mountPath": "/git", "name": "dags"}], "resources": {}, + "startupProbe": { + "failureThreshold": 10, + "httpGet": {"path": "/", "port": 1234}, + "initialDelaySeconds": 0, + "periodSeconds": 5, + "timeoutSeconds": 1, + }, } def test_validate_if_ssh_params_are_added(self): @@ -409,7 +427,7 @@ def test_resources_are_configurable(self): ) assert jmespath.search("spec.template.spec.containers[1].resources.requests.cpu", docs[0]) == "300m" - def test_liveliness_and_readiness_probes_are_configurable(self): + def test_liveness_probe_configuration(self): livenessProbe = { "failureThreshold": 10, "exec": {"command": ["/bin/true"]}, @@ -418,6 +436,25 @@ def test_liveliness_and_readiness_probes_are_configurable(self): "successThreshold": 1, "timeoutSeconds": 5, } + + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "livenessProbe": livenessProbe, + }, + }, + }, + show_only=["templates/scheduler/scheduler-deployment.yaml"], + ) + + assert livenessProbe == jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].livenessProbe", docs[0] + ) + + def test_readiness_probe_configuration(self): readinessProbe = { "failureThreshold": 10, "exec": {"command": ["/bin/true"]}, @@ -426,28 +463,141 @@ def test_liveliness_and_readiness_probes_are_configurable(self): "successThreshold": 1, "timeoutSeconds": 5, } + docs = render_chart( values={ "airflowVersion": "2.11.0", "dags": { "gitSync": { "enabled": True, - "livenessProbe": livenessProbe, "readinessProbe": readinessProbe, }, }, }, show_only=["templates/scheduler/scheduler-deployment.yaml"], ) - container_search_result = jmespath.search( - "spec.template.spec.containers[?name == 'git-sync']", docs[0] + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].readinessProbe", docs[0] + ) + is None + ) + + assert ( + jmespath.search("spec.template.spec.containers[?name=='git-sync'] | [0].readinessProbe", docs[0]) + == readinessProbe + ) + + def test_readiness_probe_configuration_recommended(self): + readinessProbe = { + "failureThreshold": 10, + "exec": {"command": ["/bin/true"]}, + "initialDelaySeconds": 0, + "periodSeconds": 1, + "successThreshold": 1, + "timeoutSeconds": 5, + } + + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "recommendedProbeSetting": True, + "readinessProbe": readinessProbe, + }, + }, + }, + show_only=["templates/scheduler/scheduler-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].readinessProbe", docs[0] + ) + is None + ) + + assert ( + jmespath.search("spec.template.spec.containers[?name=='git-sync'] | [0].readinessProbe", docs[0]) + is None + ) + + def test_liveness_probe_configuration_recommended(self): + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "httpPort": 10, + "recommendedProbeSetting": True, + "livenessProbe": { + "enabled": True, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + }, + }, + }, + }, + show_only=["templates/scheduler/scheduler-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].livenessProbe", docs[0] + ) + is None ) - init_container_search_result = jmespath.search( - "spec.template.spec.initContainers[?name == 'git-sync-init']", docs[0] + + assert jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].livenessProbe", docs[0] + ) == { + "httpGet": {"path": "/", "port": 10}, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + } + + def test_startup_probe_configuration(self): + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "httpPort": 10, + "startupProbe": { + "enabled": True, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + }, + }, + }, + }, + show_only=["templates/scheduler/scheduler-deployment.yaml"], ) - assert "livenessProbe" in container_search_result[0] - assert "readinessProbe" in container_search_result[0] - assert "readinessProbe" not in init_container_search_result[0] - assert "readinessProbe" not in init_container_search_result[0] - assert livenessProbe == container_search_result[0]["livenessProbe"] - assert readinessProbe == container_search_result[0]["readinessProbe"] + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].startupProbe", docs[0] + ) + is None + ) + + assert jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].startupProbe", docs[0] + ) == { + "httpGet": {"path": "/", "port": 10}, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + } diff --git a/helm-tests/tests/helm_tests/other/test_git_sync_triggerer.py b/helm-tests/tests/helm_tests/other/test_git_sync_triggerer.py index f074cc1bdd4a4..15a2a3b49b38c 100644 --- a/helm-tests/tests/helm_tests/other/test_git_sync_triggerer.py +++ b/helm-tests/tests/helm_tests/other/test_git_sync_triggerer.py @@ -77,7 +77,7 @@ def test_validate_if_ssh_params_are_added_with_git_ssh_key(self): "secret": {"secretName": "release-name-ssh-secret", "defaultMode": 288}, } in jmespath.search("spec.template.spec.volumes", docs[0]) - def test_liveliness_and_readiness_probes_are_configurable(self): + def test_liveness_probe_configuration(self): livenessProbe = { "failureThreshold": 10, "exec": {"command": ["/bin/true"]}, @@ -86,6 +86,24 @@ def test_liveliness_and_readiness_probes_are_configurable(self): "successThreshold": 1, "timeoutSeconds": 5, } + + docs = render_chart( + values={ + "dags": { + "gitSync": { + "enabled": True, + "livenessProbe": livenessProbe, + }, + } + }, + show_only=["templates/triggerer/triggerer-deployment.yaml"], + ) + + assert livenessProbe == jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].livenessProbe", docs[0] + ) + + def test_readiness_probe_configuration(self): readinessProbe = { "failureThreshold": 10, "exec": {"command": ["/bin/true"]}, @@ -94,27 +112,140 @@ def test_liveliness_and_readiness_probes_are_configurable(self): "successThreshold": 1, "timeoutSeconds": 5, } + docs = render_chart( values={ "dags": { "gitSync": { "enabled": True, - "livenessProbe": livenessProbe, "readinessProbe": readinessProbe, }, } }, show_only=["templates/triggerer/triggerer-deployment.yaml"], ) - container_search_result = jmespath.search( - "spec.template.spec.containers[?name == 'git-sync']", docs[0] + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].readinessProbe", docs[0] + ) + is None ) - init_container_search_result = jmespath.search( - "spec.template.spec.initContainers[?name == 'git-sync-init']", docs[0] + + assert readinessProbe == jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].readinessProbe", docs[0] ) - assert "livenessProbe" in container_search_result[0] - assert "readinessProbe" in container_search_result[0] - assert "readinessProbe" not in init_container_search_result[0] - assert "readinessProbe" not in init_container_search_result[0] - assert livenessProbe == container_search_result[0]["livenessProbe"] - assert readinessProbe == container_search_result[0]["readinessProbe"] + + def test_readiness_probe_configuration_recommended(self): + readinessProbe = { + "failureThreshold": 10, + "exec": {"command": ["/bin/true"]}, + "initialDelaySeconds": 0, + "periodSeconds": 1, + "successThreshold": 1, + "timeoutSeconds": 5, + } + + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "recommendedProbeSetting": True, + "readinessProbe": readinessProbe, + }, + }, + }, + show_only=["templates/triggerer/triggerer-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].readinessProbe", docs[0] + ) + is None + ) + + assert ( + jmespath.search("spec.template.spec.containers[?name=='git-sync'] | [0].readinessProbe", docs[0]) + is None + ) + + def test_liveness_probe_configuration_recommended(self): + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "httpPort": 10, + "recommendedProbeSetting": True, + "livenessProbe": { + "enabled": True, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + }, + }, + }, + }, + show_only=["templates/triggerer/triggerer-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].livenessProbe", docs[0] + ) + is None + ) + + assert jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].livenessProbe", docs[0] + ) == { + "httpGet": {"path": "/", "port": 10}, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + } + + def test_startup_probe_configuration(self): + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "httpPort": 10, + "recommendedProbeSetting": True, + "startupProbe": { + "enabled": True, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + }, + }, + }, + }, + show_only=["templates/triggerer/triggerer-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].startupProbe", docs[0] + ) + is None + ) + + assert jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].startupProbe", docs[0] + ) == { + "httpGet": {"path": "/", "port": 10}, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + } diff --git a/helm-tests/tests/helm_tests/other/test_git_sync_worker.py b/helm-tests/tests/helm_tests/other/test_git_sync_worker.py index 32fd71e49f21c..03fdfc1217c7c 100644 --- a/helm-tests/tests/helm_tests/other/test_git_sync_worker.py +++ b/helm-tests/tests/helm_tests/other/test_git_sync_worker.py @@ -202,7 +202,7 @@ def test_container_lifecycle_hooks(self): "preStop": {"exec": {"command": ["/bin/sh", "-c", "echo preStop handler > /git/message_start"]}}, } - def test_liveliness_and_readiness_probes_are_configurable(self): + def test_liveness_probe_configuration(self): livenessProbe = { "failureThreshold": 10, "exec": {"command": ["/bin/true"]}, @@ -211,6 +211,24 @@ def test_liveliness_and_readiness_probes_are_configurable(self): "successThreshold": 1, "timeoutSeconds": 5, } + + docs = render_chart( + values={ + "dags": { + "gitSync": { + "enabled": True, + "livenessProbe": livenessProbe, + }, + } + }, + show_only=["templates/workers/worker-deployment.yaml"], + ) + + assert livenessProbe == jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].livenessProbe", docs[0] + ) + + def test_readiness_probe_configuration(self): readinessProbe = { "failureThreshold": 10, "exec": {"command": ["/bin/true"]}, @@ -219,27 +237,140 @@ def test_liveliness_and_readiness_probes_are_configurable(self): "successThreshold": 1, "timeoutSeconds": 5, } + docs = render_chart( values={ "dags": { "gitSync": { "enabled": True, - "livenessProbe": livenessProbe, "readinessProbe": readinessProbe, }, } }, show_only=["templates/workers/worker-deployment.yaml"], ) - container_search_result = jmespath.search( - "spec.template.spec.containers[?name == 'git-sync']", docs[0] + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].readinessProbe", docs[0] + ) + is None + ) + + assert readinessProbe == jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].readinessProbe", docs[0] ) - init_container_search_result = jmespath.search( - "spec.template.spec.initContainers[?name == 'git-sync-init']", docs[0] + + def test_readiness_probe_configuration_recommended(self): + readinessProbe = { + "failureThreshold": 10, + "exec": {"command": ["/bin/true"]}, + "initialDelaySeconds": 0, + "periodSeconds": 1, + "successThreshold": 1, + "timeoutSeconds": 5, + } + + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "recommendedProbeSetting": True, + "readinessProbe": readinessProbe, + }, + }, + }, + show_only=["templates/workers/worker-deployment.yaml"], ) - assert "livenessProbe" in container_search_result[0] - assert "readinessProbe" in container_search_result[0] - assert "readinessProbe" not in init_container_search_result[0] - assert "readinessProbe" not in init_container_search_result[0] - assert livenessProbe == container_search_result[0]["livenessProbe"] - assert readinessProbe == container_search_result[0]["readinessProbe"] + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].readinessProbe", docs[0] + ) + is None + ) + + assert ( + jmespath.search("spec.template.spec.containers[?name=='git-sync'] | [0].readinessProbe", docs[0]) + is None + ) + + def test_liveness_probe_configuration_recommended(self): + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "httpPort": 10, + "recommendedProbeSetting": True, + "livenessProbe": { + "enabled": True, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + }, + }, + }, + }, + show_only=["templates/workers/worker-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].livenessProbe", docs[0] + ) + is None + ) + + assert jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].livenessProbe", docs[0] + ) == { + "httpGet": {"path": "/", "port": 10}, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + } + + def test_startup_probe_configuration(self): + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "httpPort": 10, + "recommendedProbeSetting": True, + "startupProbe": { + "enabled": True, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + }, + }, + }, + }, + show_only=["templates/workers/worker-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].startupProbe", docs[0] + ) + is None + ) + + assert jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].startupProbe", docs[0] + ) == { + "httpGet": {"path": "/", "port": 10}, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + } From 423222bec3c2c51fca82e67d99239c6ac905e1ee Mon Sep 17 00:00:00 2001 From: Daniel Reeves <31971762+dwreeves@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:14:40 -0400 Subject: [PATCH 064/280] [Snowflake] [Feat] Allow SnowflakeHook + SnowflakeSqlApiHook `private_key_content` to use raw key in addition to base64 encoding (#62378) * allow SnowflakeHook private_key_content to use raw key instead of only base64 encoding. * Make code more DRY by moving get_private_key() to SnowflakeHook. * Fix get_connection call in SnowflakeHook * assert private_key is b64decoded when passed in as b64encoded value * Update Snowflake provider docs to clarify new private_key_content behavior --- .../snowflake/docs/connections/snowflake.rst | 2 +- .../providers/snowflake/hooks/snowflake.py | 85 +++++++++++------- .../snowflake/hooks/snowflake_sql_api.py | 43 +-------- .../unit/snowflake/hooks/test_snowflake.py | 90 ++++++++++++++++++- .../snowflake/hooks/test_snowflake_sql_api.py | 16 ++-- 5 files changed, 150 insertions(+), 86 deletions(-) diff --git a/providers/snowflake/docs/connections/snowflake.rst b/providers/snowflake/docs/connections/snowflake.rst index 900ed04a1ed73..584bbcfa7c497 100644 --- a/providers/snowflake/docs/connections/snowflake.rst +++ b/providers/snowflake/docs/connections/snowflake.rst @@ -64,7 +64,7 @@ Extra (optional) * ``refresh_token``: Specify refresh_token for OAuth connection. * ``azure_conn_id``: Azure Connection ID to be used for retrieving the OAuth token using Azure Entra authentication. Login and Password fields aren't required when using this method. Scope for the Azure OAuth token can be set in the config option ``azure_oauth_scope`` under the section ``[snowflake]``. Requires `apache-airflow-providers-microsoft-azure>=12.8.0`. * ``private_key_file``: Specify the path to the private key file. - * ``private_key_content``: Specify the content of the private key file in base64 encoded format. You can use the following Python code to encode the private key: + * ``private_key_content``: Specify the content of the private key file, either in plain text or base64 encoded. When using the Airflow UI to manage the Snowflake connection, you should base64 encode the ``private_key_content``. You can use the following Python code to encode the private key: .. code-block:: python diff --git a/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake.py b/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake.py index 6199bd6ff54b0..3a9205fd8f519 100644 --- a/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake.py +++ b/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake.py @@ -56,6 +56,7 @@ if TYPE_CHECKING: + from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes from snowflake.connector import SnowflakeConnection from airflow.providers.openlineage.extractors import OperatorLineage @@ -400,40 +401,9 @@ def _get_static_conn_params(self) -> dict[str, str | None]: if client_store_temporary_credential: conn_config["client_store_temporary_credential"] = client_store_temporary_credential - # If private_key_file is specified in the extra json, load the contents of the file as a private key. - # If private_key_content is specified in the extra json, use it as a private key. - # As a next step, specify this private key in the connection configuration. - # The connection password then becomes the passphrase for the private key. - # If your private key is not encrypted (not recommended), then leave the password empty. - - private_key_file = self._get_field(extra_dict, "private_key_file") - private_key_content = self._get_field(extra_dict, "private_key_content") - - private_key_pem = None - if private_key_content and private_key_file: - raise AirflowException( - "The private_key_file and private_key_content extra fields are mutually exclusive. " - "Please remove one." - ) - if private_key_file: - private_key_file_path = Path(private_key_file) - if not private_key_file_path.is_file() or private_key_file_path.stat().st_size == 0: - raise ValueError("The private_key_file path points to an empty or invalid file.") - if private_key_file_path.stat().st_size > 4096: - raise ValueError("The private_key_file size is too big. Please keep it less than 4 KB.") - private_key_pem = Path(private_key_file_path).read_bytes() - elif private_key_content: - private_key_pem = base64.b64decode(private_key_content) - - if private_key_pem: - passphrase = None - if conn.password: - passphrase = conn.password.strip().encode() - - p_key = serialization.load_pem_private_key( - private_key_pem, password=passphrase, backend=default_backend() - ) + p_key = self.get_private_key() + if p_key: pkb = p_key.private_bytes( encoding=serialization.Encoding.DER, format=serialization.PrivateFormat.PKCS8, @@ -587,6 +557,55 @@ def _request_oauth_token( response.raise_for_status() return response + def get_private_key(self) -> PrivateKeyTypes | None: + """Get the private key from snowflake connection.""" + conn = self.get_connection(self.get_conn_id()) + extra_dict = conn.extra_dejson + + # If private_key_file is specified in the extra json, load the contents of the file as a private key. + # If private_key_content is specified in the extra json, use it as a private key. + # As a next step, specify this private key in the connection configuration. + # The connection password then becomes the passphrase for the private key. + # If your private key is not encrypted (not recommended), then leave the password empty. + + private_key_file = self._get_field(extra_dict, "private_key_file") + private_key_content = self._get_field(extra_dict, "private_key_content") + + passphrase = None + if conn.password: + passphrase = conn.password.strip().encode() + + private_key_pem = None + p_key = None + + if private_key_content and private_key_file: + raise AirflowException( + "The private_key_file and private_key_content extra fields are mutually exclusive. " + "Please remove one." + ) + if private_key_file: + private_key_file_path = Path(private_key_file) + if not private_key_file_path.is_file() or private_key_file_path.stat().st_size == 0: + raise ValueError("The private_key_file path points to an empty or invalid file.") + if private_key_file_path.stat().st_size > 4096: + raise ValueError("The private_key_file size is too big. Please keep it less than 4 KB.") + private_key_pem = Path(private_key_file_path).read_bytes() + elif private_key_content: + try: + p_key = serialization.load_pem_private_key( + private_key_content.encode(), password=passphrase, backend=default_backend() + ) + except (TypeError, ValueError): + # Assume base64 encoding if string is not valid private key + private_key_pem = base64.b64decode(private_key_content) + + if private_key_pem: + p_key = serialization.load_pem_private_key( + private_key_pem, password=passphrase, backend=default_backend() + ) + + return p_key + def get_uri(self) -> str: """Override DbApiHook get_uri method for get_sqlalchemy_engine().""" conn_params = self._get_conn_params() diff --git a/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake_sql_api.py b/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake_sql_api.py index efe45ed9d13bf..2f52be48b8887 100644 --- a/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake_sql_api.py +++ b/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake_sql_api.py @@ -17,19 +17,15 @@ from __future__ import annotations import asyncio -import base64 import time import uuid import warnings from datetime import timedelta -from pathlib import Path from typing import Any import aiohttp import requests from aiohttp import ClientConnectionError, ClientResponseError -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization from requests.exceptions import ConnectionError, HTTPError, Timeout from tenacity import ( AsyncRetrying, @@ -134,43 +130,6 @@ def __init__( self.aiohttp_session_kwargs = aiohttp_session_kwargs or {} self.aiohttp_request_kwargs = aiohttp_request_kwargs or {} - def get_private_key(self) -> None: - """Get the private key from snowflake connection.""" - conn = self.get_connection(self.snowflake_conn_id) - - # If private_key_file is specified in the extra json, load the contents of the file as a private key. - # If private_key_content is specified in the extra json, use it as a private key. - # As a next step, specify this private key in the connection configuration. - # The connection password then becomes the passphrase for the private key. - # If your private key is not encrypted (not recommended), then leave the password empty. - - private_key_file = conn.extra_dejson.get( - "extra__snowflake__private_key_file" - ) or conn.extra_dejson.get("private_key_file") - private_key_content = conn.extra_dejson.get( - "extra__snowflake__private_key_content" - ) or conn.extra_dejson.get("private_key_content") - - private_key_pem = None - if private_key_content and private_key_file: - raise AirflowException( - "The private_key_file and private_key_content extra fields are mutually exclusive. " - "Please remove one." - ) - if private_key_file: - private_key_pem = Path(private_key_file).read_bytes() - elif private_key_content: - private_key_pem = base64.b64decode(private_key_content) - - if private_key_pem: - passphrase = None - if conn.password: - passphrase = conn.password.strip().encode() - - self.private_key = serialization.load_pem_private_key( - private_key_pem, password=passphrase, backend=default_backend() - ) - def execute_query( self, sql: str, statement_count: int, query_tag: str = "", bindings: dict[str, Any] | None = None ) -> list[str]: @@ -272,7 +231,7 @@ def get_headers(self) -> dict[str, Any]: # Alternatively, get the JWT token from the connection details and the private key if not self.private_key: - self.get_private_key() + self.private_key = self.get_private_key() token = JWTGenerator( conn_config["account"], # type: ignore[arg-type] diff --git a/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake.py b/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake.py index fa3f5a65a033c..901ac925c0700 100644 --- a/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake.py +++ b/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake.py @@ -372,9 +372,93 @@ def test_hook_should_support_prepare_basic_conn_params_and_uri( assert SnowflakeHook(snowflake_conn_id="test_conn").get_uri() == expected_uri assert SnowflakeHook(snowflake_conn_id="test_conn")._get_conn_params() == expected_conn_params + def test_plain_text_unencrypted_private_key_is_not_base64_encoded( + self, unencrypted_temporary_private_key: Path + ): + """Test get_private_key function skips base64 encoding if private key is plain text.""" + private_key_content = unencrypted_temporary_private_key.read_text() + + p_key = serialization.load_pem_private_key( + private_key_content.encode(), + password=None, + backend=default_backend(), + ) + + pkb = p_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + connection_kwargs: Any = { + **BASE_CONNECTION_KWARGS, + "password": None, + "extra": { + "database": "db", + "account": "airflow", + "warehouse": "af_wh", + "region": "af_region", + "role": "af_role", + "private_key_content": private_key_content, + }, + } + with mock.patch.dict("os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri()): + conn_params = SnowflakeHook(snowflake_conn_id="test_conn")._get_conn_params() + assert "private_key" in conn_params + assert pkb == conn_params["private_key"] + + def test_plain_text_encrypted_private_key_is_not_base64_encoded( + self, encrypted_temporary_private_key: Path + ): + """Test get_private_key function skips base64 encoding if private key is plain text.""" + private_key_content = encrypted_temporary_private_key.read_text() + + p_key = serialization.load_pem_private_key( + private_key_content.encode(), + password=_PASSWORD.encode(), + backend=default_backend(), + ) + + pkb = p_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + connection_kwargs: Any = { + **BASE_CONNECTION_KWARGS, + "password": _PASSWORD, + "extra": { + "database": "db", + "account": "airflow", + "warehouse": "af_wh", + "region": "af_region", + "role": "af_role", + "private_key_content": private_key_content, + }, + } + with mock.patch.dict("os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri()): + conn_params = SnowflakeHook(snowflake_conn_id="test_conn")._get_conn_params() + assert "private_key" in conn_params + assert pkb == conn_params["private_key"] + def test_get_conn_params_should_support_private_auth_in_connection( - self, base64_encoded_encrypted_private_key: Path + self, base64_encoded_encrypted_private_key: str, encrypted_temporary_private_key: Path ): + private_key_content = encrypted_temporary_private_key.read_text() + + p_key = serialization.load_pem_private_key( + private_key_content.encode(), + password=_PASSWORD.encode(), + backend=default_backend(), + ) + + pkb = p_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + connection_kwargs: Any = { **BASE_CONNECTION_KWARGS, "password": _PASSWORD, @@ -388,7 +472,9 @@ def test_get_conn_params_should_support_private_auth_in_connection( }, } with mock.patch.dict("os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri()): - assert "private_key" in SnowflakeHook(snowflake_conn_id="test_conn")._get_conn_params() + conn_params = SnowflakeHook(snowflake_conn_id="test_conn")._get_conn_params() + assert "private_key" in conn_params + assert conn_params["private_key"] == pkb @pytest.mark.parametrize("include_params", [True, False]) def test_hook_param_beats_extra(self, include_params): diff --git a/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py b/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py index a5e332df41c17..32e475fa15b1e 100644 --- a/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py +++ b/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py @@ -561,8 +561,8 @@ def test_get_private_key_should_support_private_auth_in_connection( "os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri() ): hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn") - hook.get_private_key() - assert hook.private_key is not None + private_key = hook.get_private_key() + assert private_key is not None def test_get_private_key_raise_exception( self, encrypted_temporary_private_key: Path, base64_encoded_encrypted_private_key: str @@ -617,8 +617,8 @@ def test_get_private_key_should_support_private_auth_with_encrypted_key( "os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri() ): hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn") - hook.get_private_key() - assert hook.private_key is not None + private_key = hook.get_private_key() + assert private_key is not None def test_get_private_key_should_support_private_auth_with_unencrypted_key( self, @@ -640,15 +640,15 @@ def test_get_private_key_should_support_private_auth_with_unencrypted_key( "os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri() ): hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn") - hook.get_private_key() - assert hook.private_key is not None + private_key = hook.get_private_key() + assert private_key is not None connection_kwargs["password"] = "" with unittest.mock.patch.dict( "os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri() ): hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn") - hook.get_private_key() - assert hook.private_key is not None + private_key = hook.get_private_key() + assert private_key is not None connection_kwargs["password"] = _PASSWORD with ( unittest.mock.patch.dict( From 3c7641373bcc41c36fd4b16b45f4c63c23367072 Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Mon, 9 Mar 2026 21:22:39 +0100 Subject: [PATCH 065/280] Run test-coverage when airflowctl command has any change (#63216) --- airflow-ctl/.pre-commit-config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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$ From 56ec614a3b92a0471ae1d426b42496e3d29bc8ef Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Mon, 9 Mar 2026 16:56:19 -0400 Subject: [PATCH 066/280] Weekly updates, add more groups (#63219) --- .github/dependabot.yml | 66 +++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b8dc1d985aa5f..1362bc6ae6cc4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -81,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 @@ -102,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 @@ -123,7 +143,7 @@ updates: directories: - /dev/react-plugin-tools/react_plugin_template schedule: - interval: daily + interval: "weekly" groups: ui-plugin-template-package-updates: patterns: @@ -131,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: @@ -144,7 +161,7 @@ updates: directories: - /providers/edge3/src/airflow/providers/edge3/plugins/www schedule: - interval: daily + interval: "weekly" groups: edge-ui-package-updates: patterns: @@ -168,15 +185,15 @@ updates: directories: - /registry schedule: - interval: daily + interval: "weekly" groups: - core-ui-package-updates: + registry-package-updates: patterns: - "*" update-types: - "minor" - "patch" - major-version-updates: + registry-major-version-updates: patterns: - "*" applies-to: security-updates @@ -211,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: @@ -230,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: @@ -253,7 +270,7 @@ updates: - /docker_tests - / schedule: - interval: daily + interval: "weekly" target-branch: v2-11-test groups: pip-dependency-updates: @@ -266,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: From 4812140d532a10eb3106e8714e4d1a45f43b9b13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:06:09 +0100 Subject: [PATCH 067/280] chore(deps): bump github/codeql-action (#63224) Bumps the github-actions-updates group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.29.0 to 4.32.6 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ce28f5bb42b7a9f2c824e633a3f6ee835bab6858...0d579ffd059c29b07949a3cce3983f0780820c98) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.32.6 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 81a9bf0d9a23a..0c21c6e43d652 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -52,15 +52,15 @@ jobs: 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}}" From 2d66cc143316001e07aa4f18e9c533c71ac80d3c Mon Sep 17 00:00:00 2001 From: Vincent <97131062+vincbeck@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:08:01 -0400 Subject: [PATCH 068/280] Update provider release guide to mention Github labels (#63218) --- dev/README_RELEASE_PROVIDERS.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 From a88d61d3cbe5c7d247ae960041ee34e3815e803d Mon Sep 17 00:00:00 2001 From: "leon.jeon" <58650453+Leondon9@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:21:10 +0000 Subject: [PATCH 069/280] Add `--action-on-existing-key` to `pools import` and `connections import` (#62702) closes: #62695 Co-authored-by: claude-flow --- airflow-ctl/src/airflowctl/ctl/cli_config.py | 14 +++--- .../ctl/commands/connection_command.py | 2 +- .../airflowctl/ctl/commands/pool_command.py | 6 +-- .../ctl/commands/test_connections_command.py | 48 ++++++++++++++++++- .../ctl/commands/test_pool_command.py | 36 ++++++++++++-- 5 files changed, 91 insertions(+), 15 deletions(-) diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py index 28dce22805dd5..5ebfb3809e023 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"), ) @@ -865,7 +864,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 +897,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 +915,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/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/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.""" From 2023b624712d022ab4dd4eed9338859589b70897 Mon Sep 17 00:00:00 2001 From: "leon.jeon" <58650453+Leondon9@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:22:00 +0000 Subject: [PATCH 070/280] Send `limit` parameter in `execute_list` server requests (#63048) * Send limit parameter in execute_list server requests execute_list uses a local limit (default 50) for offset arithmetic but did not include it in server requests. The server falls back to its own fallback_page_limit which happens to also default to 50, so current behavior is correct. However, if the server config differs, offsets diverge and pages overlap. Include limit in shared_params so pagination is robust regardless of server configuration. Co-Authored-By: claude-flow * Update airflow-ctl/tests/airflow_ctl/api/test_operations.py Co-authored-by: Henry Chen --------- Co-authored-by: claude-flow Co-authored-by: Henry Chen --- airflow-ctl/src/airflowctl/api/operations.py | 2 +- .../tests/airflow_ctl/api/test_operations.py | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/airflow-ctl/src/airflowctl/api/operations.py b/airflow-ctl/src/airflowctl/api/operations.py index e4a046ed77421..445d490c08dc3 100644 --- a/airflow-ctl/src/airflowctl/api/operations.py +++ b/airflow-ctl/src/airflowctl/api/operations.py @@ -164,7 +164,7 @@ def execute_list( limit: int = 50, params: dict | None = None, ) -> T | ServerResponseError: - shared_params = {**(params or {})} + shared_params = {"limit": limit, **(params or {})} self.response = self.client.get(path, params=shared_params) first_pass = data_model.model_validate_json(self.response.content) total_entries = first_pass.total_entries # type: ignore[attr-defined] 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 From 24f70b41883771c7ab2db2c897d2d7871ef5d51d Mon Sep 17 00:00:00 2001 From: Dheeraj Turaga Date: Mon, 9 Mar 2026 16:32:18 -0500 Subject: [PATCH 071/280] Add airflowctl auth token command to print JWT access tokens (#62843) * Add airflowctl auth token command to print JWT access tokens This follows the project's convention of focusing on user impact. The command authenticates with username/password and prints the access token to stdout, which is useful for scripting and piping into other tools. * Add token command to integration tests --------- Co-authored-by: bugraoz93 --- .../test_airflowctl_commands.py | 5 +- airflow-ctl/docs/images/command_hashes.txt | 2 +- airflow-ctl/docs/images/output_auth.svg | 82 ++++++++++--------- airflow-ctl/src/airflowctl/ctl/cli_config.py | 14 ++++ .../airflowctl/ctl/commands/auth_command.py | 17 ++++ 5 files changed, 79 insertions(+), 41 deletions(-) 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 d9d002e9eda76..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", 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/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py index 5ebfb3809e023..bdcd8b2fb9060 100755 --- a/airflow-ctl/src/airflowctl/ctl/cli_config.py +++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py @@ -841,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 = ( 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 From 112e90828a0fdadaa2eee5c32b82d5600ddfd9d6 Mon Sep 17 00:00:00 2001 From: Jens Scheffler <95105677+jscheffl@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:46:49 +0100 Subject: [PATCH 072/280] Clarify docs on max_active_tasks parameter on a Dag (#63217) --- task-sdk/src/airflow/sdk/definitions/dag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/task-sdk/src/airflow/sdk/definitions/dag.py b/task-sdk/src/airflow/sdk/definitions/dag.py index 8670800c162aa..94e447e37a56d 100644 --- a/task-sdk/src/airflow/sdk/definitions/dag.py +++ b/task-sdk/src/airflow/sdk/definitions/dag.py @@ -362,7 +362,7 @@ class DAG: accessible in templates, namespaced under `params`. These params can be overridden at the task level. :param max_active_tasks: the number of task instances allowed to run - concurrently + concurrently per Dag run. Note that in Airflow 2 this was a global limit on the Dag, since Airflow 3 it is per run. :param max_active_runs: maximum number of active DAG runs, beyond this number of DAG runs in a running state, the scheduler won't create new active DAG runs From 9bbe125b12c311b2f927dc6ed62e8c7a4c26a9a6 Mon Sep 17 00:00:00 2001 From: Dheeraj Turaga Date: Mon, 9 Mar 2026 17:07:54 -0500 Subject: [PATCH 073/280] Fix _execution_api_server_url() reading edge.api_url when execution_api_server_url is already set (#63192) Previously, _execution_api_server_url() unconditionally read edge.api_url even when core.execution_api_server_url was explicitly configured, causing an unnecessary AirflowConfigException if only the latter was set. This also caused test_supervise_launch and test_supervise_launch_fail to fail since the test environment does not load provider config. Reorder config reads so core.execution_api_server_url is checked first and edge.api_url is only read as a fallback when needed. Mock _execution_api_server_url in test_supervise_launch and test_supervise_launch_fail to isolate them from config requirements. Add a test case covering core.execution_api_server_url set without edge.api_url. --- .../src/airflow/providers/edge3/cli/worker.py | 4 ++-- .../edge3/tests/unit/edge3/cli/test_worker.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/providers/edge3/src/airflow/providers/edge3/cli/worker.py b/providers/edge3/src/airflow/providers/edge3/cli/worker.py index 6ac43e388f82c..34352717350d4 100644 --- a/providers/edge3/src/airflow/providers/edge3/cli/worker.py +++ b/providers/edge3/src/airflow/providers/edge3/cli/worker.py @@ -82,10 +82,10 @@ def _edge_hostname() -> str: @cache def _execution_api_server_url() -> str: """Get the execution api server url from config or environment.""" - api_url = conf.get("edge", "api_url") execution_api_server_url = conf.get("core", "execution_api_server_url", fallback="") - if not execution_api_server_url and api_url: + if not execution_api_server_url: # Derive execution api url from edge api url as fallback + api_url = conf.get("edge", "api_url") execution_api_server_url = api_url.replace("edge_worker/v1/rpcapi", "execution") logger.info("Using execution api server url: %s", execution_api_server_url) return execution_api_server_url diff --git a/providers/edge3/tests/unit/edge3/cli/test_worker.py b/providers/edge3/tests/unit/edge3/cli/test_worker.py index a9c16d108221f..b2b97034b36ba 100644 --- a/providers/edge3/tests/unit/edge3/cli/test_worker.py +++ b/providers/edge3/tests/unit/edge3/cli/test_worker.py @@ -161,6 +161,10 @@ def mock_edgeworker(self) -> EdgeWorkerModel: }, "https://other-endpoint", ), + ( + {("core", "execution_api_server_url"): "https://direct-execution-endpoint"}, + "https://direct-execution-endpoint", + ), ], ) def test_execution_api_server_url( @@ -173,11 +177,16 @@ def test_execution_api_server_url( url = _execution_api_server_url() assert url == expected_url + @patch( + "airflow.providers.edge3.cli.worker._execution_api_server_url", + return_value="https://mock-execution-api", + ) @patch("airflow.sdk.execution_time.supervisor.supervise") @pytest.mark.asyncio async def test_supervise_launch( self, mock_supervise, + mock_execution_api_url, worker_with_job: EdgeWorker, ): edge_job = worker_with_job.jobs.pop().edge_job @@ -187,11 +196,16 @@ async def test_supervise_launch( assert result == 0 q.put.assert_not_called() + @patch( + "airflow.providers.edge3.cli.worker._execution_api_server_url", + return_value="https://mock-execution-api", + ) @patch("airflow.sdk.execution_time.supervisor.supervise") @pytest.mark.asyncio async def test_supervise_launch_fail( self, mock_supervise, + mock_execution_api_url, worker_with_job: EdgeWorker, ): mock_supervise.side_effect = Exception("Supervise failed") From 0cebad00b0e8793b0b4c1c994cf5a9ed72e49ff7 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Mon, 9 Mar 2026 23:16:04 +0100 Subject: [PATCH 074/280] CI: Upgrade important CI environment (#63231) --- .github/actions/install-prek/action.yml | 2 +- .pre-commit-config.yaml | 2 +- Dockerfile.ci | 2 +- dev/breeze/doc/ci/02_images.md | 2 +- dev/breeze/pyproject.toml | 2 +- .../commands/release_management_commands.py | 2 +- dev/breeze/uv.lock | 68 +++++++++---------- 7 files changed, 40 insertions(+), 40 deletions(-) diff --git a/.github/actions/install-prek/action.yml b/.github/actions/install-prek/action.yml index 6832db8290031..ffa5cd2fd13b1 100644 --- a/.github/actions/install-prek/action.yml +++ b/.github/actions/install-prek/action.yml @@ -27,7 +27,7 @@ inputs: 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/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66c93463168bb..a68b3080d8836 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/Dockerfile.ci b/Dockerfile.ci index f8851c78e99d3..5b0e6f7eaefc0 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1734,7 +1734,7 @@ COPY --from=scripts common.sh install_packaging_tools.sh install_additional_depe 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.9 -ARG AIRFLOW_PREK_VERSION="0.3.4" +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/dev/breeze/doc/ci/02_images.md b/dev/breeze/doc/ci/02_images.md index e77fc4b36df96..a24490262c85e 100644 --- a/dev/breeze/doc/ci/02_images.md +++ b/dev/breeze/doc/ci/02_images.md @@ -444,7 +444,7 @@ can be used for CI images: | `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.9` | `uv` version used. | -| `AIRFLOW_PREK_VERSION` | `0.3.4` | `prek` 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/pyproject.toml b/dev/breeze/pyproject.toml index 671e91bd95378..d830e8fb296cd 100644 --- a/dev/breeze/pyproject.toml +++ b/dev/breeze/pyproject.toml @@ -57,7 +57,7 @@ dependencies = [ "jinja2>=3.1.5", "jsonschema>=4.19.1", "packaging>=25.0", - "prek>=0.3.4", + "prek>=0.3.5", "psutil>=5.9.6", "pygithub>=2.1.1", "pytest-xdist>=3.3.1", diff --git a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py index 65853c1a3fe45..b136318093a65 100644 --- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py @@ -264,7 +264,7 @@ class VersionedFile(NamedTuple): AIRFLOW_USE_UV = False GITPYTHON_VERSION = "3.1.46" RICH_VERSION = "14.3.3" -PREK_VERSION = "0.3.4" +PREK_VERSION = "0.3.5" HATCH_VERSION = "1.16.5" PYYAML_VERSION = "6.0.3" diff --git a/dev/breeze/uv.lock b/dev/breeze/uv.lock index 98a7d6f468048..497066c112ffb 100644 --- a/dev/breeze/uv.lock +++ b/dev/breeze/uv.lock @@ -74,7 +74,7 @@ requires-dist = [ { name = "jinja2", specifier = ">=3.1.5" }, { name = "jsonschema", specifier = ">=4.19.1" }, { name = "packaging", specifier = ">=25.0" }, - { name = "prek", specifier = ">=0.3.4" }, + { name = "prek", specifier = ">=0.3.5" }, { name = "psutil", specifier = ">=5.9.6" }, { name = "pygithub", specifier = ">=2.1.1" }, { name = "pytest", specifier = ">=9.0.0" }, @@ -260,30 +260,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.63" +version = "1.42.64" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/2a/33d5d4b16fd97dfd629421ebed2456392eae1553cc401d9f86010c18065e/boto3-1.42.63.tar.gz", hash = "sha256:cd008cfd0d7ea30f1c5e22daf0998c55b7c6c68cb68eea05110e33fe641173d5", size = 112778, upload-time = "2026-03-06T22:47:55.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/3e/3f5f58100340f6576aa93da0fe46cabd91ea19baa746b80bd1d46498b0db/boto3-1.42.64.tar.gz", hash = "sha256:58d47897a26adbc22f6390d133dab772fb606ba72695291a8c9e20cba1c7fd23", size = 112773, upload-time = "2026-03-09T19:52:00.407Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/19/f1d8d2b24871d3d0ccb2cbd0b0cb64a3396d439384bd9643d2c25c641b84/boto3-1.42.63-py3-none-any.whl", hash = "sha256:d502a89a0acc701692ae020d15981f2a82e9eb3485acc651cfd0cf1a3afe79ee", size = 140554, upload-time = "2026-03-06T22:47:53.463Z" }, + { url = "https://files.pythonhosted.org/packages/4c/87/2f02a6db0828f4579aedef7e34ec15262e4aa402d31f31bdbc64ae8e471b/boto3-1.42.64-py3-none-any.whl", hash = "sha256:2ca6b472937a54ba74af0b4bede582ba98c070408db1061fc26d5c3aa8e6e7e6", size = 140557, upload-time = "2026-03-09T19:51:57.652Z" }, ] [[package]] name = "botocore" -version = "1.42.63" +version = "1.42.64" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/eb/a1c042f6638ada552399a9977335a6de2668a85bf80bece193c953531236/botocore-1.42.63.tar.gz", hash = "sha256:1fdfc33cff58d21e8622cf620ba2bba3cff324557932aaf935b5374e4610f059", size = 14965362, upload-time = "2026-03-06T22:47:44.158Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/3c/ac4bc939da695d2c648bf28f7b204ab741e4504e81749ccf943403cc07ca/botocore-1.42.64.tar.gz", hash = "sha256:4ee2aece227b9171ace8b749af694a77ab984fceab1639f2626bd0d6fb1aa69d", size = 14967869, upload-time = "2026-03-09T19:51:46.213Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/60/17a2d3b94658bb999c6aee7bba6c76b271905debf0c8c8e6ac63ca8491bc/botocore-1.42.63-py3-none-any.whl", hash = "sha256:83f39d04f2b316bdfc59a3cac2d12238bde7126ac99d9a57d910dbd86d58c528", size = 14639889, upload-time = "2026-03-06T22:47:39.347Z" }, + { url = "https://files.pythonhosted.org/packages/33/0f/a0feb9a93da8f583217432dce71ce1940d6d8aa5884bad340872a504ba3f/botocore-1.42.64-py3-none-any.whl", hash = "sha256:f77c5cb76ed30576ed0bc73b591265d03dddffff02a9208d3ee0c790f43d3cd2", size = 14641339, upload-time = "2026-03-09T19:51:41.244Z" }, ] [[package]] @@ -588,11 +588,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.25.0" +version = "3.25.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8b/4c32ecde6bea6486a2a5d05340e695174351ff6b06cf651a74c005f9df00/filelock-3.25.1.tar.gz", hash = "sha256:b9a2e977f794ef94d77cdf7d27129ac648a61f585bff3ca24630c1629f701aa9", size = 40319, upload-time = "2026-03-09T19:38:47.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b8/2f664b56a3b4b32d28d3d106c71783073f712ba43ff6d34b9ea0ce36dc7b/filelock-3.25.1-py3-none-any.whl", hash = "sha256:18972df45473c4aa2c7921b609ee9ca4925910cc3a0fb226c96b92fc224ef7bf", size = 26720, upload-time = "2026-03-09T19:38:45.718Z" }, ] [[package]] @@ -1221,26 +1221,26 @@ wheels = [ [[package]] name = "prek" -version = "0.3.4" +version = "0.3.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/51/2324eaad93a4b144853ca1c56da76f357d3a70c7b4fd6659e972d7bb8660/prek-0.3.4.tar.gz", hash = "sha256:56a74d02d8b7dfe3c774ecfcd8c1b4e5f1e1b84369043a8003e8e3a779fce72d", size = 356633, upload-time = "2026-02-28T03:47:13.452Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/d6/277e002e56eeab3a9d48f1ca4cc067d249d6326fc1783b770d70ad5ae2be/prek-0.3.5.tar.gz", hash = "sha256:ca40b6685a4192256bc807f32237af94bf9b8799c0d708b98735738250685642", size = 374806, upload-time = "2026-03-09T10:35:18.842Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/20/1a964cb72582307c2f1dc7f583caab90f42810ad41551e5220592406a4c3/prek-0.3.4-py3-none-linux_armv6l.whl", hash = "sha256:c35192d6e23fe7406bd2f333d1c7dab1a4b34ab9289789f453170f33550aa74d", size = 4641915, upload-time = "2026-02-28T03:47:03.772Z" }, - { url = "https://files.pythonhosted.org/packages/c5/cb/4a21f37102bac37e415b61818344aa85de8d29a581253afa7db8c08d5a33/prek-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f784d78de72a8bbe58a5fe7bde787c364ae88f0aff5222c5c5c7287876c510a", size = 4649166, upload-time = "2026-02-28T03:47:06.164Z" }, - { url = "https://files.pythonhosted.org/packages/85/9c/a7c0d117a098d57931428bdb60fcb796e0ebc0478c59288017a2e22eca96/prek-0.3.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50a43f522625e8c968e8c9992accf9e29017abad6c782d6d176b73145ad680b7", size = 4274422, upload-time = "2026-02-28T03:46:59.356Z" }, - { url = "https://files.pythonhosted.org/packages/59/84/81d06df1724d09266df97599a02543d82fde7dfaefd192f09d9b2ccb092f/prek-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:4bbb1d3912a88935f35c6ba4466b4242732e3e3a8c608623c708e83cea85de00", size = 4629873, upload-time = "2026-02-28T03:46:56.419Z" }, - { url = "https://files.pythonhosted.org/packages/09/cd/bb0aefa25cfacd8dbced75b9a9d9945707707867fa5635fb69ae1bbc2d88/prek-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca4d4134db8f6e8de3c418317becdf428957e3cab271807f475318105fd46d04", size = 4552507, upload-time = "2026-02-28T03:47:05.004Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c0/578a7af4861afb64ec81c03bfdcc1bb3341bb61f2fff8a094ecf13987a56/prek-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fb6395f6eb76133bb1e11fc718db8144522466cdc2e541d05e7813d1bbcae7d", size = 4865929, upload-time = "2026-02-28T03:47:09.231Z" }, - { url = "https://files.pythonhosted.org/packages/fc/48/f169406590028f7698ef2e1ff5bffd92ca05e017636c1163a2f5ef0f8275/prek-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae17813239ddcb4ae7b38418de4d49afff740f48f8e0556029c96f58e350412", size = 5390286, upload-time = "2026-02-28T03:47:10.796Z" }, - { url = "https://files.pythonhosted.org/packages/05/c5/98a73fec052059c3ae06ce105bef67caca42334c56d84e9ef75df72ba152/prek-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a621a690d9c127afc3d21c275030d364d1fbef3296c095068d3ae80a59546e", size = 4891028, upload-time = "2026-02-28T03:47:07.916Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b4/029966e35e59b59c142be7e1d2208ad261709ac1a66aa4a3ce33c5b9f91f/prek-0.3.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d978c31bc3b1f0b3d58895b7c6ac26f077e0ea846da54f46aeee4c7088b1b105", size = 4633986, upload-time = "2026-02-28T03:47:14.351Z" }, - { url = "https://files.pythonhosted.org/packages/1d/27/d122802555745b6940c99fcb41496001c192ddcdf56ec947ec10a0298e05/prek-0.3.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8e089a030f0a023c22a4bb2ec4ff3fcc153585d701cff67acbfca2f37e173ae", size = 4680722, upload-time = "2026-02-28T03:47:12.224Z" }, - { url = "https://files.pythonhosted.org/packages/34/40/92318c96b3a67b4e62ed82741016ede34d97ea9579d3cc1332b167632222/prek-0.3.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8060c72b764f0b88112616763da9dd3a7c293e010f8520b74079893096160a2f", size = 4535623, upload-time = "2026-02-28T03:46:52.221Z" }, - { url = "https://files.pythonhosted.org/packages/df/f5/6b383d94e722637da4926b4f609d36fe432827bb6f035ad46ee02bde66b6/prek-0.3.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:65b23268456b5a763278d4e1ec532f2df33918f13ded85869a1ddff761eb9697", size = 4729879, upload-time = "2026-02-28T03:46:57.886Z" }, - { url = "https://files.pythonhosted.org/packages/79/f8/fdc705b807d813fd713ffa4f67f96741542ed1dafbb221206078c06f3df4/prek-0.3.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3975c61139c7b3200e38dc3955e050b0f2615701d3deb9715696a902e850509e", size = 5001569, upload-time = "2026-02-28T03:47:00.892Z" }, - { url = "https://files.pythonhosted.org/packages/84/92/b007a41f58e8192a1e611a21b396ad870d51d7873b7af12068ebae7fc15f/prek-0.3.4-py3-none-win32.whl", hash = "sha256:37449ae82f4dc08b72e542401e3d7318f05d1163e87c31ab260a40f425d6516e", size = 4297057, upload-time = "2026-02-28T03:47:02.219Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dc/bcb02de9b11461e8e0c7d3c8fdf8cfa15ac6efe73472a4375549ba5defd2/prek-0.3.4-py3-none-win_amd64.whl", hash = "sha256:60e9aa86ca65de963510ae28c5d94b9d7a97bcbaa6e4cdb5bf5083ed4c45dc71", size = 4655174, upload-time = "2026-02-28T03:46:53.749Z" }, - { url = "https://files.pythonhosted.org/packages/0b/86/98f5598569f4cd3de7161e266fab6a8981e65555f79d4704810c1502ad0a/prek-0.3.4-py3-none-win_arm64.whl", hash = "sha256:486bdae8f4512d3b4f6eb61b83e5b7595da2adca385af4b2b7823c0ab38d1827", size = 4367817, upload-time = "2026-02-28T03:46:55.264Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a9/16dd8d3a50362ebccffe58518af1f1f571c96f0695d7fcd8bbd386585f58/prek-0.3.5-py3-none-linux_armv6l.whl", hash = "sha256:44b3e12791805804f286d103682b42a84e0f98a2687faa37045e9d3375d3d73d", size = 5105604, upload-time = "2026-03-09T10:35:00.332Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/bc6036f5bf03860cda66ab040b32737e54802b71a81ec381839deb25df9e/prek-0.3.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3cb451cc51ac068974557491beb4c7d2d41dfde29ed559c1694c8ce23bf53e8", size = 5506155, upload-time = "2026-03-09T10:35:17.64Z" }, + { url = "https://files.pythonhosted.org/packages/02/d9/a3745c2a10509c63b6a118ada766614dd705efefd08f275804d5c807aa4a/prek-0.3.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ad8f5f0d8da53dc94d00b76979af312b3dacccc9dcbc6417756c5dca3633c052", size = 5100383, upload-time = "2026-03-09T10:35:13.302Z" }, + { url = "https://files.pythonhosted.org/packages/43/8e/de965fc515d39309a332789cd3778161f7bc80cde15070bedf17f9f8cb93/prek-0.3.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:4511e15d34072851ac88e4b2006868fbe13655059ad941d7a0ff9ee17138fd9f", size = 5334913, upload-time = "2026-03-09T10:35:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/44f07e8940256059cfd82520e3cbe0764ab06ddb4aa43148465db00b39ad/prek-0.3.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc0b63b8337e2046f51267facaac63ba755bc14aad53991840a5eccba3e5c28", size = 5033825, upload-time = "2026-03-09T10:35:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/94/85/3ff0f96881ff2360c212d310ff23c3cf5a15b223d34fcfa8cdcef203be69/prek-0.3.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5fc0d78c3896a674aeb8247a83bbda7efec85274dbdfbc978ceff8d37e4ed20", size = 5438586, upload-time = "2026-03-09T10:34:58.779Z" }, + { url = "https://files.pythonhosted.org/packages/79/a5/c6d08d31293400fcb5d427f8e7e6bacfc959988e868ad3a9d97b4d87c4b7/prek-0.3.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64cad21cb9072d985179495b77b312f6b81e7b45357d0c68dc1de66e0408eabc", size = 6359714, upload-time = "2026-03-09T10:34:57.454Z" }, + { url = "https://files.pythonhosted.org/packages/ba/18/321dcff9ece8065d42c8c1c7a53a23b45d2b4330aa70993be75dc5f2822f/prek-0.3.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45ee84199bb48e013bdfde0c84352c17a44cc42d5792681b86d94e9474aab6f8", size = 5717632, upload-time = "2026-03-09T10:35:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7f/1288226aa381d0cea403157f4e6b64b356e1a745f2441c31dd9d8a1d63da/prek-0.3.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f43275e5d564e18e52133129ebeb5cb071af7ce4a547766c7f025aa0955dfbb6", size = 5339040, upload-time = "2026-03-09T10:35:03.665Z" }, + { url = "https://files.pythonhosted.org/packages/22/94/cfec83df9c2b8e7ed1608087bcf9538a6a77b4c2e7365123e9e0a3162cd1/prek-0.3.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:abcee520d31522bcbad9311f21326b447694cd5edba33618c25fd023fc9865ec", size = 5162586, upload-time = "2026-03-09T10:35:11.564Z" }, + { url = "https://files.pythonhosted.org/packages/13/b7/741d62132f37a5f7cc0fad1168bd31f20dea9628f482f077f569547e0436/prek-0.3.5-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:499c56a94a155790c75a973d351a33f8065579d9094c93f6d451ada5d1e469be", size = 5002933, upload-time = "2026-03-09T10:35:16.347Z" }, + { url = "https://files.pythonhosted.org/packages/6f/83/630a5671df6550fcfa67c54955e8a8174eb9b4d97ac38fb05a362029245b/prek-0.3.5-py3-none-musllinux_1_1_i686.whl", hash = "sha256:de1065b59f194624adc9dea269d4ff6b50e98a1b5bb662374a9adaa496b3c1eb", size = 5304934, upload-time = "2026-03-09T10:35:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/de/79/67a7afd0c0b6c436630b7dba6e586a42d21d5d6e5778fbd9eba7bbd3dd26/prek-0.3.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a1c4869e45ee341735d07179da3a79fa2afb5959cef8b3c8a71906eb52dc6933", size = 5829914, upload-time = "2026-03-09T10:35:05.39Z" }, + { url = "https://files.pythonhosted.org/packages/37/47/e2fe13b33e7b5fdd9dd1a312f5440208bfe1be6183e54c5c99c10f27d848/prek-0.3.5-py3-none-win32.whl", hash = "sha256:70b2152ecedc58f3f4f69adc884617b0cf44259f7414c44d6268ea6f107672eb", size = 4836910, upload-time = "2026-03-09T10:35:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ab/dc2a139fd4896d11f39631479ed385e86307af7f54059ebe9414bb0d00c6/prek-0.3.5-py3-none-win_amd64.whl", hash = "sha256:01d031b684f7e1546225393af1268d9b4451a44ef6cb9be4101c85c7862e08db", size = 5234234, upload-time = "2026-03-09T10:35:20.193Z" }, + { url = "https://files.pythonhosted.org/packages/ed/38/f7256b4b7581444f658e909c3b566f51bfabe56c03e80d107a6932d62040/prek-0.3.5-py3-none-win_arm64.whl", hash = "sha256:aa774168e3d868039ff79422bdef2df8d5a016ed804a9914607dcdd3d41da053", size = 5083330, upload-time = "2026-03-09T10:34:55.469Z" }, ] [[package]] @@ -1474,15 +1474,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.1.1" +version = "1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/67/09765eacf4e44413c4f8943ba5a317fcb9c7b447c3b8b0b7fce7e3090b0b/python_discovery-1.1.1.tar.gz", hash = "sha256:584c08b141c5b7029f206b4e8b78b1a1764b22121e21519b89dec56936e95b0a", size = 56016, upload-time = "2026-03-07T00:00:56.354Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/16/6f3f5e9258f0733aaca19aa18e298cb3a629ae49363573e78d241abeef59/python_discovery-1.1.2.tar.gz", hash = "sha256:c500bd2153e3afc5f48a61d33ff570b6f3e710d36ceaaf882fa9bbe5cc2cec49", size = 56928, upload-time = "2026-03-09T20:02:28.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/0f/2bf7e3b5a4a65f623cb820feb5793e243fad58ae561015ee15a6152f67a2/python_discovery-1.1.1-py3-none-any.whl", hash = "sha256:69f11073fa2392251e405d4e847d60ffffd25fd762a0dc4d1a7d6b9c3f79f1a3", size = 30732, upload-time = "2026-03-07T00:00:55.143Z" }, + { url = "https://files.pythonhosted.org/packages/03/48/8bdfaec240edb1a79b79201eff38b737fc3c29ce59e2e71271bdd8bafdda/python_discovery-1.1.2-py3-none-any.whl", hash = "sha256:d18edd61b382d62f8bcd004a71ebaabc87df31dbefb30aeed59f4fc6afa005be", size = 31486, upload-time = "2026-03-09T20:02:27.277Z" }, ] [[package]] @@ -2097,7 +2097,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.1.0" +version = "21.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -2106,9 +2106,9 @@ dependencies = [ { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, + { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, ] [[package]] From 3702d5e840f9d7d8582fd076d8e0a4caa3f9698a Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Mon, 9 Mar 2026 23:17:37 +0100 Subject: [PATCH 075/280] Add release notes for airflowctl 0.1.3 (#63229) --- airflow-ctl/RELEASE_NOTES.rst | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/airflow-ctl/RELEASE_NOTES.rst b/airflow-ctl/RELEASE_NOTES.rst index 164d9bef98070..acbb4afe80739 100644 --- a/airflow-ctl/RELEASE_NOTES.rst +++ b/airflow-ctl/RELEASE_NOTES.rst @@ -15,6 +15,41 @@ 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) + +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) ----------------------------- From 9b217b6044ae1aaf2beedc7d8617b116f60b8673 Mon Sep 17 00:00:00 2001 From: Xiaodong DENG Date: Mon, 9 Mar 2026 15:19:26 -0700 Subject: [PATCH 076/280] Chart: Support Helm template expressions in podAnnotations (#63019) * Support Helm template expressions in podAnnotations Enable tpl evaluation for `podAnnotations` and `airflowPodAnnotations` across all chart templates, allowing users to use Helm template expressions such as `{{ .Release.Name }}` or `{{ include ... | sha256sum }}` in annotation values. This is useful for adding checksum annotations that trigger pod restarts when dependent ConfigMaps or Secrets change. closes: #62698 * Rename chart newsfragment to PR number 63019 * Fix doc nits in customizing-labels: trim underlines and clarify api-server/webserver wording * Address doc structure: add Customizing Pod Labels subsection under umbrella heading --- chart/docs/customizing-labels.rst | 74 ++++++++- .../pod-template-file.kubernetes-helm-yaml | 2 +- chart/newsfragments/63019.feature.rst | 1 + .../api-server/api-server-deployment.yaml | 4 +- chart/templates/cleanup/cleanup-cronjob.yaml | 4 +- .../dag-processor-deployment.yaml | 4 +- .../database-cleanup-cronjob.yaml | 4 +- chart/templates/flower/flower-deployment.yaml | 2 +- chart/templates/jobs/create-user-job.yaml | 4 +- .../templates/jobs/migrate-database-job.yaml | 4 +- .../pgbouncer/pgbouncer-deployment.yaml | 2 +- chart/templates/redis/redis-statefulset.yaml | 2 +- .../scheduler/scheduler-deployment.yaml | 4 +- chart/templates/statsd/statsd-deployment.yaml | 2 +- .../triggerer/triggerer-deployment.yaml | 4 +- .../webserver/webserver-deployment.yaml | 4 +- .../templates/workers/worker-deployment.yaml | 2 +- chart/values.schema.json | 30 ++-- chart/values.yaml | 19 ++- .../airflow_aux/test_annotations.py | 150 ++++++++++++++++++ 20 files changed, 275 insertions(+), 47 deletions(-) create mode 100644 chart/newsfragments/63019.feature.rst 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..25c2de6f81318 100644 --- a/chart/files/pod-template-file.kubernetes-helm-yaml +++ b/chart/files/pod-template-file.kubernetes-helm-yaml @@ -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 }} 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/templates/api-server/api-server-deployment.yaml b/chart/templates/api-server/api-server-deployment.yaml index 0d3d8c0cdd38c..4de80ded6ca3d 100644 --- a/chart/templates/api-server/api-server-deployment.yaml +++ b/chart/templates/api-server/api-server-deployment.yaml @@ -91,10 +91,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/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..c5045e6ecefea 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 }} 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/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..3514180c874c5 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 }} 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..41a2f0d3d5501 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 }} 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..c810581bf7485 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 }} diff --git a/chart/values.schema.json b/chart/values.schema.json index 5a8dfcd9123a1..0891db13bd5f7 100644 --- a/chart/values.schema.json +++ b/chart/values.schema.json @@ -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", @@ -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": { @@ -3899,7 +3899,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 +4467,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 +5079,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 +5388,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 +5737,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": { @@ -6632,7 +6632,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 +7459,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 +8000,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 +8419,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 +8623,7 @@ } }, "podAnnotations": { - "description": "Add annotations for the PgBouncer Pod.", + "description": "Add annotations for the PgBouncer Pod (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -9462,7 +9462,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 +9838,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 +10191,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": { diff --git a/chart/values.yaml b/chart/values.yaml index bab02bdef30c0..15fa5aeefdb4b 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -338,7 +338,7 @@ networkPolicies: enabled: false # Extra annotations to apply to all -# Airflow pods +# Airflow pods (templated) airflowPodAnnotations: {} # Extra annotations to apply to @@ -973,7 +973,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 @@ -1457,6 +1457,7 @@ scheduler: # annotations for scheduler deployment annotations: {} + # Pod annotations for scheduler pods (templated) podAnnotations: {} # Labels specific to scheduler objects and pods @@ -1541,7 +1542,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 +1637,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: {} @@ -1852,6 +1853,7 @@ apiServer: # annotations for Airflow API server deployment annotations: {} + # Pod annotations for API server pods (templated) podAnnotations: {} networkPolicy: @@ -2136,6 +2138,7 @@ webserver: # annotations for webserver deployment annotations: {} + # Pod annotations for webserver pods (templated) podAnnotations: {} # Labels specific webserver app @@ -2299,6 +2302,7 @@ triggerer: # annotations for the triggerer deployment annotations: {} + # Pod annotations for triggerer pods (templated) podAnnotations: {} # Labels specific to triggerer objects and pods @@ -2532,6 +2536,7 @@ dagProcessor: # annotations for the dag processor deployment annotations: {} + # Pod annotations for dag processor pods (templated) podAnnotations: {} logGroomerSidecar: @@ -2716,6 +2721,7 @@ flower: # annotations for the flower deployment annotations: {} + # Pod annotations for flower pods (templated) podAnnotations: {} # Labels specific to flower objects and pods @@ -2833,6 +2839,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 +2869,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 +3167,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 +3266,7 @@ cleanup: topologySpreadConstraints: [] priorityClassName: ~ + # Pod annotations for cleanup pods (templated) podAnnotations: {} # Labels specific to cleanup objects and pods @@ -3346,6 +3356,7 @@ databaseCleanup: topologySpreadConstraints: [] priorityClassName: ~ + # Pod annotations for database cleanup pods (templated) podAnnotations: {} # Labels specific to database cleanup objects and pods diff --git a/helm-tests/tests/helm_tests/airflow_aux/test_annotations.py b/helm-tests/tests/helm_tests/airflow_aux/test_annotations.py index 1a2ae85ccd4e3..3df818effa7c2 100644 --- a/helm-tests/tests/helm_tests/airflow_aux/test_annotations.py +++ b/helm-tests/tests/helm_tests/airflow_aux/test_annotations.py @@ -16,7 +16,11 @@ # under the License. from __future__ import annotations +import copy + +import jmespath import pytest +import yaml from chart_utils.helm_template_generator import render_chart @@ -479,6 +483,42 @@ def test_precedence(self, values, show_only, expected_annotations): assert k in annotations assert v == annotations[k] + def test_pod_annotations_are_templated(self, values, show_only, expected_annotations): + templated_values = copy.deepcopy(values) + for val in templated_values.values(): + if isinstance(val, dict) and "podAnnotations" in val: + val["podAnnotations"] = {"release-name": "{{ .Release.Name }}"} + + k8s_objects = render_chart( + values=templated_values, + show_only=[show_only], + ) + + assert len(k8s_objects) == 1 + annotations = get_object_annotations(k8s_objects[0]) + assert annotations["release-name"] == "release-name" + + def test_airflow_pod_annotations_are_templated(self, values, show_only, expected_annotations): + templated_values = copy.deepcopy(values) + templated_values["airflowPodAnnotations"] = {"global-release": "{{ .Release.Name }}"} + + k8s_objects = render_chart( + values=templated_values, + show_only=[show_only], + ) + + assert len(k8s_objects) == 1 + annotations = get_object_annotations(k8s_objects[0]) + # pgbouncer, statsd, and redis do not render airflowPodAnnotations + if "global-release" in annotations: + assert annotations["global-release"] == "release-name" + else: + assert show_only in ( + "templates/pgbouncer/pgbouncer-deployment.yaml", + "templates/statsd/statsd-deployment.yaml", + "templates/redis/redis-statefulset.yaml", + ) + class TestRedisAnnotations: """Tests Redis Annotations.""" @@ -504,3 +544,113 @@ def test_redis_annotations_are_added(self): for k, v in expected_annotations.items(): assert k in obj["metadata"]["annotations"] assert v == obj["metadata"]["annotations"][k] + + +class TestPodTemplateFileAnnotationsTemplating: + """Tests that podAnnotations are templated in the pod template file.""" + + def test_pod_template_file_annotations_are_templated(self): + k8s_objects = render_chart( + values={ + "executor": "KubernetesExecutor", + "workers": { + "podAnnotations": { + "release-name": "{{ .Release.Name }}", + }, + }, + }, + show_only=["templates/configmaps/configmap.yaml"], + ) + + assert len(k8s_objects) == 1 + pod_template = k8s_objects[0]["data"]["pod_template_file.yaml"] + annotations = jmespath.search( + "metadata.annotations", + yaml.safe_load(pod_template), + ) + assert annotations["release-name"] == "release-name" + + def test_pod_template_file_global_annotations_are_templated(self): + k8s_objects = render_chart( + values={ + "executor": "KubernetesExecutor", + "airflowPodAnnotations": { + "global-release": "{{ .Release.Name }}", + }, + }, + show_only=["templates/configmaps/configmap.yaml"], + ) + + assert len(k8s_objects) == 1 + pod_template = k8s_objects[0]["data"]["pod_template_file.yaml"] + annotations = jmespath.search( + "metadata.annotations", + yaml.safe_load(pod_template), + ) + assert annotations["global-release"] == "release-name" + + +class TestWebserverPodAnnotationsTemplating: + """Tests webserver podAnnotations templating (requires airflowVersion < 3.0.0).""" + + def test_webserver_pod_annotations_are_templated(self): + k8s_objects = render_chart( + values={ + "airflowVersion": "2.11.0", + "webserver": { + "podAnnotations": { + "release-name": "{{ .Release.Name }}", + }, + }, + }, + show_only=["templates/webserver/webserver-deployment.yaml"], + ) + + assert len(k8s_objects) == 1 + annotations = get_object_annotations(k8s_objects[0]) + assert annotations["release-name"] == "release-name" + + def test_webserver_airflow_pod_annotations_are_templated(self): + k8s_objects = render_chart( + values={ + "airflowVersion": "2.11.0", + "airflowPodAnnotations": { + "global-release": "{{ .Release.Name }}", + }, + }, + show_only=["templates/webserver/webserver-deployment.yaml"], + ) + + assert len(k8s_objects) == 1 + annotations = get_object_annotations(k8s_objects[0]) + assert annotations["global-release"] == "release-name" + + +class TestJobAnnotationsTemplating: + """Tests that annotations are templated in job templates.""" + + @pytest.mark.parametrize( + ("values", "show_only"), + [ + ( + {"createUserJob": {"annotations": {"job-ann": "{{ .Release.Name }}"}}}, + "templates/jobs/create-user-job.yaml", + ), + ( + {"migrateDatabaseJob": {"annotations": {"job-ann": "{{ .Release.Name }}"}}}, + "templates/jobs/migrate-database-job.yaml", + ), + ], + ) + def test_job_annotations_are_templated(self, values, show_only): + templated_values = copy.deepcopy(values) + templated_values["airflowPodAnnotations"] = {"global-ann": "{{ .Release.Name }}"} + k8s_objects = render_chart( + values=templated_values, + show_only=[show_only], + ) + + assert len(k8s_objects) == 1 + annotations = k8s_objects[0]["spec"]["template"]["metadata"]["annotations"] + assert annotations["global-ann"] == "release-name" + assert annotations["job-ann"] == "release-name" From 8057e4db79a32a73d4b37d1780e1e054d3df59e1 Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Mon, 9 Mar 2026 22:21:19 +0000 Subject: [PATCH 077/280] Remove unnecessary `id-token: write` from registry workflow (#63232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The registry build job uses static AWS credentials (access key + secret), not OIDC, so `id-token: write` is not needed. Removing it fixes the `workflow_call` from `publish-docs-to-s3.yml` which only grants `contents: read` — callers cannot escalate permissions for nested jobs. --- .github/workflows/registry-build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/registry-build.yml b/.github/workflows/registry-build.yml index a5d92fa955ac6..094820c539f8b 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('[ From f4ae1cdd78af13193d5751080a586532f186aa2b Mon Sep 17 00:00:00 2001 From: Dheeraj Turaga Date: Mon, 9 Mar 2026 18:18:41 -0500 Subject: [PATCH 078/280] Add real-time concurrency control for edge workers via UI (#63142) * feat(edge3): Add real-time concurrency control for edge workers via UI Previously, operators had no way to dynamically adjust an edge worker's concurrency limit without SSH access and CLI intervention, creating an operational bottleneck in distributed task execution environments. This contribution adds a new REST API endpoint (PATCH /worker/{worker_name}/concurrency) and a React UI control to the Apache Airflow Edge Provider, enabling administrators to tune worker throughput in real time from the Airflow web interface. The change is propagated to the worker process on its next heartbeat cycle without requiring a restart. * Add back what prek removed --- .../plugins/www/openapi-gen/queries/common.ts | 1 + .../www/openapi-gen/queries/queries.ts | 9 +- .../www/openapi-gen/requests/schemas.gen.ts | 15 ++ .../www/openapi-gen/requests/services.gen.ts | 28 +++- .../www/openapi-gen/requests/types.gen.ts | 29 ++++ .../components/WorkerConcurrencyButton.tsx | 157 ++++++++++++++++++ .../www/src/components/WorkerOperations.tsx | 2 + .../edge3/worker_api/datamodels_ui.py | 6 + .../providers/edge3/worker_api/routes/ui.py | 25 +++ .../edge3/worker_api/v2-edge-generated.yaml | 47 ++++++ .../unit/edge3/worker_api/routes/test_ui.py | 27 +++ 11 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerConcurrencyButton.tsx diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts index 4d553521aea43..2b90edb3b00fc 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts @@ -50,3 +50,4 @@ export type UiServiceUpdateWorkerMaintenanceMutationResult = Awaited>; export type UiServiceDeleteWorkerMutationResult = Awaited>; export type UiServiceRemoveWorkerQueueMutationResult = Awaited>; +export type UiServiceSetWorkerConcurrencyLimitMutationResult = Awaited>; diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts index 782a26bbf9114..83cae99c134b1 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts @@ -2,7 +2,7 @@ import { UseMutationOptions, UseQueryOptions, useMutation, useQuery } from "@tanstack/react-query"; import { JobsService, LogsService, MonitorService, UiService, WorkerService } from "../requests/services.gen"; -import { EdgeWorkerState, MaintenanceRequest, PushLogsBody, TaskInstanceState, WorkerQueueUpdateBody, WorkerQueuesBody, WorkerStateBody } from "../requests/types.gen"; +import { ConcurrencyRequest, EdgeWorkerState, MaintenanceRequest, PushLogsBody, TaskInstanceState, WorkerQueueUpdateBody, WorkerQueuesBody, WorkerStateBody } from "../requests/types.gen"; import * as Common from "./common"; export const useLogsServiceLogfilePath = = unknown[]>({ authorization, dagId, mapIndex, runId, taskId, tryNumber }: { authorization: string; @@ -139,3 +139,10 @@ export const useUiServiceRemoveWorkerQueue = ({ mutationFn: ({ queueName, workerName }) => UiService.removeWorkerQueue({ queueName, workerName }) as unknown as Promise, ...options }); +export const useUiServiceSetWorkerConcurrencyLimit = (options?: Omit, "mutationFn">) => useMutation({ mutationFn: ({ requestBody, workerName }) => UiService.setWorkerConcurrencyLimit({ requestBody, workerName }) as unknown as Promise, ...options }); diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts index 11e48528f69e6..51756edfb4452 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts @@ -257,6 +257,21 @@ export const $JobCollectionResponse = { description: 'Job Collection serializer.' } as const; +export const $ConcurrencyRequest = { + properties: { + concurrency: { + type: 'integer', + exclusiveMinimum: 0, + title: 'Concurrency', + description: 'New concurrency limit for the worker.' + } + }, + type: 'object', + required: ['concurrency'], + title: 'ConcurrencyRequest', + description: 'Request body for worker concurrency update.' +} as const; + export const $MaintenanceRequest = { properties: { maintenance_comment: { diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts index da00585e04498..2dd062ec1ff56 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/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 { FetchData, FetchResponse, StateData, StateResponse, LogfilePathData, LogfilePathResponse, PushLogsData, PushLogsResponse, RegisterData, RegisterResponse, SetStateData, SetStateResponse, UpdateQueuesData, UpdateQueuesResponse, HealthResponse, WorkerData, WorkerResponse, JobsResponse, RequestWorkerMaintenanceData, RequestWorkerMaintenanceResponse, UpdateWorkerMaintenanceData, UpdateWorkerMaintenanceResponse, ExitWorkerMaintenanceData, ExitWorkerMaintenanceResponse, RequestWorkerShutdownData, RequestWorkerShutdownResponse, DeleteWorkerData, DeleteWorkerResponse, AddWorkerQueueData, AddWorkerQueueResponse, RemoveWorkerQueueData, RemoveWorkerQueueResponse } from './types.gen'; +import type { FetchData, FetchResponse, StateData, StateResponse, LogfilePathData, LogfilePathResponse, PushLogsData, PushLogsResponse, RegisterData, RegisterResponse, SetStateData, SetStateResponse, UpdateQueuesData, UpdateQueuesResponse, HealthResponse, WorkerData, WorkerResponse, JobsResponse, RequestWorkerMaintenanceData, RequestWorkerMaintenanceResponse, UpdateWorkerMaintenanceData, UpdateWorkerMaintenanceResponse, ExitWorkerMaintenanceData, ExitWorkerMaintenanceResponse, RequestWorkerShutdownData, RequestWorkerShutdownResponse, DeleteWorkerData, DeleteWorkerResponse, AddWorkerQueueData, AddWorkerQueueResponse, RemoveWorkerQueueData, RemoveWorkerQueueResponse, SetWorkerConcurrencyLimitData, SetWorkerConcurrencyLimitResponse } from './types.gen'; export class JobsService { /** @@ -472,5 +472,29 @@ export class UiService { } }); } - + + /** + * Set Worker Concurrency Limit + * Set the concurrency limit for an edge worker. + * @param data The data for the request. + * @param data.workerName + * @param data.requestBody + * @returns unknown Successful Response + * @throws ApiError + */ + public static setWorkerConcurrencyLimit(data: SetWorkerConcurrencyLimitData): CancelablePromise { + return __request(OpenAPI, { + method: 'PATCH', + url: '/edge_worker/ui/worker/{worker_name}/concurrency', + path: { + worker_name: data.workerName + }, + body: data.requestBody, + mediaType: 'application/json', + errors: { + 422: 'Validation Error' + } + }); + } + } \ No newline at end of file diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts index 12e2a71fd645b..4ac26c50b1a40 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts @@ -523,6 +523,20 @@ export type RemoveWorkerQueueData = { export type RemoveWorkerQueueResponse = unknown; +export type ConcurrencyRequest = { + /** + * New concurrency limit for the worker. + */ + concurrency: number; +}; + +export type SetWorkerConcurrencyLimitData = { + workerName: string; + requestBody: ConcurrencyRequest; +}; + +export type SetWorkerConcurrencyLimitResponse = unknown; + export type $OpenApiTs = { '/edge_worker/v1/jobs/fetch/{worker_name}': { post: { @@ -831,4 +845,19 @@ export type $OpenApiTs = { }; }; }; + '/edge_worker/ui/worker/{worker_name}/concurrency': { + patch: { + req: SetWorkerConcurrencyLimitData; + res: { + /** + * Successful Response + */ + 200: unknown; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; }; \ No newline at end of file diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerConcurrencyButton.tsx b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerConcurrencyButton.tsx new file mode 100644 index 0000000000000..90b8404cb8e50 --- /dev/null +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerConcurrencyButton.tsx @@ -0,0 +1,157 @@ +/*! + * 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 { + Button, + CloseButton, + Dialog, + IconButton, + Input, + Portal, + Text, + VStack, + useDisclosure, +} from "@chakra-ui/react"; +import { useUiServiceSetWorkerConcurrencyLimit } from "openapi/queries"; +import type { Worker } from "openapi/requests/types.gen"; +import { useState } from "react"; +import { LuSlidersHorizontal } from "react-icons/lu"; + +interface WorkerConcurrencyButtonProps { + onConcurrencyUpdate: (toast: Record) => void; + worker: Worker; +} + +export const WorkerConcurrencyButton = ({ + onConcurrencyUpdate, + worker, +}: WorkerConcurrencyButtonProps) => { + const { onClose, onOpen, open } = useDisclosure(); + const workerName = worker.worker_name; + const currentConcurrency = worker.sysinfo?.concurrency; + const [concurrency, setConcurrency] = useState( + currentConcurrency !== undefined ? String(currentConcurrency) : "", + ); + + const setConcurrencyMutation = useUiServiceSetWorkerConcurrencyLimit({ + onError: (error: unknown) => { + onConcurrencyUpdate({ + description: `Unable to set concurrency for worker ${workerName}: ${error}`, + title: "Set Concurrency Failed", + type: "error", + }); + }, + onSuccess: () => { + onConcurrencyUpdate({ + description: `Concurrency for worker ${workerName} set to ${concurrency}.`, + title: "Concurrency Updated", + type: "success", + }); + onClose(); + }, + }); + + const handleSetConcurrency = () => { + const value = parseInt(concurrency, 10); + + if (!concurrency.trim() || isNaN(value) || value <= 0) { + onConcurrencyUpdate({ + description: "Please enter a valid concurrency value greater than 0.", + title: "Invalid Input", + type: "error", + }); + return; + } + + setConcurrencyMutation.mutate({ + requestBody: { concurrency: value }, + workerName, + }); + }; + + const handleOpen = () => { + setConcurrency(currentConcurrency !== undefined ? String(currentConcurrency) : ""); + onOpen(); + }; + + const concurrencyValue = parseInt(concurrency, 10); + const isValid = concurrency.trim() !== "" && !isNaN(concurrencyValue) && concurrencyValue > 0; + + return ( + <> + + + + + + + + + + + Set Concurrency for {workerName} + + + + Enter the new concurrency limit for this worker: + + + + + + + + + + + + + + + + + ); +}; diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx index 0d0aa727eea73..1b5322ce08bf5 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx @@ -26,6 +26,7 @@ import { MaintenanceEditCommentButton } from "./MaintenanceEditCommentButton"; import { MaintenanceEnterButton } from "./MaintenanceEnterButton"; import { MaintenanceExitButton } from "./MaintenanceExitButton"; import { RemoveQueueButton } from "./RemoveQueueButton"; +import { WorkerConcurrencyButton } from "./WorkerConcurrencyButton"; import { WorkerDeleteButton } from "./WorkerDeleteButton"; import { WorkerShutdownButton } from "./WorkerShutdownButton"; @@ -48,6 +49,7 @@ export const WorkerOperations = ({ onOperations, worker }: WorkerOperationsProps + diff --git a/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels_ui.py b/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels_ui.py index 3aa2e12e9dfed..e38671928a60a 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels_ui.py +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels_ui.py @@ -77,3 +77,9 @@ class QueueUpdateRequest(BaseModel): """Request body for queue operations.""" queue_name: Annotated[str, Field(description="Name of the queue to add or remove.")] + + +class ConcurrencyRequest(BaseModel): + """Request body for worker concurrency update.""" + + concurrency: Annotated[int, Field(description="New concurrency limit for the worker.", gt=0)] diff --git a/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py index f24b1e19729fa..996a3a261283a 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py @@ -38,8 +38,10 @@ remove_worker_queues, request_maintenance, request_shutdown, + set_worker_concurrency, ) from airflow.providers.edge3.worker_api.datamodels_ui import ( + ConcurrencyRequest, Job, JobCollectionResponse, MaintenanceRequest, @@ -325,3 +327,26 @@ def remove_worker_queue( remove_worker_queues(worker_name, [queue_name], session=session) except Exception as e: raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(e)) + + +@ui_router.patch( + "/worker/{worker_name}/concurrency", + dependencies=[ + Depends(requires_access_view(access_view=AccessView.JOBS)), + ], +) +def set_worker_concurrency_limit( + worker_name: str, + concurrency_request: ConcurrencyRequest, + session: SessionDep, +) -> None: + """Set the concurrency limit for an edge worker.""" + worker_query = select(EdgeWorkerModel).where(EdgeWorkerModel.worker_name == worker_name) + worker = session.scalar(worker_query) + if not worker: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f"Worker {worker_name} not found") + + try: + set_worker_concurrency(worker_name, concurrency_request.concurrency, session=session) + except Exception as e: + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(e)) diff --git a/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml b/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml index a5daeacb86fbd..2904a2e0d3d5d 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml @@ -912,6 +912,41 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /edge_worker/ui/worker/{worker_name}/concurrency: + patch: + tags: + - UI + summary: Set Worker Concurrency Limit + description: Set the concurrency limit for an edge worker. + operationId: set_worker_concurrency_limit + security: + - OAuth2PasswordBearer: [] + - HTTPBearer: [] + parameters: + - name: worker_name + in: path + required: true + schema: + type: string + title: Worker Name + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ConcurrencyRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' components: schemas: BundleInfo: @@ -929,6 +964,18 @@ components: - name title: BundleInfo description: Schema for telling task which bundle to run with. + ConcurrencyRequest: + properties: + concurrency: + type: integer + exclusiveMinimum: 0.0 + title: Concurrency + description: New concurrency limit for the worker. + type: object + required: + - concurrency + title: ConcurrencyRequest + description: Request body for worker concurrency update. EdgeJobFetched: properties: dag_id: diff --git a/providers/edge3/tests/unit/edge3/worker_api/routes/test_ui.py b/providers/edge3/tests/unit/edge3/worker_api/routes/test_ui.py index f1934b5d0f6dd..256cdd977d396 100644 --- a/providers/edge3/tests/unit/edge3/worker_api/routes/test_ui.py +++ b/providers/edge3/tests/unit/edge3/worker_api/routes/test_ui.py @@ -47,3 +47,30 @@ def test_worker(self, session: Session): assert worker_response.total_entries == 1 assert len(worker_response.workers) == 1 assert worker_response.workers[0].worker_name == "worker1" + + def test_set_worker_concurrency_limit(self, session: Session): + from airflow.providers.edge3.worker_api.datamodels_ui import ConcurrencyRequest + from airflow.providers.edge3.worker_api.routes.ui import set_worker_concurrency_limit + + set_worker_concurrency_limit( + worker_name="worker1", + concurrency_request=ConcurrencyRequest(concurrency=4), + session=session, + ) + worker_model = session.get(EdgeWorkerModel, "worker1") + assert worker_model is not None + assert worker_model.concurrency == 4 + + def test_set_worker_concurrency_limit_not_found(self, session: Session): + from fastapi import HTTPException + + from airflow.providers.edge3.worker_api.datamodels_ui import ConcurrencyRequest + from airflow.providers.edge3.worker_api.routes.ui import set_worker_concurrency_limit + + with pytest.raises(HTTPException) as exc_info: + set_worker_concurrency_limit( + worker_name="nonexistent_worker", + concurrency_request=ConcurrencyRequest(concurrency=4), + session=session, + ) + assert exc_info.value.status_code == 404 From 91072bcebb952d6cb702930f07b91ae394547f24 Mon Sep 17 00:00:00 2001 From: Subham Date: Tue, 10 Mar 2026 04:50:32 +0530 Subject: [PATCH 079/280] Fix grid view URL for dynamic task groups (#63205) Dynamic task groups with isMapped=true were getting /mapped appended to their URL in the grid view, producing URLs like /tasks/group/{groupId}/mapped which has no matching route (404). The graph view correctly handles this by not appending /mapped for groups. This fix adds the same guard to buildTaskInstanceUrl. closes: #63197 --- airflow-core/newsfragments/63205.bugfix.rst | 1 + .../src/airflow/ui/src/utils/links.test.ts | 19 +++++++++++++++++-- .../src/airflow/ui/src/utils/links.ts | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 airflow-core/newsfragments/63205.bugfix.rst 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/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}`; From 11246c7a72c91a7497b3d00f210134d21876f010 Mon Sep 17 00:00:00 2001 From: Yoann <60654707+YoannAbriel@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:35:23 -0700 Subject: [PATCH 080/280] fix(providers/oracle): use conn.schema as service_name fallback in OracleHook (#62895) * fix(providers/oracle): use conn.schema as service_name fallback in OracleHook When creating an Oracle connection via the UI with Host, Port, and Schema fields filled but without explicitly setting service_name in extras, get_conn() built the DSN without a service name, causing TNS errors. Now conn.schema is used as the service_name when neither service_name nor sid is set in connection extras. Fixes apache/airflow#62526 * ci: retrigger CI (unrelated static check failures) * fix: remove thick_mode from schema_as_service_name test to avoid CI Oracle client dependency --- .../airflow/providers/oracle/hooks/oracle.py | 4 +++ .../tests/unit/oracle/hooks/test_oracle.py | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/providers/oracle/src/airflow/providers/oracle/hooks/oracle.py b/providers/oracle/src/airflow/providers/oracle/hooks/oracle.py index 6bd444f460aa9..97721224c0b7e 100644 --- a/providers/oracle/src/airflow/providers/oracle/hooks/oracle.py +++ b/providers/oracle/src/airflow/providers/oracle/hooks/oracle.py @@ -216,6 +216,10 @@ def get_conn(self) -> oracledb.Connection: # Set up DSN service_name = conn.extra_dejson.get("service_name") + # Fall back to conn.schema as service_name when not explicitly set in extras. + # The UI Schema field maps to conn.schema which is the Oracle service name. + if not service_name and not sid and schema: + service_name = schema port = conn.port if conn.port else DEFAULT_DB_PORT if conn.host and sid and not service_name: conn_config["dsn"] = oracledb.makedsn(conn.host, port, sid) diff --git a/providers/oracle/tests/unit/oracle/hooks/test_oracle.py b/providers/oracle/tests/unit/oracle/hooks/test_oracle.py index 2810b698acce5..e007361a26785 100644 --- a/providers/oracle/tests/unit/oracle/hooks/test_oracle.py +++ b/providers/oracle/tests/unit/oracle/hooks/test_oracle.py @@ -141,6 +141,36 @@ def test_get_conn_expire_time(self, mock_connect): assert args == () assert kwargs["expire_time"] == 10 + @mock.patch("airflow.providers.oracle.hooks.oracle.oracledb.connect") + def test_get_conn_schema_as_service_name(self, mock_connect): + """When service_name and sid are not in extras, conn.schema should be used as service_name.""" + self.connection.schema = "MY_SERVICE" + self.connection.extra = json.dumps({}) + self.db_hook.get_conn() + assert mock_connect.call_count == 1 + args, kwargs = mock_connect.call_args + assert kwargs["dsn"] == oracledb.makedsn("host", 1521, service_name="MY_SERVICE") + + @mock.patch("airflow.providers.oracle.hooks.oracle.oracledb.connect") + def test_get_conn_schema_not_used_when_service_name_set(self, mock_connect): + """Explicit service_name in extras takes precedence over conn.schema.""" + self.connection.schema = "MY_SCHEMA" + self.connection.extra = json.dumps({"service_name": "EXPLICIT_SVC"}) + self.db_hook.get_conn() + assert mock_connect.call_count == 1 + args, kwargs = mock_connect.call_args + assert kwargs["dsn"] == oracledb.makedsn("host", 1521, service_name="EXPLICIT_SVC") + + @mock.patch("airflow.providers.oracle.hooks.oracle.oracledb.connect") + def test_get_conn_schema_not_used_when_sid_set(self, mock_connect): + """Explicit sid in extras takes precedence over conn.schema.""" + self.connection.schema = "MY_SCHEMA" + self.connection.extra = json.dumps({"sid": "MY_SID"}) + self.db_hook.get_conn() + assert mock_connect.call_count == 1 + args, kwargs = mock_connect.call_args + assert kwargs["dsn"] == oracledb.makedsn("host", 1521, "MY_SID") + @mock.patch("airflow.providers.oracle.hooks.oracle.oracledb.connect") def test_set_current_schema(self, mock_connect): self.connection.schema = "schema_name" From 406bfc46a5f2e44bf6839bcdc5ef6bc570d5ab13 Mon Sep 17 00:00:00 2001 From: Vincent <97131062+vincbeck@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:11:29 -0400 Subject: [PATCH 081/280] Prepare providers release 2026-03-09 (#63198) --- providers/.last_release_date.txt | 2 +- providers/airbyte/README.rst | 8 ++-- providers/airbyte/docs/changelog.rst | 14 ++++++ providers/airbyte/docs/index.rst | 6 +-- providers/airbyte/provider.yaml | 3 +- providers/airbyte/pyproject.toml | 6 +-- .../src/airflow/providers/airbyte/__init__.py | 2 +- providers/alibaba/README.rst | 6 +-- providers/alibaba/docs/changelog.rst | 13 ++++++ providers/alibaba/docs/index.rst | 6 +-- providers/alibaba/provider.yaml | 3 +- providers/alibaba/pyproject.toml | 6 +-- .../src/airflow/providers/alibaba/__init__.py | 2 +- providers/amazon/README.rst | 6 +-- providers/amazon/docs/changelog.rst | 27 ++++++++++++ providers/amazon/docs/index.rst | 6 +-- providers/amazon/provider.yaml | 3 +- providers/amazon/pyproject.toml | 6 +-- .../src/airflow/providers/amazon/__init__.py | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- providers/apache/flink/README.rst | 14 +++--- providers/apache/flink/docs/changelog.rst | 13 ++++++ providers/apache/flink/docs/index.rst | 6 +-- providers/apache/flink/provider.yaml | 3 +- providers/apache/flink/pyproject.toml | 6 +-- .../providers/apache/flink/__init__.py | 2 +- .../hdfs/docs/.latest-doc-only-change.txt | 2 +- providers/apache/hive/README.rst | 6 +-- providers/apache/hive/docs/changelog.rst | 17 +++++++ providers/apache/hive/docs/index.rst | 6 +-- providers/apache/hive/provider.yaml | 3 +- providers/apache/hive/pyproject.toml | 6 +-- .../airflow/providers/apache/hive/__init__.py | 2 +- providers/apache/iceberg/README.rst | 27 +++++------- providers/apache/iceberg/docs/changelog.rst | 18 ++++++++ .../impala/docs/.latest-doc-only-change.txt | 2 +- providers/apache/kafka/README.rst | 6 +-- providers/apache/kafka/docs/changelog.rst | 13 ++++++ providers/apache/kafka/docs/index.rst | 6 +-- providers/apache/kafka/provider.yaml | 3 +- providers/apache/kafka/pyproject.toml | 6 +-- .../providers/apache/kafka/__init__.py | 2 +- .../kylin/docs/.latest-doc-only-change.txt | 2 +- .../pig/docs/.latest-doc-only-change.txt | 2 +- providers/apache/spark/README.rst | 6 +-- providers/apache/spark/docs/changelog.rst | 11 +++++ providers/apache/spark/docs/index.rst | 6 +-- providers/apache/spark/provider.yaml | 3 +- providers/apache/spark/pyproject.toml | 6 +-- .../providers/apache/spark/__init__.py | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- .../apprise/docs/.latest-doc-only-change.txt | 2 +- .../arangodb/docs/.latest-doc-only-change.txt | 2 +- .../asana/docs/.latest-doc-only-change.txt | 2 +- .../jira/docs/.latest-doc-only-change.txt | 2 +- providers/celery/README.rst | 6 +-- providers/celery/docs/changelog.rst | 11 +++++ providers/celery/docs/index.rst | 6 +-- providers/celery/provider.yaml | 3 +- providers/celery/pyproject.toml | 6 +-- .../src/airflow/providers/celery/__init__.py | 2 +- .../cloudant/docs/.latest-doc-only-change.txt | 2 +- providers/cncf/kubernetes/README.rst | 10 ++--- providers/cncf/kubernetes/docs/changelog.rst | 23 ++++++++++ providers/cncf/kubernetes/docs/index.rst | 8 ++-- providers/cncf/kubernetes/provider.yaml | 3 +- providers/cncf/kubernetes/pyproject.toml | 8 ++-- .../providers/cncf/kubernetes/__init__.py | 2 +- providers/cohere/README.rst | 6 +-- providers/cohere/docs/changelog.rst | 13 ++++++ providers/cohere/docs/index.rst | 6 +-- providers/cohere/provider.yaml | 3 +- providers/cohere/pyproject.toml | 6 +-- .../src/airflow/providers/cohere/__init__.py | 2 +- providers/common/ai/docs/index.rst | 4 +- providers/common/ai/pyproject.toml | 4 +- providers/common/compat/README.rst | 6 +-- providers/common/compat/docs/changelog.rst | 13 ++++++ providers/common/compat/docs/index.rst | 6 +-- providers/common/compat/provider.yaml | 3 +- providers/common/compat/pyproject.toml | 6 +-- .../providers/common/compat/__init__.py | 2 +- .../io/docs/.latest-doc-only-change.txt | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- providers/common/sql/README.rst | 44 +++++++++++-------- providers/common/sql/docs/changelog.rst | 28 ++++++++++++ providers/common/sql/docs/index.rst | 8 ++-- providers/common/sql/provider.yaml | 3 +- providers/common/sql/pyproject.toml | 8 ++-- .../airflow/providers/common/sql/__init__.py | 2 +- providers/databricks/README.rst | 6 +-- providers/databricks/docs/changelog.rst | 21 +++++++++ providers/databricks/docs/index.rst | 6 +-- providers/databricks/provider.yaml | 3 +- providers/databricks/pyproject.toml | 6 +-- .../airflow/providers/databricks/__init__.py | 2 +- .../datadog/docs/.latest-doc-only-change.txt | 2 +- providers/dbt/cloud/README.rst | 6 +-- providers/dbt/cloud/docs/changelog.rst | 16 +++++++ providers/dbt/cloud/docs/index.rst | 6 +-- providers/dbt/cloud/provider.yaml | 3 +- providers/dbt/cloud/pyproject.toml | 6 +-- .../airflow/providers/dbt/cloud/__init__.py | 2 +- .../dingding/docs/.latest-doc-only-change.txt | 2 +- .../discord/docs/.latest-doc-only-change.txt | 2 +- providers/docker/README.rst | 15 ++----- providers/docker/docs/changelog.rst | 15 +++++++ providers/docker/docs/index.rst | 6 +-- providers/docker/provider.yaml | 3 +- providers/docker/pyproject.toml | 6 +-- .../src/airflow/providers/docker/__init__.py | 2 +- providers/edge3/README.rst | 6 +-- providers/edge3/docs/changelog.rst | 29 +++++++++++- providers/edge3/docs/index.rst | 6 +-- providers/edge3/provider.yaml | 3 +- providers/edge3/pyproject.toml | 6 +-- .../src/airflow/providers/edge3/__init__.py | 2 +- providers/fab/README.rst | 22 +++++----- providers/fab/docs/changelog.rst | 31 +++++++++++++ providers/fab/docs/index.rst | 6 +-- providers/fab/provider.yaml | 3 +- providers/fab/pyproject.toml | 6 +-- .../fab/src/airflow/providers/fab/__init__.py | 2 +- .../facebook/docs/.latest-doc-only-change.txt | 2 +- .../ftp/docs/.latest-doc-only-change.txt | 2 +- .../git/docs/.latest-doc-only-change.txt | 2 +- .../github/docs/.latest-doc-only-change.txt | 2 +- providers/google/README.rst | 13 +++--- providers/google/docs/changelog.rst | 42 ++++++++++++++++++ providers/google/docs/index.rst | 6 +-- providers/google/provider.yaml | 3 +- providers/google/pyproject.toml | 6 +-- .../src/airflow/providers/google/__init__.py | 2 +- .../grpc/docs/.latest-doc-only-change.txt | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- .../imap/docs/.latest-doc-only-change.txt | 2 +- .../influxdb/docs/.latest-doc-only-change.txt | 2 +- providers/jdbc/README.rst | 6 +-- providers/jdbc/docs/changelog.rst | 11 +++++ providers/jdbc/docs/index.rst | 6 +-- providers/jdbc/provider.yaml | 3 +- providers/jdbc/pyproject.toml | 6 +-- .../src/airflow/providers/jdbc/__init__.py | 2 +- providers/jenkins/README.rst | 6 +-- providers/jenkins/docs/changelog.rst | 13 ++++++ providers/jenkins/docs/index.rst | 6 +-- providers/jenkins/provider.yaml | 3 +- providers/jenkins/pyproject.toml | 6 +-- .../src/airflow/providers/jenkins/__init__.py | 2 +- providers/keycloak/README.rst | 6 +-- providers/keycloak/docs/changelog.rst | 19 ++++++++ providers/keycloak/docs/index.rst | 6 +-- providers/keycloak/provider.yaml | 3 +- providers/keycloak/pyproject.toml | 6 +-- .../airflow/providers/keycloak/__init__.py | 2 +- providers/microsoft/azure/README.rst | 6 +-- providers/microsoft/azure/docs/changelog.rst | 11 +++++ providers/microsoft/azure/docs/index.rst | 6 +-- providers/microsoft/azure/provider.yaml | 3 +- providers/microsoft/azure/pyproject.toml | 6 +-- .../providers/microsoft/azure/__init__.py | 2 +- .../psrp/docs/.latest-doc-only-change.txt | 2 +- providers/mongo/README.rst | 6 +-- providers/mongo/docs/changelog.rst | 13 ++++++ providers/mongo/docs/index.rst | 6 +-- providers/mongo/provider.yaml | 3 +- providers/mongo/pyproject.toml | 6 +-- .../src/airflow/providers/mongo/__init__.py | 2 +- .../neo4j/docs/.latest-doc-only-change.txt | 2 +- .../openai/docs/.latest-doc-only-change.txt | 2 +- .../openfaas/docs/.latest-doc-only-change.txt | 2 +- providers/openlineage/README.rst | 6 +-- providers/openlineage/docs/changelog.rst | 11 +++++ providers/openlineage/docs/index.rst | 6 +-- providers/openlineage/provider.yaml | 3 +- providers/openlineage/pyproject.toml | 6 +-- .../airflow/providers/openlineage/__init__.py | 2 +- .../opsgenie/docs/.latest-doc-only-change.txt | 2 +- providers/oracle/README.rst | 6 +-- providers/oracle/docs/changelog.rst | 11 +++++ providers/oracle/docs/index.rst | 6 +-- providers/oracle/provider.yaml | 3 +- providers/oracle/pyproject.toml | 6 +-- .../src/airflow/providers/oracle/__init__.py | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- .../pinecone/docs/.latest-doc-only-change.txt | 2 +- providers/postgres/README.rst | 6 +-- providers/postgres/docs/changelog.rst | 11 +++++ providers/postgres/docs/index.rst | 6 +-- providers/postgres/provider.yaml | 3 +- providers/postgres/pyproject.toml | 6 +-- .../airflow/providers/postgres/__init__.py | 2 +- .../qdrant/docs/.latest-doc-only-change.txt | 2 +- .../redis/docs/.latest-doc-only-change.txt | 2 +- providers/salesforce/README.rst | 6 +-- providers/salesforce/docs/changelog.rst | 13 ++++++ providers/salesforce/docs/index.rst | 6 +-- providers/salesforce/provider.yaml | 3 +- providers/salesforce/pyproject.toml | 6 +-- .../airflow/providers/salesforce/__init__.py | 2 +- providers/samba/README.rst | 6 +-- providers/samba/docs/changelog.rst | 13 ++++++ providers/samba/docs/index.rst | 6 +-- providers/samba/provider.yaml | 3 +- providers/samba/pyproject.toml | 6 +-- .../src/airflow/providers/samba/__init__.py | 2 +- .../segment/docs/.latest-doc-only-change.txt | 2 +- .../sendgrid/docs/.latest-doc-only-change.txt | 2 +- providers/sftp/README.rst | 8 ++-- providers/sftp/docs/changelog.rst | 14 ++++++ providers/sftp/docs/index.rst | 6 +-- providers/sftp/provider.yaml | 3 +- providers/sftp/pyproject.toml | 6 +-- .../src/airflow/providers/sftp/__init__.py | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- providers/slack/README.rst | 6 +-- providers/slack/docs/changelog.rst | 11 +++++ providers/slack/docs/index.rst | 6 +-- providers/slack/provider.yaml | 3 +- providers/slack/pyproject.toml | 6 +-- .../src/airflow/providers/slack/__init__.py | 2 +- providers/smtp/README.rst | 6 +-- providers/smtp/docs/changelog.rst | 15 +++++++ providers/smtp/docs/index.rst | 6 +-- providers/smtp/provider.yaml | 3 +- providers/smtp/pyproject.toml | 6 +-- .../src/airflow/providers/smtp/__init__.py | 2 +- providers/snowflake/README.rst | 6 +-- providers/snowflake/docs/changelog.rst | 17 +++++++ providers/snowflake/docs/index.rst | 6 +-- providers/snowflake/provider.yaml | 3 +- providers/snowflake/pyproject.toml | 6 +-- .../airflow/providers/snowflake/__init__.py | 2 +- providers/ssh/README.rst | 8 ++-- providers/ssh/docs/changelog.rst | 14 ++++++ providers/ssh/docs/index.rst | 6 +-- providers/ssh/provider.yaml | 3 +- providers/ssh/pyproject.toml | 6 +-- .../ssh/src/airflow/providers/ssh/__init__.py | 2 +- providers/standard/README.rst | 8 ++-- providers/standard/docs/changelog.rst | 18 ++++++++ providers/standard/docs/index.rst | 8 ++-- providers/standard/provider.yaml | 3 +- providers/standard/pyproject.toml | 8 ++-- .../airflow/providers/standard/__init__.py | 2 +- .../tableau/docs/.latest-doc-only-change.txt | 2 +- .../telegram/docs/.latest-doc-only-change.txt | 2 +- .../weaviate/docs/.latest-doc-only-change.txt | 2 +- .../zendesk/docs/.latest-doc-only-change.txt | 2 +- 250 files changed, 1094 insertions(+), 492 deletions(-) diff --git a/providers/.last_release_date.txt b/providers/.last_release_date.txt index 642b2c4f033ba..f05b3f46dbc26 100644 --- a/providers/.last_release_date.txt +++ b/providers/.last_release_date.txt @@ -1 +1 @@ -2026-03-03 +2026-03-09 diff --git a/providers/airbyte/README.rst b/providers/airbyte/README.rst index 35f6b13a96c7c..86494264f4493 100644 --- a/providers/airbyte/README.rst +++ b/providers/airbyte/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-airbyte`` -Release: ``5.3.2`` +Release: ``5.3.3`` `Airbyte `__ @@ -36,7 +36,7 @@ This is a provider package for ``airbyte`` provider. All classes for this provid are in ``airflow.providers.airbyte`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -54,7 +54,7 @@ Requirements PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.10.1`` +``apache-airflow-providers-common-compat`` ``>=1.12.0`` ``airbyte-api`` ``>=0.52.0`` ``requests`` ``>=2.32.0`` ========================================== ================== @@ -79,4 +79,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/airbyte/docs/changelog.rst b/providers/airbyte/docs/changelog.rst index d5bf18e2e8815..67a246a7b9e94 100644 --- a/providers/airbyte/docs/changelog.rst +++ b/providers/airbyte/docs/changelog.rst @@ -27,6 +27,20 @@ Changelog --------- +5.3.3 +..... + +Misc +~~~~ + +* ``Migrate-airbyte-connection-UI-metadata-to-YAML (#62426)`` + + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + 5.3.2 ..... diff --git a/providers/airbyte/docs/index.rst b/providers/airbyte/docs/index.rst index f5673bfc9389e..a7316d3e2553f 100644 --- a/providers/airbyte/docs/index.rst +++ b/providers/airbyte/docs/index.rst @@ -76,7 +76,7 @@ apache-airflow-providers-airbyte package `Airbyte `__ -Release: 5.3.2 +Release: 5.3.3 Provider package ---------------- @@ -130,5 +130,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-airbyte 5.3.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-airbyte 5.3.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-airbyte 5.3.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-airbyte 5.3.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/airbyte/provider.yaml b/providers/airbyte/provider.yaml index 23a771548d547..b60aa43473fb0 100644 --- a/providers/airbyte/provider.yaml +++ b/providers/airbyte/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768333828 +source-date-epoch: 1773069973 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 5.3.3 - 5.3.2 - 5.3.1 - 5.3.0 diff --git a/providers/airbyte/pyproject.toml b/providers/airbyte/pyproject.toml index a29c777b40432..073cec5b5c2f9 100644 --- a/providers/airbyte/pyproject.toml +++ b/providers/airbyte/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-airbyte" -version = "5.3.2" +version = "5.3.3" description = "Provider package apache-airflow-providers-airbyte for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -99,8 +99,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-airbyte/5.3.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-airbyte/5.3.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-airbyte/5.3.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-airbyte/5.3.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/airbyte/src/airflow/providers/airbyte/__init__.py b/providers/airbyte/src/airflow/providers/airbyte/__init__.py index dd3fea4574f00..88a79e907c44d 100644 --- a/providers/airbyte/src/airflow/providers/airbyte/__init__.py +++ b/providers/airbyte/src/airflow/providers/airbyte/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.3.2" +__version__ = "5.3.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/alibaba/README.rst b/providers/alibaba/README.rst index 341c166449a2b..9ad9742932708 100644 --- a/providers/alibaba/README.rst +++ b/providers/alibaba/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-alibaba`` -Release: ``3.3.4`` +Release: ``3.3.5`` Alibaba Cloud integration (including `Alibaba Cloud `__). @@ -36,7 +36,7 @@ This is a provider package for ``alibaba`` provider. All classes for this provid are in ``airflow.providers.alibaba`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -81,4 +81,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/alibaba/docs/changelog.rst b/providers/alibaba/docs/changelog.rst index 6e48150a7a514..90bfacab0d60d 100644 --- a/providers/alibaba/docs/changelog.rst +++ b/providers/alibaba/docs/changelog.rst @@ -26,6 +26,19 @@ Changelog --------- +3.3.5 +..... + +Misc +~~~~ + +* ``Migrate alibaba connection UI metadata to YAML (#62379)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 3.3.4 ..... diff --git a/providers/alibaba/docs/index.rst b/providers/alibaba/docs/index.rst index a200c77ac9027..70206ebb13807 100644 --- a/providers/alibaba/docs/index.rst +++ b/providers/alibaba/docs/index.rst @@ -77,7 +77,7 @@ apache-airflow-providers-alibaba package Alibaba Cloud integration (including `Alibaba Cloud `__). -Release: 3.3.4 +Release: 3.3.5 Provider package ---------------- @@ -133,5 +133,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-alibaba 3.3.4 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-alibaba 3.3.4 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-alibaba 3.3.5 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-alibaba 3.3.5 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/alibaba/provider.yaml b/providers/alibaba/provider.yaml index 82fcbbd55a192..e02be8f9a676f 100644 --- a/providers/alibaba/provider.yaml +++ b/providers/alibaba/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1769460342 +source-date-epoch: 1773070020 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 3.3.5 - 3.3.4 - 3.3.3 - 3.3.2 diff --git a/providers/alibaba/pyproject.toml b/providers/alibaba/pyproject.toml index 85adae95b83c8..dfb918523f45c 100644 --- a/providers/alibaba/pyproject.toml +++ b/providers/alibaba/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-alibaba" -version = "3.3.4" +version = "3.3.5" description = "Provider package apache-airflow-providers-alibaba for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -101,8 +101,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-alibaba/3.3.4" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-alibaba/3.3.4/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-alibaba/3.3.5" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-alibaba/3.3.5/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/alibaba/src/airflow/providers/alibaba/__init__.py b/providers/alibaba/src/airflow/providers/alibaba/__init__.py index 16a862c1e9f9d..78f6afb499648 100644 --- a/providers/alibaba/src/airflow/providers/alibaba/__init__.py +++ b/providers/alibaba/src/airflow/providers/alibaba/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "3.3.4" +__version__ = "3.3.5" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/amazon/README.rst b/providers/amazon/README.rst index 65d2c3a88ee82..6fd0afb4e735b 100644 --- a/providers/amazon/README.rst +++ b/providers/amazon/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-amazon`` -Release: ``9.22.0`` +Release: ``9.23.0`` Amazon integration (including `Amazon Web Services (AWS) `__). @@ -36,7 +36,7 @@ This is a provider package for ``amazon`` provider. All classes for this provide are in ``airflow.providers.amazon`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -133,4 +133,4 @@ Extra Dependencies ==================== ======================================================================================================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/amazon/docs/changelog.rst b/providers/amazon/docs/changelog.rst index 4b559496f861b..884e600680697 100644 --- a/providers/amazon/docs/changelog.rst +++ b/providers/amazon/docs/changelog.rst @@ -26,6 +26,33 @@ Changelog --------- +9.23.0 +...... + +Features +~~~~~~~~ + +* ``Add 'SesEmailOperator' (#58312)`` +* ``Adding sftp_remote_host to S3 transfer Operators (#63147)`` + +Bug Fixes +~~~~~~~~~ + +* ``Fix CloudwatchTaskHandler not deleting local logs after streaming (#62985)`` +* ``Fix invalid RequestPayer usage in S3Hook.select_key() method (#63148)`` +* ``S3GetBucketTaggingOperator ignoring aws_conn_id parameter (#63137)`` +* ``Scope session token in cookie to base_url (#62771)`` +* ``S3DagBundle does not delete stale dag recursively (#63104)`` + +Misc +~~~~ + +* ``Remove dependency limitations related to FAB's py3.13 incompatibility (#62924)`` +* ``Clarify to avoid bumping min version for sagemaker-studio (#62891)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 9.22.0 ...... diff --git a/providers/amazon/docs/index.rst b/providers/amazon/docs/index.rst index 8a05dcf951dd1..a8f73b3e58bda 100644 --- a/providers/amazon/docs/index.rst +++ b/providers/amazon/docs/index.rst @@ -87,7 +87,7 @@ apache-airflow-providers-amazon package Amazon integration (including `Amazon Web Services (AWS) `__). -Release: 9.22.0 +Release: 9.23.0 Provider package ---------------- @@ -168,5 +168,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-amazon 9.22.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-amazon 9.22.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-amazon 9.23.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-amazon 9.23.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/amazon/provider.yaml b/providers/amazon/provider.yaml index dee23e4dd86ab..13903584c2c8f 100644 --- a/providers/amazon/provider.yaml +++ b/providers/amazon/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1770750957 +source-date-epoch: 1773070067 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 9.23.0 - 9.22.0 - 9.21.0 - 9.20.0 diff --git a/providers/amazon/pyproject.toml b/providers/amazon/pyproject.toml index 723be6c77d27a..20a9e3c46a406 100644 --- a/providers/amazon/pyproject.toml +++ b/providers/amazon/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-amazon" -version = "9.22.0" +version = "9.23.0" description = "Provider package apache-airflow-providers-amazon for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -218,8 +218,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-amazon/9.22.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-amazon/9.22.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-amazon/9.23.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-amazon/9.23.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/amazon/src/airflow/providers/amazon/__init__.py b/providers/amazon/src/airflow/providers/amazon/__init__.py index 899a09b548ed3..4bc0477d911a0 100644 --- a/providers/amazon/src/airflow/providers/amazon/__init__.py +++ b/providers/amazon/src/airflow/providers/amazon/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "9.22.0" +__version__ = "9.23.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/apache/cassandra/docs/.latest-doc-only-change.txt b/providers/apache/cassandra/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/apache/cassandra/docs/.latest-doc-only-change.txt +++ b/providers/apache/cassandra/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/apache/flink/README.rst b/providers/apache/flink/README.rst index 93487aec60754..a8cba1f034954 100644 --- a/providers/apache/flink/README.rst +++ b/providers/apache/flink/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-apache-flink`` -Release: ``1.8.2`` +Release: ``1.8.3`` `Apache Flink `__ @@ -36,7 +36,7 @@ This is a provider package for ``apache.flink`` provider. All classes for this p are in ``airflow.providers.apache.flink`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -50,14 +50,14 @@ The package supports the following python versions: 3.10,3.11,3.12,3.13 Requirements ------------ -============================================ ==================== +============================================ ================== PIP package Version required -============================================ ==================== +============================================ ================== ``apache-airflow`` ``>=2.11.0`` ``apache-airflow-providers-common-compat`` ``>=1.10.1`` -``cryptography`` ``>=41.0.0,<46.0.0`` +``cryptography`` ``>=44.0.3`` ``apache-airflow-providers-cncf-kubernetes`` ``>=5.1.0`` -============================================ ==================== +============================================ ================== Cross provider package dependencies ----------------------------------- @@ -80,4 +80,4 @@ Dependent package ====================================================================================================================== =================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/apache/flink/docs/changelog.rst b/providers/apache/flink/docs/changelog.rst index da6f44d88e546..896f05b4099fd 100644 --- a/providers/apache/flink/docs/changelog.rst +++ b/providers/apache/flink/docs/changelog.rst @@ -26,6 +26,19 @@ Changelog --------- +1.8.3 +..... + +Misc +~~~~ + +* ``Bump minimum cryptography to 44.0.3 and paramiko to 3.4.0 (#62723)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 1.8.2 ..... diff --git a/providers/apache/flink/docs/index.rst b/providers/apache/flink/docs/index.rst index 6424236d27ee0..183cc74012b79 100644 --- a/providers/apache/flink/docs/index.rst +++ b/providers/apache/flink/docs/index.rst @@ -68,7 +68,7 @@ apache-airflow-providers-apache-flink package `Apache Flink `__ -Release: 1.8.2 +Release: 1.8.3 Provider package ---------------- @@ -123,5 +123,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-apache-flink 1.8.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-apache-flink 1.8.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-flink 1.8.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-flink 1.8.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/apache/flink/provider.yaml b/providers/apache/flink/provider.yaml index b49699e31c552..e86caff756023 100644 --- a/providers/apache/flink/provider.yaml +++ b/providers/apache/flink/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768334173 +source-date-epoch: 1773070115 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 1.8.3 - 1.8.2 - 1.8.1 - 1.8.0 diff --git a/providers/apache/flink/pyproject.toml b/providers/apache/flink/pyproject.toml index 0fdd1be5ab76d..f625f12bf5513 100644 --- a/providers/apache/flink/pyproject.toml +++ b/providers/apache/flink/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-apache-flink" -version = "1.8.2" +version = "1.8.3" description = "Provider package apache-airflow-providers-apache-flink for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -104,8 +104,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-flink/1.8.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-flink/1.8.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-flink/1.8.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-flink/1.8.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/apache/flink/src/airflow/providers/apache/flink/__init__.py b/providers/apache/flink/src/airflow/providers/apache/flink/__init__.py index ab97fcf2e7e57..83a69242bbdc5 100644 --- a/providers/apache/flink/src/airflow/providers/apache/flink/__init__.py +++ b/providers/apache/flink/src/airflow/providers/apache/flink/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "1.8.2" +__version__ = "1.8.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/apache/hdfs/docs/.latest-doc-only-change.txt b/providers/apache/hdfs/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/apache/hdfs/docs/.latest-doc-only-change.txt +++ b/providers/apache/hdfs/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/apache/hive/README.rst b/providers/apache/hive/README.rst index b6543cf3881ce..3581efb0c2d92 100644 --- a/providers/apache/hive/README.rst +++ b/providers/apache/hive/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-apache-hive`` -Release: ``9.3.0`` +Release: ``9.4.0`` `Apache Hive `__ @@ -36,7 +36,7 @@ This is a provider package for ``apache.hive`` provider. All classes for this pr are in ``airflow.providers.apache.hive`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -106,4 +106,4 @@ Extra Dependencies =================== ============================================================================================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/apache/hive/docs/changelog.rst b/providers/apache/hive/docs/changelog.rst index de8369e579e8f..0d89cfc632f96 100644 --- a/providers/apache/hive/docs/changelog.rst +++ b/providers/apache/hive/docs/changelog.rst @@ -27,6 +27,23 @@ Changelog --------- +9.4.0 +..... + +Features +~~~~~~~~ + +* ``Make hive cli 'zooKeeperNamespace' and 'ssl' parameters configurable (#63193)`` + +Misc +~~~~ + +* ``Made sqlalchemy dependency optional for hive (#62329)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``change to owner airflow in example dag (#62957)`` + 9.3.0 ..... diff --git a/providers/apache/hive/docs/index.rst b/providers/apache/hive/docs/index.rst index 75a1c44249126..c22f505c54aef 100644 --- a/providers/apache/hive/docs/index.rst +++ b/providers/apache/hive/docs/index.rst @@ -79,7 +79,7 @@ apache-airflow-providers-apache-hive package `Apache Hive `__ -Release: 9.3.0 +Release: 9.4.0 Provider package ---------------- @@ -144,5 +144,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-apache-hive 9.3.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-apache-hive 9.3.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-hive 9.4.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-hive 9.4.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/apache/hive/provider.yaml b/providers/apache/hive/provider.yaml index d650df5b4633f..cae016e6718e6 100644 --- a/providers/apache/hive/provider.yaml +++ b/providers/apache/hive/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064024 +source-date-epoch: 1773070139 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 9.4.0 - 9.3.0 - 9.2.5 - 9.2.4 diff --git a/providers/apache/hive/pyproject.toml b/providers/apache/hive/pyproject.toml index 96eb44061fd8a..d6aa77652c821 100644 --- a/providers/apache/hive/pyproject.toml +++ b/providers/apache/hive/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-apache-hive" -version = "9.3.0" +version = "9.4.0" description = "Provider package apache-airflow-providers-apache-hive for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -142,8 +142,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-hive/9.3.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-hive/9.3.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-hive/9.4.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-hive/9.4.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/apache/hive/src/airflow/providers/apache/hive/__init__.py b/providers/apache/hive/src/airflow/providers/apache/hive/__init__.py index dff1b23e65016..2c53bc5cd8794 100644 --- a/providers/apache/hive/src/airflow/providers/apache/hive/__init__.py +++ b/providers/apache/hive/src/airflow/providers/apache/hive/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "9.3.0" +__version__ = "9.4.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/apache/iceberg/README.rst b/providers/apache/iceberg/README.rst index 9cb28ca56f445..8cd81148ea635 100644 --- a/providers/apache/iceberg/README.rst +++ b/providers/apache/iceberg/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-apache-iceberg`` -Release: ``1.4.1`` +Release: ``2.0.0`` `Iceberg `__ @@ -36,7 +36,7 @@ This is a provider package for ``apache.iceberg`` provider. All classes for this are in ``airflow.providers.apache.iceberg`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -50,11 +50,13 @@ The package supports the following python versions: 3.10,3.11,3.12,3.13 Requirements ------------ -================== ================== -PIP package Version required -================== ================== -``apache-airflow`` ``>=2.11.0`` -================== ================== +========================================== ================== +PIP package Version required +========================================== ================== +``apache-airflow`` ``>=2.11.0`` +``apache-airflow-providers-common-compat`` ``>=1.8.0`` +``pyiceberg`` ``>=0.8.0`` +========================================== ================== Cross provider package dependencies ----------------------------------- @@ -75,14 +77,5 @@ Dependent package `apache-airflow-providers-common-compat `_ ``common.compat`` ================================================================================================================== ================= -Optional dependencies ----------------------- - -================= ========================================== -Extra Dependencies -================= ========================================== -``common.compat`` ``apache-airflow-providers-common-compat`` -================= ========================================== - The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/apache/iceberg/docs/changelog.rst b/providers/apache/iceberg/docs/changelog.rst index b9622355bea20..dcc943cbc19af 100644 --- a/providers/apache/iceberg/docs/changelog.rst +++ b/providers/apache/iceberg/docs/changelog.rst @@ -26,6 +26,24 @@ Changelog --------- +2.0.0 +..... + +Breaking changes +~~~~~~~~~~~~~~~~ + +* ``Add catalog introspection to IcebergHook using pyiceberg (#62634)`` + +Features +~~~~~~~~ + +* ``Add Iceberg support to AnalyticsOperator (#62754)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 1.4.1 ..... diff --git a/providers/apache/impala/docs/.latest-doc-only-change.txt b/providers/apache/impala/docs/.latest-doc-only-change.txt index 3f35346f79b81..2c1ab461a9c8e 100644 --- a/providers/apache/impala/docs/.latest-doc-only-change.txt +++ b/providers/apache/impala/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -134348e1895ad54cfa4d3a75a78bafe872328b11 +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/apache/kafka/README.rst b/providers/apache/kafka/README.rst index 1e9f1ba715024..82b9ff86b2649 100644 --- a/providers/apache/kafka/README.rst +++ b/providers/apache/kafka/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-apache-kafka`` -Release: ``1.12.0`` +Release: ``1.13.0`` `Apache Kafka `__ @@ -36,7 +36,7 @@ This is a provider package for ``apache.kafka`` provider. All classes for this p are in ``airflow.providers.apache.kafka`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -91,4 +91,4 @@ Extra Dependencies ==================== ==================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/apache/kafka/docs/changelog.rst b/providers/apache/kafka/docs/changelog.rst index dd4863f2f39ed..1cd00ba30103f 100644 --- a/providers/apache/kafka/docs/changelog.rst +++ b/providers/apache/kafka/docs/changelog.rst @@ -27,6 +27,19 @@ Changelog --------- +1.13.0 +...... + +Features +~~~~~~~~ + +* ``Add commit_offset option to AwaitMessageSensor and AwaitMessageTrigger (#62916)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 1.12.0 ...... diff --git a/providers/apache/kafka/docs/index.rst b/providers/apache/kafka/docs/index.rst index abd3d4f427266..57cecb4501252 100644 --- a/providers/apache/kafka/docs/index.rst +++ b/providers/apache/kafka/docs/index.rst @@ -83,7 +83,7 @@ apache-airflow-providers-apache-kafka package `Apache Kafka `__ -Release: 1.12.0 +Release: 1.13.0 Provider package ---------------- @@ -139,5 +139,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-apache-kafka 1.12.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-apache-kafka 1.12.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-kafka 1.13.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-kafka 1.13.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/apache/kafka/provider.yaml b/providers/apache/kafka/provider.yaml index 2006d3c7a2d55..046c2871235e1 100644 --- a/providers/apache/kafka/provider.yaml +++ b/providers/apache/kafka/provider.yaml @@ -21,7 +21,7 @@ name: Apache Kafka state: ready lifecycle: production -source-date-epoch: 1770751158 +source-date-epoch: 1773070274 description: | `Apache Kafka `__ # Note that those versions are maintained by release manager - do not update them manually @@ -29,6 +29,7 @@ description: | # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 1.13.0 - 1.12.0 - 1.11.3 - 1.11.2 diff --git a/providers/apache/kafka/pyproject.toml b/providers/apache/kafka/pyproject.toml index 5b48d76b32d1b..20fd34ac1c4bf 100644 --- a/providers/apache/kafka/pyproject.toml +++ b/providers/apache/kafka/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-apache-kafka" -version = "1.12.0" +version = "1.13.0" description = "Provider package apache-airflow-providers-apache-kafka for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -112,8 +112,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-kafka/1.12.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-kafka/1.12.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-kafka/1.13.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-kafka/1.13.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/apache/kafka/src/airflow/providers/apache/kafka/__init__.py b/providers/apache/kafka/src/airflow/providers/apache/kafka/__init__.py index fb711ffcb5cd9..710939c37b643 100644 --- a/providers/apache/kafka/src/airflow/providers/apache/kafka/__init__.py +++ b/providers/apache/kafka/src/airflow/providers/apache/kafka/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "1.12.0" +__version__ = "1.13.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/apache/kylin/docs/.latest-doc-only-change.txt b/providers/apache/kylin/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/apache/kylin/docs/.latest-doc-only-change.txt +++ b/providers/apache/kylin/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/apache/pig/docs/.latest-doc-only-change.txt b/providers/apache/pig/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/apache/pig/docs/.latest-doc-only-change.txt +++ b/providers/apache/pig/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/apache/spark/README.rst b/providers/apache/spark/README.rst index 80c8924c80d1e..b3b998df4e55b 100644 --- a/providers/apache/spark/README.rst +++ b/providers/apache/spark/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-apache-spark`` -Release: ``5.5.1`` +Release: ``5.6.0`` `Apache Spark `__ @@ -36,7 +36,7 @@ This is a provider package for ``apache.spark`` provider. All classes for this p are in ``airflow.providers.apache.spark`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -90,4 +90,4 @@ Extra Dependencies =================== =================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/apache/spark/docs/changelog.rst b/providers/apache/spark/docs/changelog.rst index aaede54e58595..0078f932415af 100644 --- a/providers/apache/spark/docs/changelog.rst +++ b/providers/apache/spark/docs/changelog.rst @@ -29,6 +29,17 @@ Changelog --------- +5.6.0 +..... + +Features +~~~~~~~~ + +* ``spark-pipelines operator (#61681)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 5.5.1 ..... diff --git a/providers/apache/spark/docs/index.rst b/providers/apache/spark/docs/index.rst index e097cbd3596a0..23a7c1ef407c8 100644 --- a/providers/apache/spark/docs/index.rst +++ b/providers/apache/spark/docs/index.rst @@ -77,7 +77,7 @@ apache-airflow-providers-apache-spark package `Apache Spark `__ -Release: 5.5.1 +Release: 5.6.0 Provider package ---------------- @@ -132,5 +132,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-apache-spark 5.5.1 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-apache-spark 5.5.1 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-spark 5.6.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-spark 5.6.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/apache/spark/provider.yaml b/providers/apache/spark/provider.yaml index 376cec1068380..33b212f73be33 100644 --- a/providers/apache/spark/provider.yaml +++ b/providers/apache/spark/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064082 +source-date-epoch: 1773070302 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 5.6.0 - 5.5.1 - 5.5.0 - 5.4.2 diff --git a/providers/apache/spark/pyproject.toml b/providers/apache/spark/pyproject.toml index 7412aa3a1b8fd..be89b0599746a 100644 --- a/providers/apache/spark/pyproject.toml +++ b/providers/apache/spark/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-apache-spark" -version = "5.5.1" +version = "5.6.0" description = "Provider package apache-airflow-providers-apache-spark for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -111,8 +111,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-spark/5.5.1" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-spark/5.5.1/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-spark/5.6.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-spark/5.6.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/apache/spark/src/airflow/providers/apache/spark/__init__.py b/providers/apache/spark/src/airflow/providers/apache/spark/__init__.py index 1e2d5a44f64d6..7aec790b255e4 100644 --- a/providers/apache/spark/src/airflow/providers/apache/spark/__init__.py +++ b/providers/apache/spark/src/airflow/providers/apache/spark/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.5.1" +__version__ = "5.6.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/apache/tinkerpop/docs/.latest-doc-only-change.txt b/providers/apache/tinkerpop/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/apache/tinkerpop/docs/.latest-doc-only-change.txt +++ b/providers/apache/tinkerpop/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/apprise/docs/.latest-doc-only-change.txt b/providers/apprise/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/apprise/docs/.latest-doc-only-change.txt +++ b/providers/apprise/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/arangodb/docs/.latest-doc-only-change.txt b/providers/arangodb/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/arangodb/docs/.latest-doc-only-change.txt +++ b/providers/arangodb/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/asana/docs/.latest-doc-only-change.txt b/providers/asana/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/asana/docs/.latest-doc-only-change.txt +++ b/providers/asana/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/atlassian/jira/docs/.latest-doc-only-change.txt b/providers/atlassian/jira/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/atlassian/jira/docs/.latest-doc-only-change.txt +++ b/providers/atlassian/jira/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/celery/README.rst b/providers/celery/README.rst index a1af4e6a3dbf6..1b89f2b97d0e9 100644 --- a/providers/celery/README.rst +++ b/providers/celery/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-celery`` -Release: ``3.17.0`` +Release: ``3.17.1`` `Celery `__ @@ -36,7 +36,7 @@ This is a provider package for ``celery`` provider. All classes for this provide are in ``airflow.providers.celery`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -89,4 +89,4 @@ Extra Dependencies =================== =================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/celery/docs/changelog.rst b/providers/celery/docs/changelog.rst index 1c2db002068e5..aa094671b328f 100644 --- a/providers/celery/docs/changelog.rst +++ b/providers/celery/docs/changelog.rst @@ -27,6 +27,17 @@ Changelog --------- +3.17.1 +...... + +Bug Fixes +~~~~~~~~~ + +* ``Ensure Celery tasks are registered at worker startup (main) (#63110)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 3.17.0 ...... diff --git a/providers/celery/docs/index.rst b/providers/celery/docs/index.rst index 3baa116abcf0c..d1659198a93f5 100644 --- a/providers/celery/docs/index.rst +++ b/providers/celery/docs/index.rst @@ -67,7 +67,7 @@ apache-airflow-providers-celery package `Celery `__ -Release: 3.17.0 +Release: 3.17.1 Provider package ---------------- @@ -122,5 +122,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-celery 3.17.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-celery 3.17.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-celery 3.17.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-celery 3.17.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/celery/provider.yaml b/providers/celery/provider.yaml index 71f9c663cca83..448d0fc369088 100644 --- a/providers/celery/provider.yaml +++ b/providers/celery/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064141 +source-date-epoch: 1773070341 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 3.17.1 - 3.17.0 - 3.16.0 - 3.15.2 diff --git a/providers/celery/pyproject.toml b/providers/celery/pyproject.toml index 7c9fed71892ba..7fdb1b219548a 100644 --- a/providers/celery/pyproject.toml +++ b/providers/celery/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-celery" -version = "3.17.0" +version = "3.17.1" description = "Provider package apache-airflow-providers-celery for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -111,8 +111,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-celery/3.17.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-celery/3.17.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-celery/3.17.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-celery/3.17.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/celery/src/airflow/providers/celery/__init__.py b/providers/celery/src/airflow/providers/celery/__init__.py index 8060c68950004..5cdd33e57d956 100644 --- a/providers/celery/src/airflow/providers/celery/__init__.py +++ b/providers/celery/src/airflow/providers/celery/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "3.17.0" +__version__ = "3.17.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/cloudant/docs/.latest-doc-only-change.txt b/providers/cloudant/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/cloudant/docs/.latest-doc-only-change.txt +++ b/providers/cloudant/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/cncf/kubernetes/README.rst b/providers/cncf/kubernetes/README.rst index 1176aa2381df8..b58487246e738 100644 --- a/providers/cncf/kubernetes/README.rst +++ b/providers/cncf/kubernetes/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-cncf-kubernetes`` -Release: ``10.13.0`` +Release: ``10.14.0`` `Kubernetes `__ @@ -36,7 +36,7 @@ This is a provider package for ``cncf.kubernetes`` provider. All classes for thi are in ``airflow.providers.cncf.kubernetes`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -55,9 +55,9 @@ PIP package Version required ========================================== ==================== ``aiofiles`` ``>=23.2.0`` ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.13.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` ``asgiref`` ``>=3.5.2`` -``cryptography`` ``>=41.0.0,<46.0.0`` +``cryptography`` ``>=44.0.3`` ``kubernetes`` ``>=35.0.0,<36.0.0`` ``urllib3`` ``>=2.1.0,!=2.6.0`` ``kubernetes_asyncio`` ``>=32.0.0,<35.0.0`` @@ -83,4 +83,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/cncf/kubernetes/docs/changelog.rst b/providers/cncf/kubernetes/docs/changelog.rst index c9dcbd4169fe9..03353a1915577 100644 --- a/providers/cncf/kubernetes/docs/changelog.rst +++ b/providers/cncf/kubernetes/docs/changelog.rst @@ -32,6 +32,29 @@ Changelog Previously this would create a job that would never complete and always fail the task. Executing a task with ``parallelism = 0`` and ``wait_until_job_complete=True`` will now raise a validation error. +10.14.0 +....... + +Features +~~~~~~~~ + +* ``Add multi-team support for KubernetesExecutor (#61798)`` +* ``Executor Synchronous callback workload (#61153)`` + +Bug Fixes +~~~~~~~~~ + +* ``fixed an issue that caused a state mismatch (#63061)`` + +Misc +~~~~ + +* ``Bump minimum cryptography to 44.0.3 and paramiko to 3.4.0 (#62723)`` +* ``Move determine_kwargs and KeywordParameters to SDK DecoratedOperator (#62746)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 10.13.0 ....... diff --git a/providers/cncf/kubernetes/docs/index.rst b/providers/cncf/kubernetes/docs/index.rst index 6fff7e0b93be4..0eb4071a4039c 100644 --- a/providers/cncf/kubernetes/docs/index.rst +++ b/providers/cncf/kubernetes/docs/index.rst @@ -88,7 +88,7 @@ apache-airflow-providers-cncf-kubernetes package `Kubernetes `__ -Release: 10.13.0 +Release: 10.14.0 Provider package ---------------- @@ -113,7 +113,7 @@ PIP package Version required ========================================== ==================== ``aiofiles`` ``>=23.2.0`` ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.13.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` ``asgiref`` ``>=3.5.2`` ``cryptography`` ``>=44.0.3`` ``kubernetes`` ``>=35.0.0,<36.0.0`` @@ -146,5 +146,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-cncf-kubernetes 10.13.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-cncf-kubernetes 10.13.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-cncf-kubernetes 10.14.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-cncf-kubernetes 10.14.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/cncf/kubernetes/provider.yaml b/providers/cncf/kubernetes/provider.yaml index 337c26fc982e7..4e5c48ea830d4 100644 --- a/providers/cncf/kubernetes/provider.yaml +++ b/providers/cncf/kubernetes/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064185 +source-date-epoch: 1773070394 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 10.14.0 - 10.13.0 - 10.12.4 - 10.12.3 diff --git a/providers/cncf/kubernetes/pyproject.toml b/providers/cncf/kubernetes/pyproject.toml index 5221158ec1f1e..f5a37c64be4c2 100644 --- a/providers/cncf/kubernetes/pyproject.toml +++ b/providers/cncf/kubernetes/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-cncf-kubernetes" -version = "10.13.0" +version = "10.14.0" description = "Provider package apache-airflow-providers-cncf-kubernetes for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -60,7 +60,7 @@ requires-python = ">=3.10" dependencies = [ "aiofiles>=23.2.0", "apache-airflow>=2.11.0", - "apache-airflow-providers-common-compat>=1.13.0", # use next version + "apache-airflow-providers-common-compat>=1.14.1", "asgiref>=3.5.2", # Cryptography could be upgraded to 46.0.5, but it does not have overlap with earlier versions # Of Airflow which were limited to <46.0.0 also earlier provider versions will not be compatible with newer @@ -119,8 +119,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.13.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.13.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.14.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.14.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/__init__.py b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/__init__.py index 37682c4599048..5298ebcc5bf37 100644 --- a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/__init__.py +++ b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "10.13.0" +__version__ = "10.14.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/cohere/README.rst b/providers/cohere/README.rst index 73f76b0c1fbaa..4d14a7355fa0d 100644 --- a/providers/cohere/README.rst +++ b/providers/cohere/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-cohere`` -Release: ``1.6.2`` +Release: ``1.6.3`` `Cohere `__ @@ -36,7 +36,7 @@ This is a provider package for ``cohere`` provider. All classes for this provide are in ``airflow.providers.cohere`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -79,4 +79,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/cohere/docs/changelog.rst b/providers/cohere/docs/changelog.rst index c9e10bdeaa1d9..c814133fa9840 100644 --- a/providers/cohere/docs/changelog.rst +++ b/providers/cohere/docs/changelog.rst @@ -20,6 +20,19 @@ Changelog --------- +1.6.3 +..... + +Misc +~~~~ + +* ``Migrate trino/cohere connection UI metadata to YAML (#62390)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 1.6.2 ..... diff --git a/providers/cohere/docs/index.rst b/providers/cohere/docs/index.rst index ef3151a4da1ff..84baeb772bb50 100644 --- a/providers/cohere/docs/index.rst +++ b/providers/cohere/docs/index.rst @@ -71,7 +71,7 @@ apache-airflow-providers-cohere package `Cohere `__ -Release: 1.6.2 +Release: 1.6.3 Provider package ---------------- @@ -125,5 +125,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-cohere 1.6.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-cohere 1.6.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-cohere 1.6.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-cohere 1.6.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/cohere/provider.yaml b/providers/cohere/provider.yaml index 562b9275aa587..31a6a7827b94e 100644 --- a/providers/cohere/provider.yaml +++ b/providers/cohere/provider.yaml @@ -25,13 +25,14 @@ description: | state: ready lifecycle: production -source-date-epoch: 1769460748 +source-date-epoch: 1773070423 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 1.6.3 - 1.6.2 - 1.6.1 - 1.6.0 diff --git a/providers/cohere/pyproject.toml b/providers/cohere/pyproject.toml index 30c10c38b8223..b311a5c8b4e56 100644 --- a/providers/cohere/pyproject.toml +++ b/providers/cohere/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-cohere" -version = "1.6.2" +version = "1.6.3" description = "Provider package apache-airflow-providers-cohere for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -99,8 +99,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-cohere/1.6.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-cohere/1.6.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-cohere/1.6.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-cohere/1.6.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/cohere/src/airflow/providers/cohere/__init__.py b/providers/cohere/src/airflow/providers/cohere/__init__.py index bc3ae95d7a4be..02fb74a98e6d5 100644 --- a/providers/cohere/src/airflow/providers/cohere/__init__.py +++ b/providers/cohere/src/airflow/providers/cohere/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "1.6.2" +__version__ = "1.6.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/common/ai/docs/index.rst b/providers/common/ai/docs/index.rst index e94e3b13e6150..661effc430ac1 100644 --- a/providers/common/ai/docs/index.rst +++ b/providers/common/ai/docs/index.rst @@ -103,8 +103,8 @@ The minimum Apache Airflow version supported by this provider distribution is `` PIP package Version required ========================================== ================== ``apache-airflow`` ``>=3.0.0`` -``apache-airflow-providers-common-compat`` ``>=1.13.1`` -``apache-airflow-providers-standard`` ``>=1.12.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` +``apache-airflow-providers-standard`` ``>=1.12.1`` ``pydantic-ai-slim`` ``>=1.14.0`` ========================================== ================== diff --git a/providers/common/ai/pyproject.toml b/providers/common/ai/pyproject.toml index 19b5aa98292bc..507265c293f88 100644 --- a/providers/common/ai/pyproject.toml +++ b/providers/common/ai/pyproject.toml @@ -59,8 +59,8 @@ requires-python = ">=3.10" # After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build`` dependencies = [ "apache-airflow>=3.0.0", - "apache-airflow-providers-common-compat>=1.13.1", # use next version - "apache-airflow-providers-standard>=1.12.0", # use next version + "apache-airflow-providers-common-compat>=1.14.1", + "apache-airflow-providers-standard>=1.12.1", "pydantic-ai-slim>=1.14.0", ] diff --git a/providers/common/compat/README.rst b/providers/common/compat/README.rst index ec9404b56f2c3..c91fcacd139dd 100644 --- a/providers/common/compat/README.rst +++ b/providers/common/compat/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-common-compat`` -Release: ``1.14.0`` +Release: ``1.14.1`` Common Compatibility Provider - providing compatibility code for previous Airflow versions @@ -36,7 +36,7 @@ This is a provider package for ``common.compat`` provider. All classes for this are in ``airflow.providers.common.compat`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -87,4 +87,4 @@ Extra Dependencies =============== ======================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/common/compat/docs/changelog.rst b/providers/common/compat/docs/changelog.rst index 421940d3263f1..7823ce6869249 100644 --- a/providers/common/compat/docs/changelog.rst +++ b/providers/common/compat/docs/changelog.rst @@ -25,6 +25,19 @@ Changelog --------- +1.14.1 +...... + +Misc +~~~~ + +* ``Consolidate 'SkipMixin' imports through 'common-compat' layer (#62776)`` +* ``Move SkipMixin and BranchMixIn to Task SDK (#62749)`` +* ``Move determine_kwargs and KeywordParameters to SDK DecoratedOperator (#62746)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 1.14.0 ...... diff --git a/providers/common/compat/docs/index.rst b/providers/common/compat/docs/index.rst index 8bd6c53af3304..a8052bf2eceb4 100644 --- a/providers/common/compat/docs/index.rst +++ b/providers/common/compat/docs/index.rst @@ -62,7 +62,7 @@ apache-airflow-providers-common-compat package Common Compatibility Provider - providing compatibility code for previous Airflow versions -Release: 1.14.0 +Release: 1.14.1 Provider package ---------------- @@ -114,5 +114,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-common-compat 1.14.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-common-compat 1.14.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-common-compat 1.14.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-common-compat 1.14.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/common/compat/provider.yaml b/providers/common/compat/provider.yaml index 746bcb603d415..fc21d3a094332 100644 --- a/providers/common/compat/provider.yaml +++ b/providers/common/compat/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064215 +source-date-epoch: 1773070447 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 1.14.1 - 1.14.0 - 1.13.1 - 1.13.0 diff --git a/providers/common/compat/pyproject.toml b/providers/common/compat/pyproject.toml index 46a7ffab16fb7..a4b0eaf10463c 100644 --- a/providers/common/compat/pyproject.toml +++ b/providers/common/compat/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-common-compat" -version = "1.14.0" +version = "1.14.1" description = "Provider package apache-airflow-providers-common-compat for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -107,8 +107,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-common-compat/1.14.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-common-compat/1.14.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-common-compat/1.14.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-common-compat/1.14.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/common/compat/src/airflow/providers/common/compat/__init__.py b/providers/common/compat/src/airflow/providers/common/compat/__init__.py index 29bd8d0d75ed5..e312ca9ef3a03 100644 --- a/providers/common/compat/src/airflow/providers/common/compat/__init__.py +++ b/providers/common/compat/src/airflow/providers/common/compat/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "1.14.0" +__version__ = "1.14.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/common/io/docs/.latest-doc-only-change.txt b/providers/common/io/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/common/io/docs/.latest-doc-only-change.txt +++ b/providers/common/io/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/common/messaging/docs/.latest-doc-only-change.txt b/providers/common/messaging/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/common/messaging/docs/.latest-doc-only-change.txt +++ b/providers/common/messaging/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/common/sql/README.rst b/providers/common/sql/README.rst index 3f1bc0d73396e..5fe692b50d526 100644 --- a/providers/common/sql/README.rst +++ b/providers/common/sql/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-common-sql`` -Release: ``1.32.0`` +Release: ``1.33.0`` `Common SQL Provider `__ @@ -36,7 +36,7 @@ This is a provider package for ``common.sql`` provider. All classes for this pro are in ``airflow.providers.common.sql`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -54,7 +54,7 @@ Requirements PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.12.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` ``sqlparse`` ``>=0.5.1`` ``more-itertools`` ``>=9.0.0`` ``methodtools`` ``>=0.4.7`` @@ -70,27 +70,33 @@ You can install such cross-provider dependencies when installing from PyPI. For .. code-block:: bash - pip install apache-airflow-providers-common-sql[common.compat] + pip install apache-airflow-providers-common-sql[amazon] -================================================================================================================== ================= -Dependent package Extra -================================================================================================================== ================= -`apache-airflow-providers-common-compat `_ ``common.compat`` -`apache-airflow-providers-openlineage `_ ``openlineage`` -================================================================================================================== ================= +==================================================================================================================== ================== +Dependent package Extra +==================================================================================================================== ================== +`apache-airflow-providers-amazon `_ ``amazon`` +`apache-airflow-providers-apache-iceberg `_ ``apache.iceberg`` +`apache-airflow-providers-common-compat `_ ``common.compat`` +`apache-airflow-providers-openlineage `_ ``openlineage`` +==================================================================================================================== ================== Optional dependencies ---------------------- -=============== ================================================================================================ -Extra Dependencies -=============== ================================================================================================ -``pandas`` ``pandas[sql-other]>=2.1.2; python_version <"3.13"``, ``pandas>=2.2.3; python_version >="3.13"`` -``openlineage`` ``apache-airflow-providers-openlineage`` -``polars`` ``polars>=1.26.0`` -``sqlalchemy`` ``sqlalchemy>=1.4.54`` -=============== ================================================================================================ +================== ================================================================================================ +Extra Dependencies +================== ================================================================================================ +``pandas`` ``pandas[sql-other]>=2.1.2; python_version <"3.13"``, ``pandas>=2.2.3; python_version >="3.13"`` +``openlineage`` ``apache-airflow-providers-openlineage`` +``polars`` ``polars>=1.26.0`` +``sqlalchemy`` ``sqlalchemy>=1.4.54`` +``amazon`` ``apache-airflow-providers-amazon`` +``datafusion`` ``datafusion>=50.0.0,<52.0.0`` +``pyiceberg-core`` ``pyiceberg-core>=0.8.0`` +``apache.iceberg`` ``apache-airflow-providers-apache-iceberg`` +================== ================================================================================================ The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/common/sql/docs/changelog.rst b/providers/common/sql/docs/changelog.rst index 590dfac85a00c..9a4c0d4867d2e 100644 --- a/providers/common/sql/docs/changelog.rst +++ b/providers/common/sql/docs/changelog.rst @@ -25,6 +25,34 @@ Changelog --------- +1.33.0 +...... + +Features +~~~~~~~~ + +* ``Add Iceberg support to AnalyticsOperator (#62754)`` +* ``Add @task.analytics Decorator (#62648)`` +* ``Add ObjectStorage support to LLMSQLQueryOperator via DataFusion (#62640)`` +* ``Add 'LLMSQLQueryOperator' and '@task.llm_sql' to common.ai provider (#62599)`` +* ``AIP-99: Add AnalyticsOperator (#62232)`` + +Bug Fixes +~~~~~~~~~ + +* ``Cache DbApiHook.inspector to avoid creating N engines (#62594)`` + +Misc +~~~~ + +* ``Consolidate 'SkipMixin' imports through 'common-compat' layer (#62776)`` +* ``Move determine_kwargs and KeywordParameters to SDK DecoratedOperator (#62746)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Fix removal of '__str__' method from Datafusion Format enums (#62830)`` + * ``Explicitly set extra for connections in generic transfer tests (#62581)`` + 1.32.0 ...... diff --git a/providers/common/sql/docs/index.rst b/providers/common/sql/docs/index.rst index d78f310a2f03c..ada01d754b4dd 100644 --- a/providers/common/sql/docs/index.rst +++ b/providers/common/sql/docs/index.rst @@ -79,7 +79,7 @@ apache-airflow-providers-common-sql package `Common SQL Provider `__ -Release: 1.32.0 +Release: 1.33.0 Provider package ---------------- @@ -103,7 +103,7 @@ The minimum Apache Airflow version supported by this provider distribution is `` PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.12.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` ``sqlparse`` ``>=0.5.1`` ``more-itertools`` ``>=9.0.0`` ``methodtools`` ``>=0.4.7`` @@ -137,5 +137,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-common-sql 1.32.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-common-sql 1.32.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-common-sql 1.33.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-common-sql 1.33.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/common/sql/provider.yaml b/providers/common/sql/provider.yaml index 8afdd4bfc5830..2cf0b2f294b1a 100644 --- a/providers/common/sql/provider.yaml +++ b/providers/common/sql/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1771577228 +source-date-epoch: 1773070489 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 1.33.0 - 1.32.0 - 1.31.0 - 1.30.4 diff --git a/providers/common/sql/pyproject.toml b/providers/common/sql/pyproject.toml index 7780a7fe94598..a08c1ac3fd2db 100644 --- a/providers/common/sql/pyproject.toml +++ b/providers/common/sql/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-common-sql" -version = "1.32.0" +version = "1.33.0" description = "Provider package apache-airflow-providers-common-sql for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -59,7 +59,7 @@ requires-python = ">=3.10" # After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build`` dependencies = [ "apache-airflow>=2.11.0", - "apache-airflow-providers-common-compat>=1.12.0", # use next version + "apache-airflow-providers-common-compat>=1.14.1", "sqlparse>=0.5.1", "more-itertools>=9.0.0", # The methodtools dependency is necessary since the introduction of dialects: @@ -146,8 +146,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/1.32.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/1.32.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/1.33.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/1.33.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/common/sql/src/airflow/providers/common/sql/__init__.py b/providers/common/sql/src/airflow/providers/common/sql/__init__.py index ba4f11945fdb2..3d5342ad3cd3c 100644 --- a/providers/common/sql/src/airflow/providers/common/sql/__init__.py +++ b/providers/common/sql/src/airflow/providers/common/sql/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "1.32.0" +__version__ = "1.33.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/databricks/README.rst b/providers/databricks/README.rst index d424c1be9fd23..ca640c098a6a1 100644 --- a/providers/databricks/README.rst +++ b/providers/databricks/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-databricks`` -Release: ``7.10.0`` +Release: ``7.11.0`` `Databricks `__ @@ -36,7 +36,7 @@ This is a provider package for ``databricks`` provider. All classes for this pro are in ``airflow.providers.databricks`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -105,4 +105,4 @@ Extra Dependencies ================== ================================================================ The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/databricks/docs/changelog.rst b/providers/databricks/docs/changelog.rst index b0aaa70e0ad39..5734d4a8970d2 100644 --- a/providers/databricks/docs/changelog.rst +++ b/providers/databricks/docs/changelog.rst @@ -26,6 +26,27 @@ Changelog --------- +7.11.0 +...... + +Features +~~~~~~~~ + +* ``Add validation for table_name and expression_list in DatabricksCopyIntoOperator (#62499)`` + +Bug Fixes +~~~~~~~~~ + +* ``Raise ValueError instead of KeyError when cancel_previous_runs=True and no job identifier is provided (#62393)`` + +Misc +~~~~ + +* ``Remove dependency limitations related to FAB's py3.13 incompatibility (#62924)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 7.10.0 ...... diff --git a/providers/databricks/docs/index.rst b/providers/databricks/docs/index.rst index 2b9618bf1b70a..164b453dc7c85 100644 --- a/providers/databricks/docs/index.rst +++ b/providers/databricks/docs/index.rst @@ -78,7 +78,7 @@ apache-airflow-providers-databricks package `Databricks `__ -Release: 7.10.0 +Release: 7.11.0 Provider package ---------------- @@ -142,5 +142,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-databricks 7.10.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-databricks 7.10.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-databricks 7.11.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-databricks 7.11.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/databricks/provider.yaml b/providers/databricks/provider.yaml index ec83c0ae1c642..41ab271bcbcd1 100644 --- a/providers/databricks/provider.yaml +++ b/providers/databricks/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064288 +source-date-epoch: 1773070523 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 7.11.0 - 7.10.0 - 7.9.1 - 7.9.0 diff --git a/providers/databricks/pyproject.toml b/providers/databricks/pyproject.toml index 756384686f210..a755ba6b5803e 100644 --- a/providers/databricks/pyproject.toml +++ b/providers/databricks/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-databricks" -version = "7.10.0" +version = "7.11.0" description = "Provider package apache-airflow-providers-databricks for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -146,8 +146,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-databricks/7.10.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-databricks/7.10.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-databricks/7.11.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-databricks/7.11.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/databricks/src/airflow/providers/databricks/__init__.py b/providers/databricks/src/airflow/providers/databricks/__init__.py index 9a2831d24eac3..856c275706dda 100644 --- a/providers/databricks/src/airflow/providers/databricks/__init__.py +++ b/providers/databricks/src/airflow/providers/databricks/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "7.10.0" +__version__ = "7.11.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/datadog/docs/.latest-doc-only-change.txt b/providers/datadog/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/datadog/docs/.latest-doc-only-change.txt +++ b/providers/datadog/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/dbt/cloud/README.rst b/providers/dbt/cloud/README.rst index a31b4ec52d6c3..631b7768770ca 100644 --- a/providers/dbt/cloud/README.rst +++ b/providers/dbt/cloud/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-dbt-cloud`` -Release: ``4.6.5`` +Release: ``4.7.0`` `dbt Cloud `__ @@ -36,7 +36,7 @@ This is a provider package for ``dbt.cloud`` provider. All classes for this prov are in ``airflow.providers.dbt.cloud`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -92,4 +92,4 @@ Extra Dependencies =============== =============================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/dbt/cloud/docs/changelog.rst b/providers/dbt/cloud/docs/changelog.rst index 478363753a433..a39ded73990c4 100644 --- a/providers/dbt/cloud/docs/changelog.rst +++ b/providers/dbt/cloud/docs/changelog.rst @@ -28,6 +28,22 @@ Changelog --------- +4.7.0 +..... + +Features +~~~~~~~~ + +* ``Add sync and async helpers to resolve the dbt Cloud account ID from the (#61757)`` + +Bug Fixes +~~~~~~~~~ + +* ``Raise on unexpected terminal dbt Cloud job run states (#61300)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 4.6.5 ..... diff --git a/providers/dbt/cloud/docs/index.rst b/providers/dbt/cloud/docs/index.rst index ae05429382d53..0da8131f10942 100644 --- a/providers/dbt/cloud/docs/index.rst +++ b/providers/dbt/cloud/docs/index.rst @@ -81,7 +81,7 @@ apache-airflow-providers-dbt-cloud package `dbt Cloud `__ -Release: 4.6.5 +Release: 4.7.0 Provider package ---------------- @@ -139,5 +139,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-dbt-cloud 4.6.5 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-dbt-cloud 4.6.5 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-dbt-cloud 4.7.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-dbt-cloud 4.7.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/dbt/cloud/provider.yaml b/providers/dbt/cloud/provider.yaml index 95f557b083b3d..a27c7dc1107ea 100644 --- a/providers/dbt/cloud/provider.yaml +++ b/providers/dbt/cloud/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064316 +source-date-epoch: 1773070569 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 4.7.0 - 4.6.5 - 4.6.4 - 4.6.3 diff --git a/providers/dbt/cloud/pyproject.toml b/providers/dbt/cloud/pyproject.toml index ed10b75468e85..78fc7a1569357 100644 --- a/providers/dbt/cloud/pyproject.toml +++ b/providers/dbt/cloud/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-dbt-cloud" -version = "4.6.5" +version = "4.7.0" description = "Provider package apache-airflow-providers-dbt-cloud for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -111,8 +111,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-dbt-cloud/4.6.5" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-dbt-cloud/4.6.5/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-dbt-cloud/4.7.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-dbt-cloud/4.7.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/dbt/cloud/src/airflow/providers/dbt/cloud/__init__.py b/providers/dbt/cloud/src/airflow/providers/dbt/cloud/__init__.py index 66ff805704e6e..4abc0f34ccafc 100644 --- a/providers/dbt/cloud/src/airflow/providers/dbt/cloud/__init__.py +++ b/providers/dbt/cloud/src/airflow/providers/dbt/cloud/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "4.6.5" +__version__ = "4.7.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/dingding/docs/.latest-doc-only-change.txt b/providers/dingding/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/dingding/docs/.latest-doc-only-change.txt +++ b/providers/dingding/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/discord/docs/.latest-doc-only-change.txt b/providers/discord/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/discord/docs/.latest-doc-only-change.txt +++ b/providers/discord/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/docker/README.rst b/providers/docker/README.rst index 130b4de4b6844..ad6e4a9b4ebe9 100644 --- a/providers/docker/README.rst +++ b/providers/docker/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-docker`` -Release: ``4.5.2`` +Release: ``4.5.3`` `Docker `__ @@ -36,7 +36,7 @@ This is a provider package for ``docker`` provider. All classes for this provide are in ``airflow.providers.docker`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -78,14 +78,5 @@ Dependent package `apache-airflow-providers-common-compat `_ ``common.compat`` ================================================================================================================== ================= -Optional dependencies ----------------------- - -================= ========================================== -Extra Dependencies -================= ========================================== -``common.compat`` ``apache-airflow-providers-common-compat`` -================= ========================================== - The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/docker/docs/changelog.rst b/providers/docker/docs/changelog.rst index 5ddaa9a2d3ef1..8e46ac38bcd22 100644 --- a/providers/docker/docs/changelog.rst +++ b/providers/docker/docs/changelog.rst @@ -28,6 +28,21 @@ Changelog --------- +4.5.3 +..... + +Bug Fixes +~~~~~~~~~ + +* ``Switch from surrogateescape to replace to handle utf-8 error (#62632)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + * ``[Part 2] Migrate connection UI metadata to YAML for more providers (#62109)`` + * ``CI: Upgrade important CI environment (#61417)`` + 4.5.2 ..... diff --git a/providers/docker/docs/index.rst b/providers/docker/docs/index.rst index f0d6420fe2452..9865c8ebbc5e4 100644 --- a/providers/docker/docs/index.rst +++ b/providers/docker/docs/index.rst @@ -70,7 +70,7 @@ apache-airflow-providers-docker package `Docker `__ -Release: 4.5.2 +Release: 4.5.3 Provider package ---------------- @@ -124,5 +124,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-docker 4.5.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-docker 4.5.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-docker 4.5.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-docker 4.5.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/docker/provider.yaml b/providers/docker/provider.yaml index 6824c551cd0f8..7018ce1cdf7d9 100644 --- a/providers/docker/provider.yaml +++ b/providers/docker/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768334813 +source-date-epoch: 1773070597 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 4.5.3 - 4.5.2 - 4.5.1 - 4.5.0 diff --git a/providers/docker/pyproject.toml b/providers/docker/pyproject.toml index 7bd84e70afa28..187c51df8c737 100644 --- a/providers/docker/pyproject.toml +++ b/providers/docker/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-docker" -version = "4.5.2" +version = "4.5.3" description = "Provider package apache-airflow-providers-docker for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -99,8 +99,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-docker/4.5.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-docker/4.5.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-docker/4.5.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-docker/4.5.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/docker/src/airflow/providers/docker/__init__.py b/providers/docker/src/airflow/providers/docker/__init__.py index fa8f18b48a41a..5f83b16e276a5 100644 --- a/providers/docker/src/airflow/providers/docker/__init__.py +++ b/providers/docker/src/airflow/providers/docker/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "4.5.2" +__version__ = "4.5.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/edge3/README.rst b/providers/edge3/README.rst index 5fd13d3e00124..9f6ff32c6364b 100644 --- a/providers/edge3/README.rst +++ b/providers/edge3/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-edge3`` -Release: ``3.1.0`` +Release: ``3.2.0`` Handle edge workers on remote sites via HTTP(s) connection and orchestrates work over distributed sites. @@ -48,7 +48,7 @@ This is a provider package for ``edge3`` provider. All classes for this provider are in ``airflow.providers.edge3`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -93,4 +93,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/edge3/docs/changelog.rst b/providers/edge3/docs/changelog.rst index f36f983745adf..db23c37380edb 100644 --- a/providers/edge3/docs/changelog.rst +++ b/providers/edge3/docs/changelog.rst @@ -27,6 +27,31 @@ Changelog --------- +3.2.0 +..... + +Features +~~~~~~~~ + +* ``Add real-time concurrency control for edge workers via UI (#63142)`` + +Bug Fixes +~~~~~~~~~ + +* ``Centralized runtime control of Edge Worker concurrency in distributed deployments (#62896)`` +* ``Fix _execution_api_server_url() reading edge.api_url when execution_api_server_url is already set (#63192)`` + +Doc-only +~~~~~~~~ + +* ``docs(edge3): add set-worker-concurrency command to deployment guide (#63083)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``chore(deps): bump the edge-ui-package-updates group across 1 directory with 6 updates (#63070)`` + * ``Upgrade 'tar' (#62939)`` + * ``Update dependencies for TS code (#62678)`` + 3.1.0 ..... @@ -255,14 +280,14 @@ Misc .. warning:: The React Plugin integration in this release is incompatible with Airflow 3.1.0 - It is recommended to use apache-airflow>=3.1.1 + It is recommended to use apache-airflow>=3.2.0 Bug Fixes ~~~~~~~~~ * ``Fix Link to Dag in Plugin (#55642)`` * ``Bugfix/support Subpath w/o Execution API Url (#57372)`` -* ``Adjust authentication token after UI changes in Airflow 3.1.1 (#57370)`` +* ``Adjust authentication token after UI changes in Airflow 3.2.0 (#57370)`` Misc ~~~~ diff --git a/providers/edge3/docs/index.rst b/providers/edge3/docs/index.rst index 18a39ad68ecaf..6f79fa617f7cc 100644 --- a/providers/edge3/docs/index.rst +++ b/providers/edge3/docs/index.rst @@ -90,7 +90,7 @@ Additional REST API endpoints are provided to distribute tasks and manage the ed are provided by the API server. -Release: 3.1.0 +Release: 3.2.0 Provider package ---------------- @@ -146,5 +146,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-edge3 3.1.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-edge3 3.1.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-edge3 3.2.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-edge3 3.2.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/edge3/provider.yaml b/providers/edge3/provider.yaml index 9f9d87cfd1865..1e5d4386cab9f 100644 --- a/providers/edge3/provider.yaml +++ b/providers/edge3/provider.yaml @@ -34,7 +34,7 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064422 +source-date-epoch: 1773070647 build-system: hatchling # Note that those versions are maintained by release manager - do not update them manually @@ -42,6 +42,7 @@ build-system: hatchling # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 3.2.0 - 3.1.0 - 3.0.2 - 3.0.1 diff --git a/providers/edge3/pyproject.toml b/providers/edge3/pyproject.toml index eb9b4a6e136ae..f1209231b804e 100644 --- a/providers/edge3/pyproject.toml +++ b/providers/edge3/pyproject.toml @@ -32,7 +32,7 @@ build-backend = "hatchling.build" [project] name = "apache-airflow-providers-edge3" -version = "3.1.0" +version = "3.2.0" description = "Provider package apache-airflow-providers-edge3 for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -108,8 +108,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-edge3/3.1.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-edge3/3.1.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-edge3/3.2.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-edge3/3.2.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/edge3/src/airflow/providers/edge3/__init__.py b/providers/edge3/src/airflow/providers/edge3/__init__.py index 534146709056d..0b8f09bb4764d 100644 --- a/providers/edge3/src/airflow/providers/edge3/__init__.py +++ b/providers/edge3/src/airflow/providers/edge3/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "3.1.0" +__version__ = "3.2.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "3.0.0" diff --git a/providers/fab/README.rst b/providers/fab/README.rst index 14dbbbeed71a4..83445661c5994 100644 --- a/providers/fab/README.rst +++ b/providers/fab/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-fab`` -Release: ``3.4.0`` +Release: ``3.5.0`` `Flask App Builder `__ @@ -36,7 +36,7 @@ This is a provider package for ``fab`` provider. All classes for this provider p are in ``airflow.providers.fab`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -45,14 +45,14 @@ You can install this package on top of an existing Airflow installation (see ``R for the minimum Airflow version supported) via ``pip install apache-airflow-providers-fab`` -The package supports the following python versions: 3.10,3.11,3.12 +The package supports the following python versions: 3.10,3.11,3.12,3.13 Requirements ------------ -========================================== ========================================== +========================================== ================== PIP package Version required -========================================== ========================================== +========================================== ================== ``apache-airflow`` ``>=3.0.2`` ``apache-airflow-providers-common-compat`` ``>=1.12.0`` ``blinker`` ``>=1.6.2`` @@ -68,7 +68,7 @@ PIP package Version required ``wtforms`` ``>=3.0,<4`` ``cachetools`` ``>=6.0`` ``flask_limiter`` ``>3,!=3.13,<4`` -========================================== ========================================== +========================================== ================== Cross provider package dependencies ----------------------------------- @@ -92,11 +92,11 @@ Dependent package Optional dependencies ---------------------- -============ ============================================ +============ =================== Extra Dependencies -============ ============================================ -``kerberos`` ``kerberos>=1.3.0; python_version < '3.13'`` -============ ============================================ +============ =================== +``kerberos`` ``kerberos>=1.3.0`` +============ =================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/fab/docs/changelog.rst b/providers/fab/docs/changelog.rst index 1407236bc456e..c18c1ab6fa388 100644 --- a/providers/fab/docs/changelog.rst +++ b/providers/fab/docs/changelog.rst @@ -20,6 +20,37 @@ Changelog --------- +3.5.0 +..... + +Features +~~~~~~~~ + +* ``Replace connexion with FastAPI for FAB provider (#62664)`` + +Bug Fixes +~~~~~~~~~ + +* ``Add missing HTTP timeout to FAB JWKS fetching (#63058)`` +* ``Fix race condition in auth manager initialization (#62431)`` +* ``Fix/FabAuthManager race condition on startup with multiple workers (#62737)`` +* ``Scope session token in cookie to base_url (#62771)`` + +Misc +~~~~ + +* ``Remove dependency limitations related to FAB's py3.13 incompatibility (#62924)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``chore(deps-dev): bump the fab-ui-package-updates group across 1 directory with 3 updates (#63067)`` + * ``chore(deps-dev): bump copy-webpack-plugin (#63004)`` + * ``Upgrade 'sgvo' (#62941)`` + * ``chore(deps-dev): bump the fab-ui-package-updates group across 1 directory with 3 updates (#62719)`` + * ``Update dependencies for TS code in Fab Provider (#62679)`` + * ``CI: Upgrade important CI environment (#62610)`` + * ``Fix all build-system/requires including transitive dependencies (#62570)`` + 3.4.0 ..... diff --git a/providers/fab/docs/index.rst b/providers/fab/docs/index.rst index bb7137d489a88..d0a4de316fa8d 100644 --- a/providers/fab/docs/index.rst +++ b/providers/fab/docs/index.rst @@ -83,7 +83,7 @@ apache-airflow-providers-fab package `Flask App Builder `__ -Release: 3.4.0 +Release: 3.5.0 Provider package ---------------- @@ -148,5 +148,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-fab 3.4.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-fab 3.4.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-fab 3.5.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-fab 3.5.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/fab/provider.yaml b/providers/fab/provider.yaml index e4dcef5520564..d1be6d2d9a051 100644 --- a/providers/fab/provider.yaml +++ b/providers/fab/provider.yaml @@ -29,7 +29,7 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064573 +source-date-epoch: 1773070724 build-system: hatchling # Note that those versions are maintained by release manager - do not update them manually @@ -37,6 +37,7 @@ build-system: hatchling # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 3.5.0 - 3.4.0 - 3.3.0 - 3.2.0 diff --git a/providers/fab/pyproject.toml b/providers/fab/pyproject.toml index 8041fa77e6350..11a4bada9c063 100644 --- a/providers/fab/pyproject.toml +++ b/providers/fab/pyproject.toml @@ -32,7 +32,7 @@ build-backend = "hatchling.build" [project] name = "apache-airflow-providers-fab" -version = "3.4.0" +version = "3.5.0" description = "Provider package apache-airflow-providers-fab for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -139,8 +139,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-fab/3.4.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-fab/3.4.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-fab/3.5.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-fab/3.5.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/fab/src/airflow/providers/fab/__init__.py b/providers/fab/src/airflow/providers/fab/__init__.py index 7acecac38ad07..d075015de3802 100644 --- a/providers/fab/src/airflow/providers/fab/__init__.py +++ b/providers/fab/src/airflow/providers/fab/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "3.4.0" +__version__ = "3.5.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "3.0.2" diff --git a/providers/facebook/docs/.latest-doc-only-change.txt b/providers/facebook/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/facebook/docs/.latest-doc-only-change.txt +++ b/providers/facebook/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/ftp/docs/.latest-doc-only-change.txt b/providers/ftp/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/ftp/docs/.latest-doc-only-change.txt +++ b/providers/ftp/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/git/docs/.latest-doc-only-change.txt b/providers/git/docs/.latest-doc-only-change.txt index 18968b027f51a..2c1ab461a9c8e 100644 --- a/providers/git/docs/.latest-doc-only-change.txt +++ b/providers/git/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -a4a51a02db2994e4ea94b83887739dc79a4d11d9 +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/github/docs/.latest-doc-only-change.txt b/providers/github/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/github/docs/.latest-doc-only-change.txt +++ b/providers/github/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/google/README.rst b/providers/google/README.rst index cc9b9082ee093..4acfce9e2bec9 100644 --- a/providers/google/README.rst +++ b/providers/google/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-google`` -Release: ``20.0.0`` +Release: ``21.0.0`` Google services including: @@ -43,7 +43,7 @@ This is a provider package for ``google`` provider. All classes for this provide are in ``airflow.providers.google`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -177,10 +177,9 @@ Dependent package Optional dependencies ---------------------- -==================== ================================================================ +==================== ==================================================== Extra Dependencies -==================== ================================================================ -``apache.beam`` ``apache-airflow-providers-apache-beam>=6.2.2`` +==================== ==================================================== ``cncf.kubernetes`` ``apache-airflow-providers-cncf-kubernetes>=10.1.0`` ``fab`` ``apache-airflow-providers-fab>=2.0.0`` ``leveldb`` ``plyvel>=1.5.1; python_version < '3.13'`` @@ -201,7 +200,7 @@ Extra Dependencies ``http`` ``apache-airflow-providers-http`` ``standard`` ``apache-airflow-providers-standard`` ``common.messaging`` ``apache-airflow-providers-common-messaging>=2.0.0`` -==================== ================================================================ +==================== ==================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/google/docs/changelog.rst b/providers/google/docs/changelog.rst index 7257e0fbd3937..d91d76192296c 100644 --- a/providers/google/docs/changelog.rst +++ b/providers/google/docs/changelog.rst @@ -67,6 +67,48 @@ Changelog * Remove ``CloudDataCatalogHook`` use ``airflow.providers.google.cloud.hooks.dataplex.DataplexHook`` instead * Remove ``airflow.providers.google.cloud.hooks.vertex_ai.generative_model.ExperimentRunHook`` use ``airflow.providers.google.cloud.hooks.vertex_ai.experiment_service.ExperimentRunHook`` instead +21.0.0 +...... + +Breaking changes +~~~~~~~~~~~~~~~~ + +* ``Delete google provider deprecated items sheduled for Jan 2026 (#62802)`` + +Features +~~~~~~~~ + +* ``Add drift detection and optional recreation to ComputeEngineInsertInstanceOperator (#61830)`` +* ``Return destination GCS URIs from ADLSToGCSOperator (#61463)`` +* ``Return GCS URIs from GoogleAdsToGcsOperator (#61334)`` +* ``Add bounded best-effort cluster deletion when PermissionDenied occurs after cluster creation has been initiated in non-deferrable mode. Deletion is triggered with wait_to_complete=False and retried on FailedPrecondition until cleanup_timeout_seconds is reached, and the original exception is always re-raised. Add unit tests covering cleanup initiation, retry behavior, and error propagation. (#62302)`` +* ``Add ClusterType field for Zero-Scale cluster support (#62207)`` + +Bug Fixes +~~~~~~~~~ + +* ``fix DataprocSubmitTrigger deferred tasks stuck forever (#62082)`` + +Misc +~~~~ + +* ``Upgrade version of Campaign Manager API to v5 (#62510)`` +* ``Add .json template_ext to BigQueryCreateTableOperator (#62058)`` + +Doc-only +~~~~~~~~ + +* ``Add known issue notice for version 19.5.0 (#61927)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Add exception test for GenAICountTokensOperator (#61391)`` + * ``Remove dependency limitations related to FAB's py3.13 incompatibility (#62924)`` + * ``Fix mypy issues from trinodb 0.337.0 (#62998)`` + * ``Replace connexion with FastAPI for FAB provider (#62664)`` + * ``Update provider's compatibility matrix with 2.11.1 (#62295)`` + * ``Suspend Apache Beam Provider due to grpcio limitation (#61926)`` + 20.0.0 ...... diff --git a/providers/google/docs/index.rst b/providers/google/docs/index.rst index e7da96e24af38..8cc8cca4a36e4 100644 --- a/providers/google/docs/index.rst +++ b/providers/google/docs/index.rst @@ -89,7 +89,7 @@ Google services including: - `Google Workspace `__ (formerly Google Suite) -Release: 20.0.0 +Release: 21.0.0 Provider package ---------------- @@ -232,5 +232,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-google 20.0.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-google 20.0.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-google 21.0.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-google 21.0.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/google/provider.yaml b/providers/google/provider.yaml index 4f0d1d90c81c9..c12c3e80d2993 100644 --- a/providers/google/provider.yaml +++ b/providers/google/provider.yaml @@ -30,12 +30,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1770752544 +source-date-epoch: 1773070870 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 21.0.0 - 20.0.0 - 19.5.0 - 19.4.0 diff --git a/providers/google/pyproject.toml b/providers/google/pyproject.toml index 125348db2bb05..3159b4ce712e2 100644 --- a/providers/google/pyproject.toml +++ b/providers/google/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-google" -version = "20.0.0" +version = "21.0.0" description = "Provider package apache-airflow-providers-google for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -263,8 +263,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-google/20.0.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-google/20.0.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-google/21.0.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-google/21.0.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/google/src/airflow/providers/google/__init__.py b/providers/google/src/airflow/providers/google/__init__.py index a976e00da2610..b698e55c63bd7 100644 --- a/providers/google/src/airflow/providers/google/__init__.py +++ b/providers/google/src/airflow/providers/google/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "20.0.0" +__version__ = "21.0.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/grpc/docs/.latest-doc-only-change.txt b/providers/grpc/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/grpc/docs/.latest-doc-only-change.txt +++ b/providers/grpc/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/hashicorp/docs/.latest-doc-only-change.txt b/providers/hashicorp/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/hashicorp/docs/.latest-doc-only-change.txt +++ b/providers/hashicorp/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/imap/docs/.latest-doc-only-change.txt b/providers/imap/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/imap/docs/.latest-doc-only-change.txt +++ b/providers/imap/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/influxdb/docs/.latest-doc-only-change.txt b/providers/influxdb/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/influxdb/docs/.latest-doc-only-change.txt +++ b/providers/influxdb/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/jdbc/README.rst b/providers/jdbc/README.rst index 8a0640c60d00a..8862f95a76ff1 100644 --- a/providers/jdbc/README.rst +++ b/providers/jdbc/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-jdbc`` -Release: ``5.4.0`` +Release: ``5.4.1`` `Java Database Connectivity (JDBC) `__ @@ -36,7 +36,7 @@ This is a provider package for ``jdbc`` provider. All classes for this provider are in ``airflow.providers.jdbc`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -80,4 +80,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/jdbc/docs/changelog.rst b/providers/jdbc/docs/changelog.rst index 15d82afa088be..c8e981bb17251 100644 --- a/providers/jdbc/docs/changelog.rst +++ b/providers/jdbc/docs/changelog.rst @@ -26,6 +26,17 @@ Changelog --------- +5.4.1 +..... + +Misc +~~~~ + +* `` Migrate JDBC connection UI metadata to YAML (#62427)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 5.4.0 ..... diff --git a/providers/jdbc/docs/index.rst b/providers/jdbc/docs/index.rst index da78458f75bc5..85ffe5c0b1cd8 100644 --- a/providers/jdbc/docs/index.rst +++ b/providers/jdbc/docs/index.rst @@ -78,7 +78,7 @@ apache-airflow-providers-jdbc package `Java Database Connectivity (JDBC) `__ -Release: 5.4.0 +Release: 5.4.1 Provider package ---------------- @@ -133,5 +133,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-jdbc 5.4.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-jdbc 5.4.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-jdbc 5.4.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-jdbc 5.4.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/jdbc/provider.yaml b/providers/jdbc/provider.yaml index 8786059249ffb..c89933f3bd860 100644 --- a/providers/jdbc/provider.yaml +++ b/providers/jdbc/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064974 +source-date-epoch: 1773070907 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 5.4.1 - 5.4.0 - 5.3.2 - 5.3.1 diff --git a/providers/jdbc/pyproject.toml b/providers/jdbc/pyproject.toml index fea3e735f6616..3217d510a91f7 100644 --- a/providers/jdbc/pyproject.toml +++ b/providers/jdbc/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-jdbc" -version = "5.4.0" +version = "5.4.1" description = "Provider package apache-airflow-providers-jdbc for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -100,8 +100,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-jdbc/5.4.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-jdbc/5.4.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-jdbc/5.4.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-jdbc/5.4.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/jdbc/src/airflow/providers/jdbc/__init__.py b/providers/jdbc/src/airflow/providers/jdbc/__init__.py index fe42a853a0c86..3b3bce1367917 100644 --- a/providers/jdbc/src/airflow/providers/jdbc/__init__.py +++ b/providers/jdbc/src/airflow/providers/jdbc/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.4.0" +__version__ = "5.4.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/jenkins/README.rst b/providers/jenkins/README.rst index 0af8749ab9b88..07a0ec767ec97 100644 --- a/providers/jenkins/README.rst +++ b/providers/jenkins/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-jenkins`` -Release: ``4.2.2`` +Release: ``4.2.3`` `Jenkins `__ @@ -36,7 +36,7 @@ This is a provider package for ``jenkins`` provider. All classes for this provid are in ``airflow.providers.jenkins`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -78,4 +78,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/jenkins/docs/changelog.rst b/providers/jenkins/docs/changelog.rst index 537b62068f89a..ddebd985bca67 100644 --- a/providers/jenkins/docs/changelog.rst +++ b/providers/jenkins/docs/changelog.rst @@ -27,6 +27,19 @@ Changelog --------- +4.2.3 +..... + +Misc +~~~~ + +* ``Migrate Jenkins connection UI metadata to YAML (#62432)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 4.2.2 ..... diff --git a/providers/jenkins/docs/index.rst b/providers/jenkins/docs/index.rst index 4795e02ec8134..16bb23054a39e 100644 --- a/providers/jenkins/docs/index.rst +++ b/providers/jenkins/docs/index.rst @@ -76,7 +76,7 @@ apache-airflow-providers-jenkins package `Jenkins `__ -Release: 4.2.2 +Release: 4.2.3 Provider package ---------------- @@ -129,5 +129,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-jenkins 4.2.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-jenkins 4.2.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-jenkins 4.2.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-jenkins 4.2.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/jenkins/provider.yaml b/providers/jenkins/provider.yaml index 6c6f08dba394b..402e5b93c5152 100644 --- a/providers/jenkins/provider.yaml +++ b/providers/jenkins/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768335202 +source-date-epoch: 1773070923 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 4.2.3 - 4.2.2 - 4.2.1 - 4.2.0 diff --git a/providers/jenkins/pyproject.toml b/providers/jenkins/pyproject.toml index 18db1ad0ea024..e3dd747fa6a64 100644 --- a/providers/jenkins/pyproject.toml +++ b/providers/jenkins/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-jenkins" -version = "4.2.2" +version = "4.2.3" description = "Provider package apache-airflow-providers-jenkins for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -98,8 +98,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-jenkins/4.2.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-jenkins/4.2.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-jenkins/4.2.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-jenkins/4.2.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/jenkins/src/airflow/providers/jenkins/__init__.py b/providers/jenkins/src/airflow/providers/jenkins/__init__.py index 256291edc2068..8a8614d24362c 100644 --- a/providers/jenkins/src/airflow/providers/jenkins/__init__.py +++ b/providers/jenkins/src/airflow/providers/jenkins/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "4.2.2" +__version__ = "4.2.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/keycloak/README.rst b/providers/keycloak/README.rst index c32c27458d2f2..d0bc895a88a42 100644 --- a/providers/keycloak/README.rst +++ b/providers/keycloak/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-keycloak`` -Release: ``0.5.3`` +Release: ``0.6.0`` ``Keycloak Provider`` @@ -36,7 +36,7 @@ This is a provider package for ``keycloak`` provider. All classes for this provi are in ``airflow.providers.keycloak`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -78,4 +78,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/keycloak/docs/changelog.rst b/providers/keycloak/docs/changelog.rst index b405ae7170aaf..529a81ffbe9d7 100644 --- a/providers/keycloak/docs/changelog.rst +++ b/providers/keycloak/docs/changelog.rst @@ -25,6 +25,25 @@ Changelog --------- +0.6.0 +..... + +Features +~~~~~~~~ + +* ``Add method to retrieve teams from Keycloak as resources (#62715)`` + +Bug Fixes +~~~~~~~~~ + +* ``Check 'id_token' format before redirecting in Keycloak auth manager (#62813)`` +* ``Do not get 'logout_callback_url' from request in keycloak auth manager (#62795)`` +* ``Scope session token in cookie to base_url (#62771)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Add Apache Airflow Provider Registry (#62261)`` + 0.5.3 ..... diff --git a/providers/keycloak/docs/index.rst b/providers/keycloak/docs/index.rst index 93143d0c18eac..704a84a388b15 100644 --- a/providers/keycloak/docs/index.rst +++ b/providers/keycloak/docs/index.rst @@ -78,7 +78,7 @@ apache-airflow-providers-keycloak package ``Keycloak Provider`` -Release: 0.5.3 +Release: 0.6.0 Provider package ---------------- @@ -131,5 +131,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-keycloak 0.5.3 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-keycloak 0.5.3 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-keycloak 0.6.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-keycloak 0.6.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/keycloak/provider.yaml b/providers/keycloak/provider.yaml index ababda03f1588..081421d74e28b 100644 --- a/providers/keycloak/provider.yaml +++ b/providers/keycloak/provider.yaml @@ -23,9 +23,10 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772065004 +source-date-epoch: 1773070964 # note that those versions are maintained by release manager - do not update them manually versions: + - 0.6.0 - 0.5.3 - 0.5.2 - 0.5.1 diff --git a/providers/keycloak/pyproject.toml b/providers/keycloak/pyproject.toml index b52d63dfe8d9b..4e666b5ac6c0e 100644 --- a/providers/keycloak/pyproject.toml +++ b/providers/keycloak/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-keycloak" -version = "0.5.3" +version = "0.6.0" description = "Provider package apache-airflow-providers-keycloak for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -98,8 +98,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-keycloak/0.5.3" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-keycloak/0.5.3/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-keycloak/0.6.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-keycloak/0.6.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/keycloak/src/airflow/providers/keycloak/__init__.py b/providers/keycloak/src/airflow/providers/keycloak/__init__.py index 3656d64cbd016..9ef43bea7aaae 100644 --- a/providers/keycloak/src/airflow/providers/keycloak/__init__.py +++ b/providers/keycloak/src/airflow/providers/keycloak/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "0.5.3" +__version__ = "0.6.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "3.0.0" diff --git a/providers/microsoft/azure/README.rst b/providers/microsoft/azure/README.rst index ed9497bcd46ea..05b49eaebb204 100644 --- a/providers/microsoft/azure/README.rst +++ b/providers/microsoft/azure/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-microsoft-azure`` -Release: ``13.0.0`` +Release: ``13.0.1`` `Microsoft Azure `__ @@ -36,7 +36,7 @@ This is a provider package for ``microsoft.azure`` provider. All classes for thi are in ``airflow.providers.microsoft.azure`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -121,4 +121,4 @@ Extra Dependencies ==================== ==================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/microsoft/azure/docs/changelog.rst b/providers/microsoft/azure/docs/changelog.rst index e1c2f81a29fae..14fc206ed0f0b 100644 --- a/providers/microsoft/azure/docs/changelog.rst +++ b/providers/microsoft/azure/docs/changelog.rst @@ -27,6 +27,17 @@ Changelog --------- +13.0.1 +...... + +Bug Fixes +~~~~~~~~~ + +* ``Fix PowerBIDatasetRefreshOperator to properly respect wait_for_completion flag (#62842)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 13.0.0 ...... diff --git a/providers/microsoft/azure/docs/index.rst b/providers/microsoft/azure/docs/index.rst index 6aa0bfe4483fe..8ba0c07f63738 100644 --- a/providers/microsoft/azure/docs/index.rst +++ b/providers/microsoft/azure/docs/index.rst @@ -84,7 +84,7 @@ apache-airflow-providers-microsoft-azure package `Microsoft Azure `__ -Release: 13.0.0 +Release: 13.0.1 Provider package ---------------- @@ -168,5 +168,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-microsoft-azure 13.0.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-microsoft-azure 13.0.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-microsoft-azure 13.0.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-microsoft-azure 13.0.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/microsoft/azure/provider.yaml b/providers/microsoft/azure/provider.yaml index c49e000bcb7b0..8b89a7cbd0a0e 100644 --- a/providers/microsoft/azure/provider.yaml +++ b/providers/microsoft/azure/provider.yaml @@ -21,12 +21,13 @@ description: | `Microsoft Azure `__ state: ready lifecycle: production -source-date-epoch: 1770752842 +source-date-epoch: 1773070980 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 13.0.1 - 13.0.0 - 12.10.3 - 12.10.2 diff --git a/providers/microsoft/azure/pyproject.toml b/providers/microsoft/azure/pyproject.toml index 00ca93d806339..3cc4edf2c5376 100644 --- a/providers/microsoft/azure/pyproject.toml +++ b/providers/microsoft/azure/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-microsoft-azure" -version = "13.0.0" +version = "13.0.1" description = "Provider package apache-airflow-providers-microsoft-azure for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -152,8 +152,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-microsoft-azure/13.0.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-microsoft-azure/13.0.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-microsoft-azure/13.0.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-microsoft-azure/13.0.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/__init__.py b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/__init__.py index 8e3e291443a6e..84eafe5fb7b0d 100644 --- a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/__init__.py +++ b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "13.0.0" +__version__ = "13.0.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/microsoft/psrp/docs/.latest-doc-only-change.txt b/providers/microsoft/psrp/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/microsoft/psrp/docs/.latest-doc-only-change.txt +++ b/providers/microsoft/psrp/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/mongo/README.rst b/providers/mongo/README.rst index d0bea0d7b5654..2ab0bacd2d871 100644 --- a/providers/mongo/README.rst +++ b/providers/mongo/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-mongo`` -Release: ``5.3.2`` +Release: ``5.3.3`` `MongoDB `__ @@ -36,7 +36,7 @@ This is a provider package for ``mongo`` provider. All classes for this provider are in ``airflow.providers.mongo`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -79,4 +79,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/mongo/docs/changelog.rst b/providers/mongo/docs/changelog.rst index e39d290fbf399..b680a04154d01 100644 --- a/providers/mongo/docs/changelog.rst +++ b/providers/mongo/docs/changelog.rst @@ -27,6 +27,19 @@ Changelog --------- +5.3.3 +..... + +Misc +~~~~ + +* ``Migrate mongo connection UI metadata to YAML (#62444)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 5.3.2 ..... diff --git a/providers/mongo/docs/index.rst b/providers/mongo/docs/index.rst index eb704b6129d13..d7f192eb5a53c 100644 --- a/providers/mongo/docs/index.rst +++ b/providers/mongo/docs/index.rst @@ -62,7 +62,7 @@ apache-airflow-providers-mongo package `MongoDB `__ -Release: 5.3.2 +Release: 5.3.3 Provider package ---------------- @@ -116,5 +116,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-mongo 5.3.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-mongo 5.3.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-mongo 5.3.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-mongo 5.3.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/mongo/provider.yaml b/providers/mongo/provider.yaml index adb1c5de2d666..6082719a7c4ca 100644 --- a/providers/mongo/provider.yaml +++ b/providers/mongo/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768335322 +source-date-epoch: 1773071003 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 5.3.3 - 5.3.2 - 5.3.1 - 5.3.0 diff --git a/providers/mongo/pyproject.toml b/providers/mongo/pyproject.toml index f870868558703..1949f0f7a6002 100644 --- a/providers/mongo/pyproject.toml +++ b/providers/mongo/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-mongo" -version = "5.3.2" +version = "5.3.3" description = "Provider package apache-airflow-providers-mongo for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -100,8 +100,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-mongo/5.3.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-mongo/5.3.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-mongo/5.3.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-mongo/5.3.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/mongo/src/airflow/providers/mongo/__init__.py b/providers/mongo/src/airflow/providers/mongo/__init__.py index b6342b81b360e..7b5e05a66a6a4 100644 --- a/providers/mongo/src/airflow/providers/mongo/__init__.py +++ b/providers/mongo/src/airflow/providers/mongo/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.3.2" +__version__ = "5.3.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/neo4j/docs/.latest-doc-only-change.txt b/providers/neo4j/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/neo4j/docs/.latest-doc-only-change.txt +++ b/providers/neo4j/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/openai/docs/.latest-doc-only-change.txt b/providers/openai/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/openai/docs/.latest-doc-only-change.txt +++ b/providers/openai/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/openfaas/docs/.latest-doc-only-change.txt b/providers/openfaas/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/openfaas/docs/.latest-doc-only-change.txt +++ b/providers/openfaas/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/openlineage/README.rst b/providers/openlineage/README.rst index 51fd4f7839b54..c599ca655d9c4 100644 --- a/providers/openlineage/README.rst +++ b/providers/openlineage/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-openlineage`` -Release: ``2.11.0`` +Release: ``2.12.0`` `OpenLineage `__ is an open framework for data lineage collection. @@ -37,7 +37,7 @@ This is a provider package for ``openlineage`` provider. All classes for this pr are in ``airflow.providers.openlineage`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -92,4 +92,4 @@ Extra Dependencies ============== ====================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/openlineage/docs/changelog.rst b/providers/openlineage/docs/changelog.rst index cc078d4561025..d3421615124aa 100644 --- a/providers/openlineage/docs/changelog.rst +++ b/providers/openlineage/docs/changelog.rst @@ -26,6 +26,17 @@ Changelog --------- +2.12.0 +...... + +Features +~~~~~~~~ + +* ``Add DagRun note to OL events (#62347)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 2.11.0 ...... diff --git a/providers/openlineage/docs/index.rst b/providers/openlineage/docs/index.rst index c82fec5863a4f..eb02224d38175 100644 --- a/providers/openlineage/docs/index.rst +++ b/providers/openlineage/docs/index.rst @@ -83,7 +83,7 @@ apache-airflow-providers-openlineage package At its core it is an extensible specification that systems can use to interoperate with lineage metadata. -Release: 2.11.0 +Release: 2.12.0 Provider package ---------------- @@ -140,5 +140,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-openlineage 2.11.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-openlineage 2.11.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-openlineage 2.12.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-openlineage 2.12.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/openlineage/provider.yaml b/providers/openlineage/provider.yaml index f0c28868c56ae..42bd12f391922 100644 --- a/providers/openlineage/provider.yaml +++ b/providers/openlineage/provider.yaml @@ -24,12 +24,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772065120 +source-date-epoch: 1773071035 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 2.12.0 - 2.11.0 - 2.10.2 - 2.10.1 diff --git a/providers/openlineage/pyproject.toml b/providers/openlineage/pyproject.toml index 101e190d0b98a..890dc5fc37766 100644 --- a/providers/openlineage/pyproject.toml +++ b/providers/openlineage/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-openlineage" -version = "2.11.0" +version = "2.12.0" description = "Provider package apache-airflow-providers-openlineage for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -114,8 +114,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-openlineage/2.11.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-openlineage/2.11.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-openlineage/2.12.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-openlineage/2.12.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/openlineage/src/airflow/providers/openlineage/__init__.py b/providers/openlineage/src/airflow/providers/openlineage/__init__.py index d27aa3d9dab0f..17d9e85bd0859 100644 --- a/providers/openlineage/src/airflow/providers/openlineage/__init__.py +++ b/providers/openlineage/src/airflow/providers/openlineage/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "2.11.0" +__version__ = "2.12.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/opsgenie/docs/.latest-doc-only-change.txt b/providers/opsgenie/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/opsgenie/docs/.latest-doc-only-change.txt +++ b/providers/opsgenie/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/oracle/README.rst b/providers/oracle/README.rst index 2ed04a9bb3814..67377de8cfd57 100644 --- a/providers/oracle/README.rst +++ b/providers/oracle/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-oracle`` -Release: ``4.5.0`` +Release: ``4.5.1`` `Oracle `__ @@ -36,7 +36,7 @@ This is a provider package for ``oracle`` provider. All classes for this provide are in ``airflow.providers.oracle`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -91,4 +91,4 @@ Extra Dependencies =============== ======================================================================================================================================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/oracle/docs/changelog.rst b/providers/oracle/docs/changelog.rst index 7c2fc4aa413f7..018fb29ef9ef8 100644 --- a/providers/oracle/docs/changelog.rst +++ b/providers/oracle/docs/changelog.rst @@ -27,6 +27,17 @@ Changelog --------- +4.5.1 +..... + +Bug Fixes +~~~~~~~~~ + +* ``Use conn.schema as service_name fallback in OracleHook (#62895)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 4.5.0 ..... diff --git a/providers/oracle/docs/index.rst b/providers/oracle/docs/index.rst index 300ade40662cd..6be1b0738b382 100644 --- a/providers/oracle/docs/index.rst +++ b/providers/oracle/docs/index.rst @@ -77,7 +77,7 @@ apache-airflow-providers-oracle package `Oracle `__ -Release: 4.5.0 +Release: 4.5.1 Provider package ---------------- @@ -133,5 +133,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-oracle 4.5.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-oracle 4.5.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-oracle 4.5.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-oracle 4.5.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/oracle/provider.yaml b/providers/oracle/provider.yaml index b159bbca15e5d..49d3553037487 100644 --- a/providers/oracle/provider.yaml +++ b/providers/oracle/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772065135 +source-date-epoch: 1773100961 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 4.5.1 - 4.5.0 - 4.4.0 - 4.3.1 diff --git a/providers/oracle/pyproject.toml b/providers/oracle/pyproject.toml index 9ee108484ef08..82f1ea783b025 100644 --- a/providers/oracle/pyproject.toml +++ b/providers/oracle/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-oracle" -version = "4.5.0" +version = "4.5.1" description = "Provider package apache-airflow-providers-oracle for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -118,8 +118,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-oracle/4.5.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-oracle/4.5.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-oracle/4.5.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-oracle/4.5.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/oracle/src/airflow/providers/oracle/__init__.py b/providers/oracle/src/airflow/providers/oracle/__init__.py index 802d6e799e1b8..38b2bbae07723 100644 --- a/providers/oracle/src/airflow/providers/oracle/__init__.py +++ b/providers/oracle/src/airflow/providers/oracle/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "4.5.0" +__version__ = "4.5.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/pagerduty/docs/.latest-doc-only-change.txt b/providers/pagerduty/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/pagerduty/docs/.latest-doc-only-change.txt +++ b/providers/pagerduty/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/papermill/docs/.latest-doc-only-change.txt b/providers/papermill/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/papermill/docs/.latest-doc-only-change.txt +++ b/providers/papermill/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/pinecone/docs/.latest-doc-only-change.txt b/providers/pinecone/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/pinecone/docs/.latest-doc-only-change.txt +++ b/providers/pinecone/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/postgres/README.rst b/providers/postgres/README.rst index bcdf94557aa3c..fb21e54b43ba7 100644 --- a/providers/postgres/README.rst +++ b/providers/postgres/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-postgres`` -Release: ``6.6.0`` +Release: ``6.6.1`` `PostgreSQL `__ @@ -36,7 +36,7 @@ This is a provider package for ``postgres`` provider. All classes for this provi are in ``airflow.providers.postgres`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -100,4 +100,4 @@ Extra Dependencies =================== ===================================================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/postgres/docs/changelog.rst b/providers/postgres/docs/changelog.rst index cf290e5d81343..bd2d034db3d90 100644 --- a/providers/postgres/docs/changelog.rst +++ b/providers/postgres/docs/changelog.rst @@ -27,6 +27,17 @@ Changelog --------- +6.6.1 +..... + +Misc +~~~~ + +* ``Migrate postgres connection UI metadata to YAML (#62445)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 6.6.0 ..... diff --git a/providers/postgres/docs/index.rst b/providers/postgres/docs/index.rst index e81d4f5c0bc7b..df69d2cef59df 100644 --- a/providers/postgres/docs/index.rst +++ b/providers/postgres/docs/index.rst @@ -78,7 +78,7 @@ apache-airflow-providers-postgres package `PostgreSQL `__ -Release: 6.6.0 +Release: 6.6.1 Provider package ---------------- @@ -138,5 +138,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-postgres 6.6.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-postgres 6.6.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-postgres 6.6.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-postgres 6.6.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/postgres/provider.yaml b/providers/postgres/provider.yaml index 9535a91baddec..23d5801ac4bd7 100644 --- a/providers/postgres/provider.yaml +++ b/providers/postgres/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772065167 +source-date-epoch: 1773071067 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 6.6.1 - 6.6.0 - 6.5.4 - 6.5.3 diff --git a/providers/postgres/pyproject.toml b/providers/postgres/pyproject.toml index f24d724696c26..922aa70c68db2 100644 --- a/providers/postgres/pyproject.toml +++ b/providers/postgres/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-postgres" -version = "6.6.0" +version = "6.6.1" description = "Provider package apache-airflow-providers-postgres for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -134,8 +134,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-postgres/6.6.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-postgres/6.6.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-postgres/6.6.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-postgres/6.6.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/postgres/src/airflow/providers/postgres/__init__.py b/providers/postgres/src/airflow/providers/postgres/__init__.py index 17ec5e58942c3..848db5ea9a905 100644 --- a/providers/postgres/src/airflow/providers/postgres/__init__.py +++ b/providers/postgres/src/airflow/providers/postgres/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "6.6.0" +__version__ = "6.6.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/qdrant/docs/.latest-doc-only-change.txt b/providers/qdrant/docs/.latest-doc-only-change.txt index 3f35346f79b81..2c1ab461a9c8e 100644 --- a/providers/qdrant/docs/.latest-doc-only-change.txt +++ b/providers/qdrant/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -134348e1895ad54cfa4d3a75a78bafe872328b11 +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/redis/docs/.latest-doc-only-change.txt b/providers/redis/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/redis/docs/.latest-doc-only-change.txt +++ b/providers/redis/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/salesforce/README.rst b/providers/salesforce/README.rst index 04a7caa51e42e..dbc673594f779 100644 --- a/providers/salesforce/README.rst +++ b/providers/salesforce/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-salesforce`` -Release: ``5.12.2`` +Release: ``5.12.3`` `Salesforce `__ @@ -36,7 +36,7 @@ This is a provider package for ``salesforce`` provider. All classes for this pro are in ``airflow.providers.salesforce`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -80,4 +80,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/salesforce/docs/changelog.rst b/providers/salesforce/docs/changelog.rst index 4f3d446c8de32..9221f3a338f4c 100644 --- a/providers/salesforce/docs/changelog.rst +++ b/providers/salesforce/docs/changelog.rst @@ -27,6 +27,19 @@ Changelog --------- +5.12.3 +...... + +Misc +~~~~ + +* ``Migrate salesforce connection UI metadata to YAML (#62446)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 5.12.2 ...... diff --git a/providers/salesforce/docs/index.rst b/providers/salesforce/docs/index.rst index ec768b2e531b6..f4a4edad10a02 100644 --- a/providers/salesforce/docs/index.rst +++ b/providers/salesforce/docs/index.rst @@ -77,7 +77,7 @@ apache-airflow-providers-salesforce package `Salesforce `__ -Release: 5.12.2 +Release: 5.12.3 Provider package ---------------- @@ -132,5 +132,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-salesforce 5.12.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-salesforce 5.12.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-salesforce 5.12.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-salesforce 5.12.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/salesforce/provider.yaml b/providers/salesforce/provider.yaml index 06cfbcdfc49b1..e1b1035bf9a9a 100644 --- a/providers/salesforce/provider.yaml +++ b/providers/salesforce/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768335553 +source-date-epoch: 1773071089 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 5.12.3 - 5.12.2 - 5.12.1 - 5.12.0 diff --git a/providers/salesforce/pyproject.toml b/providers/salesforce/pyproject.toml index 576de240bc6f8..8af3ace68e2a1 100644 --- a/providers/salesforce/pyproject.toml +++ b/providers/salesforce/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-salesforce" -version = "5.12.2" +version = "5.12.3" description = "Provider package apache-airflow-providers-salesforce for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -100,8 +100,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-salesforce/5.12.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-salesforce/5.12.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-salesforce/5.12.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-salesforce/5.12.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/salesforce/src/airflow/providers/salesforce/__init__.py b/providers/salesforce/src/airflow/providers/salesforce/__init__.py index f7d156a4fee8b..7ae0165b0585f 100644 --- a/providers/salesforce/src/airflow/providers/salesforce/__init__.py +++ b/providers/salesforce/src/airflow/providers/salesforce/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.12.2" +__version__ = "5.12.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/samba/README.rst b/providers/samba/README.rst index e1f79b9216588..ef8b08092174b 100644 --- a/providers/samba/README.rst +++ b/providers/samba/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-samba`` -Release: ``4.12.2`` +Release: ``4.12.3`` `Samba `__ @@ -36,7 +36,7 @@ This is a provider package for ``samba`` provider. All classes for this provider are in ``airflow.providers.samba`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -88,4 +88,4 @@ Extra Dependencies ========== =================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/samba/docs/changelog.rst b/providers/samba/docs/changelog.rst index 61242761b44cb..4a2216edb8cca 100644 --- a/providers/samba/docs/changelog.rst +++ b/providers/samba/docs/changelog.rst @@ -27,6 +27,19 @@ Changelog --------- +4.12.3 +...... + +Misc +~~~~ + +* ``Migrate samba connection UI metadata to YAML (#62514)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 4.12.2 ...... diff --git a/providers/samba/docs/index.rst b/providers/samba/docs/index.rst index c9f7dffd7b3cb..8e18adc109d60 100644 --- a/providers/samba/docs/index.rst +++ b/providers/samba/docs/index.rst @@ -76,7 +76,7 @@ apache-airflow-providers-samba package `Samba `__ -Release: 4.12.2 +Release: 4.12.3 Provider package ---------------- @@ -130,5 +130,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-samba 4.12.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-samba 4.12.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-samba 4.12.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-samba 4.12.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/samba/provider.yaml b/providers/samba/provider.yaml index ba87fc8e5b58e..0e11b63ad6f38 100644 --- a/providers/samba/provider.yaml +++ b/providers/samba/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768335561 +source-date-epoch: 1773071107 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 4.12.3 - 4.12.2 - 4.12.1 - 4.12.0 diff --git a/providers/samba/pyproject.toml b/providers/samba/pyproject.toml index b8d9178d2bdcb..0cb09047b7b06 100644 --- a/providers/samba/pyproject.toml +++ b/providers/samba/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-samba" -version = "4.12.2" +version = "4.12.3" description = "Provider package apache-airflow-providers-samba for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -106,8 +106,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-samba/4.12.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-samba/4.12.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-samba/4.12.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-samba/4.12.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/samba/src/airflow/providers/samba/__init__.py b/providers/samba/src/airflow/providers/samba/__init__.py index 6b4ed70730f88..f1c28bff822ed 100644 --- a/providers/samba/src/airflow/providers/samba/__init__.py +++ b/providers/samba/src/airflow/providers/samba/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "4.12.2" +__version__ = "4.12.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/segment/docs/.latest-doc-only-change.txt b/providers/segment/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/segment/docs/.latest-doc-only-change.txt +++ b/providers/segment/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/sendgrid/docs/.latest-doc-only-change.txt b/providers/sendgrid/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/sendgrid/docs/.latest-doc-only-change.txt +++ b/providers/sendgrid/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/sftp/README.rst b/providers/sftp/README.rst index 867e2bbdbf170..060fef7ae53e3 100644 --- a/providers/sftp/README.rst +++ b/providers/sftp/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-sftp`` -Release: ``5.7.0`` +Release: ``5.7.1`` `SSH File Transfer Protocol (SFTP) `__ @@ -36,7 +36,7 @@ This is a provider package for ``sftp`` provider. All classes for this provider are in ``airflow.providers.sftp`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -56,7 +56,7 @@ PIP package Version required ``apache-airflow`` ``>=2.11.0`` ``apache-airflow-providers-ssh`` ``>=4.0.0`` ``apache-airflow-providers-common-compat`` ``>=1.12.0`` -``paramiko`` ``>=2.9.0,<4.0.0`` +``paramiko`` ``>=3.4.0,<4.0.0`` ``asyncssh`` ``>=2.12.0`` ========================================== ================== @@ -92,4 +92,4 @@ Extra Dependencies =============== ======================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/sftp/docs/changelog.rst b/providers/sftp/docs/changelog.rst index 3ea2f4a97f700..3f21e158373d8 100644 --- a/providers/sftp/docs/changelog.rst +++ b/providers/sftp/docs/changelog.rst @@ -27,6 +27,20 @@ Changelog --------- +5.7.1 +..... + +Misc +~~~~ + +* ``Bump minimum cryptography to 44.0.3 and paramiko to 3.4.0 (#62723)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + * ``[Part 2] Migrate connection UI metadata to YAML for more providers (#62109)`` + 5.7.0 ..... diff --git a/providers/sftp/docs/index.rst b/providers/sftp/docs/index.rst index b8e79b2aab2dc..e84e67bd263b6 100644 --- a/providers/sftp/docs/index.rst +++ b/providers/sftp/docs/index.rst @@ -77,7 +77,7 @@ apache-airflow-providers-sftp package `SSH File Transfer Protocol (SFTP) `__ -Release: 5.7.0 +Release: 5.7.1 Provider package ---------------- @@ -134,5 +134,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-sftp 5.7.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-sftp 5.7.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-sftp 5.7.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-sftp 5.7.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/sftp/provider.yaml b/providers/sftp/provider.yaml index b763b8c49ce78..f9082754991cf 100644 --- a/providers/sftp/provider.yaml +++ b/providers/sftp/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1769461567 +source-date-epoch: 1773071130 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 5.7.1 - 5.7.0 - 5.6.0 - 5.5.1 diff --git a/providers/sftp/pyproject.toml b/providers/sftp/pyproject.toml index 0322cf9ae41ce..0b5094b8a2e2e 100644 --- a/providers/sftp/pyproject.toml +++ b/providers/sftp/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-sftp" -version = "5.7.0" +version = "5.7.1" description = "Provider package apache-airflow-providers-sftp for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -113,8 +113,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.7.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.7.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.7.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.7.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/sftp/src/airflow/providers/sftp/__init__.py b/providers/sftp/src/airflow/providers/sftp/__init__.py index 12dd3477cefa5..650766498cacc 100644 --- a/providers/sftp/src/airflow/providers/sftp/__init__.py +++ b/providers/sftp/src/airflow/providers/sftp/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.7.0" +__version__ = "5.7.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/singularity/docs/.latest-doc-only-change.txt b/providers/singularity/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/singularity/docs/.latest-doc-only-change.txt +++ b/providers/singularity/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/slack/README.rst b/providers/slack/README.rst index f1d7982968043..adfc818821949 100644 --- a/providers/slack/README.rst +++ b/providers/slack/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-slack`` -Release: ``9.7.0`` +Release: ``9.8.0`` `Slack `__ services integration including: @@ -39,7 +39,7 @@ This is a provider package for ``slack`` provider. All classes for this provider are in ``airflow.providers.slack`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -84,4 +84,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/slack/docs/changelog.rst b/providers/slack/docs/changelog.rst index 15ebd8e518742..1bdef66d5fb05 100644 --- a/providers/slack/docs/changelog.rst +++ b/providers/slack/docs/changelog.rst @@ -27,6 +27,17 @@ Changelog --------- +9.8.0 +..... + +Features +~~~~~~~~ + +* ``Add thread_ts parameter to Slack operators for thread replies (#62289)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 9.7.0 ..... diff --git a/providers/slack/docs/index.rst b/providers/slack/docs/index.rst index 19d9fdc5e344b..38095c90e9975 100644 --- a/providers/slack/docs/index.rst +++ b/providers/slack/docs/index.rst @@ -81,7 +81,7 @@ apache-airflow-providers-slack package - `Slack Incoming Webhook `__ -Release: 9.7.0 +Release: 9.8.0 Provider package ---------------- @@ -137,5 +137,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-slack 9.7.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-slack 9.7.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-slack 9.8.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-slack 9.8.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/slack/provider.yaml b/providers/slack/provider.yaml index c68232222619c..0f477e12658ec 100644 --- a/providers/slack/provider.yaml +++ b/providers/slack/provider.yaml @@ -26,12 +26,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772065264 +source-date-epoch: 1773071150 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 9.8.0 - 9.7.0 - 9.6.2 - 9.6.1 diff --git a/providers/slack/pyproject.toml b/providers/slack/pyproject.toml index 3b33b5b327991..3092429744fda 100644 --- a/providers/slack/pyproject.toml +++ b/providers/slack/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-slack" -version = "9.7.0" +version = "9.8.0" description = "Provider package apache-airflow-providers-slack for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -104,8 +104,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-slack/9.7.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-slack/9.7.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-slack/9.8.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-slack/9.8.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/slack/src/airflow/providers/slack/__init__.py b/providers/slack/src/airflow/providers/slack/__init__.py index 76faae8051e5d..ca7c532255cb0 100644 --- a/providers/slack/src/airflow/providers/slack/__init__.py +++ b/providers/slack/src/airflow/providers/slack/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "9.7.0" +__version__ = "9.8.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/smtp/README.rst b/providers/smtp/README.rst index d200725496af0..b8f30b46a40e9 100644 --- a/providers/smtp/README.rst +++ b/providers/smtp/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-smtp`` -Release: ``2.4.2`` +Release: ``2.4.3`` `Simple Mail Transfer Protocol (SMTP) `__ @@ -36,7 +36,7 @@ This is a provider package for ``smtp`` provider. All classes for this provider are in ``airflow.providers.smtp`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -78,4 +78,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/smtp/docs/changelog.rst b/providers/smtp/docs/changelog.rst index f9ea95f367dff..eb3f28d0f8a53 100644 --- a/providers/smtp/docs/changelog.rst +++ b/providers/smtp/docs/changelog.rst @@ -28,6 +28,21 @@ Changelog --------- +2.4.3 +..... + +Bug Fixes +~~~~~~~~~ + +* ``Fix OAuth2 XOAUTH2 auth and EHLO after STARTTLS in SmtpHook (#62879)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + * ``[Part 3] Migrate connection UI metadata to YAML for more providers (#62165)`` + * ``YAML first discovery for connection form metadata (#60410)`` + 2.4.2 ..... diff --git a/providers/smtp/docs/index.rst b/providers/smtp/docs/index.rst index 46136d870726a..cf471ea0b698b 100644 --- a/providers/smtp/docs/index.rst +++ b/providers/smtp/docs/index.rst @@ -64,7 +64,7 @@ apache-airflow-providers-smtp package `Simple Mail Transfer Protocol (SMTP) `__ -Release: 2.4.2 +Release: 2.4.3 Provider package ---------------- @@ -117,5 +117,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-smtp 2.4.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-smtp 2.4.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-smtp 2.4.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-smtp 2.4.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/smtp/provider.yaml b/providers/smtp/provider.yaml index 52dc0c5ce88ce..6ace7528fc96e 100644 --- a/providers/smtp/provider.yaml +++ b/providers/smtp/provider.yaml @@ -24,12 +24,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768335608 +source-date-epoch: 1773071172 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 2.4.3 - 2.4.2 - 2.4.1 - 2.4.0 diff --git a/providers/smtp/pyproject.toml b/providers/smtp/pyproject.toml index 1faab697ef3b0..d8dbc9532d712 100644 --- a/providers/smtp/pyproject.toml +++ b/providers/smtp/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-smtp" -version = "2.4.2" +version = "2.4.3" description = "Provider package apache-airflow-providers-smtp for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -98,8 +98,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-smtp/2.4.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-smtp/2.4.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-smtp/2.4.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-smtp/2.4.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/smtp/src/airflow/providers/smtp/__init__.py b/providers/smtp/src/airflow/providers/smtp/__init__.py index c5143d55da10d..585fd3c708794 100644 --- a/providers/smtp/src/airflow/providers/smtp/__init__.py +++ b/providers/smtp/src/airflow/providers/smtp/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "2.4.2" +__version__ = "2.4.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/snowflake/README.rst b/providers/snowflake/README.rst index 258daff7d417c..5f5e6442906e2 100644 --- a/providers/snowflake/README.rst +++ b/providers/snowflake/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-snowflake`` -Release: ``6.10.0`` +Release: ``6.11.0`` `Snowflake `__ @@ -36,7 +36,7 @@ This is a provider package for ``snowflake`` provider. All classes for this prov are in ``airflow.providers.snowflake`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -100,4 +100,4 @@ Extra Dependencies =================== ==================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/snowflake/docs/changelog.rst b/providers/snowflake/docs/changelog.rst index 80bb41ef89197..5919c7d833ecf 100644 --- a/providers/snowflake/docs/changelog.rst +++ b/providers/snowflake/docs/changelog.rst @@ -27,6 +27,23 @@ Changelog --------- +6.11.0 +...... + +Features +~~~~~~~~ + +* ``Allow SnowflakeHook + SnowflakeSqlApiHook 'private_key_content' to use raw key in addition to base64 encoding (#62378)`` + +Misc +~~~~ + +* ``Centralize OAuth grant_type validation in SnowflakeHook (#61969)`` +* ``Lazy load 'snowflake' imports in Snowflake provider. (#62365)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 6.10.0 ...... diff --git a/providers/snowflake/docs/index.rst b/providers/snowflake/docs/index.rst index 4114a92fdb873..e6bff2210729d 100644 --- a/providers/snowflake/docs/index.rst +++ b/providers/snowflake/docs/index.rst @@ -79,7 +79,7 @@ apache-airflow-providers-snowflake package `Snowflake `__ -Release: 6.10.0 +Release: 6.11.0 Provider package ---------------- @@ -144,5 +144,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-snowflake 6.10.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-snowflake 6.10.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-snowflake 6.11.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-snowflake 6.11.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/snowflake/provider.yaml b/providers/snowflake/provider.yaml index 8c60aefecc859..3b466b0e3388e 100644 --- a/providers/snowflake/provider.yaml +++ b/providers/snowflake/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772065306 +source-date-epoch: 1773071191 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 6.11.0 - 6.10.0 - 6.9.1 - 6.9.0 diff --git a/providers/snowflake/pyproject.toml b/providers/snowflake/pyproject.toml index 442fb8008098b..167941cc564b9 100644 --- a/providers/snowflake/pyproject.toml +++ b/providers/snowflake/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-snowflake" -version = "6.10.0" +version = "6.11.0" description = "Provider package apache-airflow-providers-snowflake for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -124,8 +124,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-snowflake/6.10.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-snowflake/6.10.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-snowflake/6.11.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-snowflake/6.11.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/snowflake/src/airflow/providers/snowflake/__init__.py b/providers/snowflake/src/airflow/providers/snowflake/__init__.py index 8fc3dd06e4795..700cb20e6f9f3 100644 --- a/providers/snowflake/src/airflow/providers/snowflake/__init__.py +++ b/providers/snowflake/src/airflow/providers/snowflake/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "6.10.0" +__version__ = "6.11.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/ssh/README.rst b/providers/ssh/README.rst index 019843f53b4a3..734636e872ae2 100644 --- a/providers/ssh/README.rst +++ b/providers/ssh/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-ssh`` -Release: ``4.3.1`` +Release: ``4.3.2`` `Secure Shell (SSH) `__ @@ -36,7 +36,7 @@ This is a provider package for ``ssh`` provider. All classes for this provider p are in ``airflow.providers.ssh`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -56,7 +56,7 @@ PIP package Version required ``apache-airflow`` ``>=2.11.0`` ``apache-airflow-providers-common-compat`` ``>=1.12.0`` ``asyncssh`` ``>=2.12.0`` -``paramiko`` ``>=2.9.0,<4.0.0`` +``paramiko`` ``>=3.4.0,<4.0.0`` ``sshtunnel`` ``>=0.3.2`` ========================================== ================== @@ -80,4 +80,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/ssh/docs/changelog.rst b/providers/ssh/docs/changelog.rst index a2f5d4b6d4faa..60740e88b3f03 100644 --- a/providers/ssh/docs/changelog.rst +++ b/providers/ssh/docs/changelog.rst @@ -27,6 +27,20 @@ Changelog --------- +4.3.2 +..... + +Misc +~~~~ + +* ``Bump minimum cryptography to 44.0.3 and paramiko to 3.4.0 (#62723)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + * ``[Part 2] Migrate connection UI metadata to YAML for more providers (#62109)`` + 4.3.1 ..... diff --git a/providers/ssh/docs/index.rst b/providers/ssh/docs/index.rst index 1081e2c748080..1b8517b630683 100644 --- a/providers/ssh/docs/index.rst +++ b/providers/ssh/docs/index.rst @@ -69,7 +69,7 @@ apache-airflow-providers-ssh package `Secure Shell (SSH) `__ -Release: 4.3.1 +Release: 4.3.2 Provider package ---------------- @@ -124,5 +124,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-ssh 4.3.1 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-ssh 4.3.1 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-ssh 4.3.2 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-ssh 4.3.2 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/ssh/provider.yaml b/providers/ssh/provider.yaml index 8971c35d2d0f6..6424cd6a267a1 100644 --- a/providers/ssh/provider.yaml +++ b/providers/ssh/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1769537303 +source-date-epoch: 1773071210 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 4.3.2 - 4.3.1 - 4.3.0 - 4.2.1 diff --git a/providers/ssh/pyproject.toml b/providers/ssh/pyproject.toml index 40966736a34ed..753ca8e48171a 100644 --- a/providers/ssh/pyproject.toml +++ b/providers/ssh/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-ssh" -version = "4.3.1" +version = "4.3.2" description = "Provider package apache-airflow-providers-ssh for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -101,8 +101,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-ssh/4.3.1" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-ssh/4.3.1/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-ssh/4.3.2" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-ssh/4.3.2/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/ssh/src/airflow/providers/ssh/__init__.py b/providers/ssh/src/airflow/providers/ssh/__init__.py index 0dc13683e2b2a..e9f8f6fb4e894 100644 --- a/providers/ssh/src/airflow/providers/ssh/__init__.py +++ b/providers/ssh/src/airflow/providers/ssh/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "4.3.1" +__version__ = "4.3.2" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/standard/README.rst b/providers/standard/README.rst index e934d97042206..5c61621ffd9ae 100644 --- a/providers/standard/README.rst +++ b/providers/standard/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-standard`` -Release: ``1.12.0`` +Release: ``1.12.1`` Airflow Standard Provider @@ -36,7 +36,7 @@ This is a provider package for ``standard`` provider. All classes for this provi are in ``airflow.providers.standard`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -54,7 +54,7 @@ Requirements PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.13.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` ========================================== ================== Cross provider package dependencies @@ -87,4 +87,4 @@ Extra Dependencies =============== ======================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/standard/docs/changelog.rst b/providers/standard/docs/changelog.rst index 598903059329c..77e8173ee2552 100644 --- a/providers/standard/docs/changelog.rst +++ b/providers/standard/docs/changelog.rst @@ -35,6 +35,24 @@ Changelog --------- +1.12.1 +...... + +Bug Fixes +~~~~~~~~~ + +* ``Fix PythonVirtualenvOperator cannot run with pendulum<3 (#62604)`` + +Misc +~~~~ + +* ``Consolidate 'SkipMixin' imports through 'common-compat' layer (#62776)`` +* ``Move SkipMixin and BranchMixIn to Task SDK (#62749)`` +* ``Move determine_kwargs and KeywordParameters to SDK DecoratedOperator (#62746)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 1.12.0 ...... diff --git a/providers/standard/docs/index.rst b/providers/standard/docs/index.rst index b482ca7a5f46a..9cf45e0868489 100644 --- a/providers/standard/docs/index.rst +++ b/providers/standard/docs/index.rst @@ -66,7 +66,7 @@ apache-airflow-providers-standard package Airflow Standard Provider -Release: 1.12.0 +Release: 1.12.1 Provider package ---------------- @@ -90,7 +90,7 @@ The minimum Apache Airflow version supported by this provider distribution is `` PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.13.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` ========================================== ================== Cross provider package dependencies @@ -119,5 +119,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-standard 1.12.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-standard 1.12.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-standard 1.12.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-standard 1.12.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/standard/provider.yaml b/providers/standard/provider.yaml index b815bab96aa9f..91e2498118a46 100644 --- a/providers/standard/provider.yaml +++ b/providers/standard/provider.yaml @@ -22,12 +22,13 @@ description: | Airflow Standard Provider state: ready lifecycle: production -source-date-epoch: 1772065352 +source-date-epoch: 1773071240 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 1.12.1 - 1.12.0 - 1.11.1 - 1.11.0 diff --git a/providers/standard/pyproject.toml b/providers/standard/pyproject.toml index d6d9df1972e70..0b20194009ebd 100644 --- a/providers/standard/pyproject.toml +++ b/providers/standard/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-standard" -version = "1.12.0" +version = "1.12.1" description = "Provider package apache-airflow-providers-standard for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -59,7 +59,7 @@ requires-python = ">=3.10" # After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build`` dependencies = [ "apache-airflow>=2.11.0", - "apache-airflow-providers-common-compat>=1.13.0", # use next version + "apache-airflow-providers-common-compat>=1.14.1", ] # The optional dependencies should be modified in place in the generated file @@ -106,8 +106,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-standard/1.12.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-standard/1.12.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-standard/1.12.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-standard/1.12.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/standard/src/airflow/providers/standard/__init__.py b/providers/standard/src/airflow/providers/standard/__init__.py index 9a041985f2332..609daa225234a 100644 --- a/providers/standard/src/airflow/providers/standard/__init__.py +++ b/providers/standard/src/airflow/providers/standard/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "1.12.0" +__version__ = "1.12.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/tableau/docs/.latest-doc-only-change.txt b/providers/tableau/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/tableau/docs/.latest-doc-only-change.txt +++ b/providers/tableau/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/telegram/docs/.latest-doc-only-change.txt b/providers/telegram/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/telegram/docs/.latest-doc-only-change.txt +++ b/providers/telegram/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/weaviate/docs/.latest-doc-only-change.txt b/providers/weaviate/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/weaviate/docs/.latest-doc-only-change.txt +++ b/providers/weaviate/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/zendesk/docs/.latest-doc-only-change.txt b/providers/zendesk/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/zendesk/docs/.latest-doc-only-change.txt +++ b/providers/zendesk/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 From dc63d8433ce52ba95b5f6d02f352f023672ecc91 Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Tue, 10 Mar 2026 00:48:20 +0000 Subject: [PATCH 082/280] Clean up stale pagefind index files from S3 during registry builds (#63234) --- .github/workflows/registry-build.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/registry-build.yml b/.github/workflows/registry-build.yml index 094820c539f8b..0d35d02417b22 100644 --- a/.github/workflows/registry-build.yml +++ b/.github/workflows/registry-build.yml @@ -239,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: From 85d6e34760458089233d33a5ec0512dc909eae73 Mon Sep 17 00:00:00 2001 From: Xiaodong DENG Date: Mon, 9 Mar 2026 17:57:08 -0700 Subject: [PATCH 083/280] Fix undefined variable in install_from_external_spec error message (#63233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The error message in install_from_external_spec() referenced ${INSTALLATION_METHOD} which does not exist — the correct variable is ${AIRFLOW_INSTALLATION_METHOD}. With set -u active, hitting this error path would crash with an "unbound variable" error instead of printing the intended user-friendly message. The typo was introduced in a1717a652b and carried forward into the inlined copies in both Dockerfiles. --- Dockerfile | 2 +- Dockerfile.ci | 2 +- scripts/docker/install_airflow_when_building_images.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 251b589564d43..aa3f1fba405bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/Dockerfile.ci b/Dockerfile.ci index 5b0e6f7eaefc0..55229964f2412 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 diff --git a/scripts/docker/install_airflow_when_building_images.sh b/scripts/docker/install_airflow_when_building_images.sh index 5341a1cfc331b..31b2c4062d4e7 100644 --- a/scripts/docker/install_airflow_when_building_images.sh +++ b/scripts/docker/install_airflow_when_building_images.sh @@ -148,7 +148,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 From 7e60b31884612f45df6b3b3da94b11c3e2e7ffa3 Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Tue, 10 Mar 2026 01:06:15 +0000 Subject: [PATCH 084/280] Fix codespell failures on main (#63236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit by-passes → bypasses, by-passing → bypassing, pre-selected → preselected --- airflow-core/docs/tutorial/hitl.rst | 2 +- .../airflow/api_fastapi/execution_api/routes/task_instances.py | 2 +- .../unit/api_fastapi/execution_api/versions/head/test_xcoms.py | 2 +- go-sdk/pkg/api/client.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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/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..53e3bbb2a9f9c 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 @@ -133,7 +133,7 @@ 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), ) 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..554c2ad2c8437 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 @@ -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/go-sdk/pkg/api/client.go b/go-sdk/pkg/api/client.go index f8abbf99cbc18..fd7b77fd3954c 100644 --- a/go-sdk/pkg/api/client.go +++ b/go-sdk/pkg/api/client.go @@ -61,7 +61,7 @@ func (c *Client) WithBearerToken(token string) (ClientInterface, error) { // We don't use SetAuthToken/SetAuthScheme, as that produces a (valid, but annoying) warning about using Auth // over HTTP: "Using sensitive credentials in HTTP mode is not secure." It's a time-limited-token though, so we - // can reasonably ignore that here and setting the header directly by-passes that + // can reasonably ignore that here and setting the header directly bypasses that rc.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) opts := []ClientOption{ From 2d5579336b593414d186fba1d82a8c248d527522 Mon Sep 17 00:00:00 2001 From: "D. Ferruzzi" Date: Mon, 9 Mar 2026 18:22:34 -0700 Subject: [PATCH 085/280] Docs updates for Deadlines for 3.2 (#62494) * Docs updates for Deadlines for 3.2 --- .../logging-monitoring/callbacks.rst | 10 ++ airflow-core/docs/howto/deadline-alerts.rst | 150 ++++++++++++++++-- .../newsfragments/61153.significant.rst | 19 +++ 3 files changed, 163 insertions(+), 16 deletions(-) create mode 100644 airflow-core/newsfragments/61153.significant.rst 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 25838377e5c25..9cbd3b2976bb3 100644 --- a/airflow-core/docs/administration-and-deployment/logging-monitoring/callbacks.rst +++ b/airflow-core/docs/administration-and-deployment/logging-monitoring/callbacks.rst @@ -163,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/howto/deadline-alerts.rst b/airflow-core/docs/howto/deadline-alerts.rst index 643e17fc185fb..1ed9750bf4e2e 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 @@ -386,8 +463,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 +476,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/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 From bfd35093d6767a528d7608524034d8e33e63ebb4 Mon Sep 17 00:00:00 2001 From: Jed Cunningham <66968678+jedcunningham@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:53:55 -0600 Subject: [PATCH 086/280] Fix typo in listener warning (#63238) --- airflow-core/docs/administration-and-deployment/listeners.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 069832064d09e3a01a44eb397c50abb96e225bca Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Tue, 10 Mar 2026 03:58:29 +0000 Subject: [PATCH 087/280] Add Matomo analytics to the provider registry (#63239) The Airflow landing pages and Sphinx docs both track page views via Apache's Matomo instance (analytics.apache.org, site ID 13). The registry was missing this. Add the same snippet to the base layout template so all registry pages are tracked under the same property. Cookies are disabled (privacy-first) and the script loads async. --- registry/src/_includes/base.njk | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/registry/src/_includes/base.njk b/registry/src/_includes/base.njk index d2092ebd38097..02906819bb4b4 100644 --- a/registry/src/_includes/base.njk +++ b/registry/src/_includes/base.njk @@ -34,6 +34,20 @@ + + From fcbb6cd4c0604bd00806b728d10044a5f1fe0447 Mon Sep 17 00:00:00 2001 From: "Jason(Zhe-You) Liu" <68415893+jason810496@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:13:20 +0800 Subject: [PATCH 088/280] Ensure all the PR links in auto-triage are clickable (#63241) --- .../airflow_breeze/commands/pr_commands.py | 108 +++++++++--------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/dev/breeze/src/airflow_breeze/commands/pr_commands.py b/dev/breeze/src/airflow_breeze/commands/pr_commands.py index 04cba80fd8d84..288dbb7e7c0f6 100644 --- a/dev/breeze/src/airflow_breeze/commands/pr_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/pr_commands.py @@ -945,6 +945,11 @@ def _compute_default_action( return action, f"{reason} — suggesting {action_label}" +def _pr_link(pr: PRData) -> str: + """Return a Rich-markup clickable link for a PR: [link=url]#number[/link].""" + return f"[link={pr.url}]#{pr.number}[/link]" + + def _display_pr_info_panels(pr: PRData, author_profile: dict | None): """Display PR info and author panels (shared by flagged-PR and workflow-approval flows).""" console = get_console() @@ -1098,10 +1103,10 @@ def _close_suspicious_prs( node_id = pr_info["node_id"] if _close_pr(token, node_id): - get_console().print(f" [success]PR #{pr_num} closed.[/]") + get_console().print(f" [success]PR [link={pr_info['url']}]#{pr_num}[/link] closed.[/]") closed += 1 else: - get_console().print(f" [error]Failed to close PR #{pr_num}.[/]") + get_console().print(f" [error]Failed to close PR [link={pr_info['url']}]#{pr_num}[/link].[/]") continue _add_label(token, github_repository, node_id, _SUSPICIOUS_CHANGES_LABEL) @@ -1322,7 +1327,7 @@ def auto_triage( overall = "[green]OK[/]" pr_table.add_row( - f"[link={pr.url}]#{pr.number}[/link]", + _pr_link(pr), pr.title[:50], pr.author_login, pr.author_association.lower(), @@ -1345,22 +1350,18 @@ def auto_triage( total_skipped_collaborator += 1 if verbose: get_console().print( - f" [dim]Skipping PR [link={pr.url}]#{pr.number}[/link] by " + f" [dim]Skipping PR {_pr_link(pr)} by " f"{pr.author_association.lower()} {pr.author_login}[/]" ) elif _is_bot_account(pr.author_login): total_skipped_bot += 1 if verbose: - get_console().print( - f" [dim]Skipping PR [link={pr.url}]#{pr.number}[/link] — " - f"bot account {pr.author_login}[/]" - ) + get_console().print(f" [dim]Skipping PR {_pr_link(pr)} — bot account {pr.author_login}[/]") elif _READY_FOR_REVIEW_LABEL in pr.labels: total_skipped_accepted += 1 if verbose: get_console().print( - f" [dim]Skipping PR [link={pr.url}]#{pr.number}[/link] — " - f"already has '{_READY_FOR_REVIEW_LABEL}' label[/]" + f" [dim]Skipping PR {_pr_link(pr)} — already has '{_READY_FOR_REVIEW_LABEL}' label[/]" ) else: candidate_prs.append(pr) @@ -1394,7 +1395,7 @@ def auto_triage( for pr in candidate_prs: if pr.checks_state == "FAILURE" and not pr.failed_checks and pr.head_sha: get_console().print( - f" [dim]Fetching full check details for PR #{pr.number} " + f" [dim]Fetching full check details for PR {_pr_link(pr)} " f"(failures beyond first 100 checks)...[/]" ) pr.failed_checks = _fetch_failed_checks(token, github_repository, pr.head_sha) @@ -1474,9 +1475,7 @@ def auto_triage( total_llm_errors += 1 continue if not assessment.should_flag: - get_console().print( - f" [success]PR [link={pr.url}]#{pr.number}[/link] passes quality check.[/]" - ) + get_console().print(f" [success]PR {_pr_link(pr)} passes quality check.[/]") continue assessments[pr.number] = assessment @@ -1549,7 +1548,9 @@ def auto_triage( continue action = prompt_triage_action( - f"Action for PR #{pr.number}?", default=default_action, forced_answer=answer_triage + f"Action for PR {_pr_link(pr)}?", + default=default_action, + forced_answer=answer_triage, ) if action == TriageAction.QUIT: @@ -1558,60 +1559,60 @@ def auto_triage( break if action == TriageAction.SKIP: - get_console().print(f" [info]Skipping PR #{pr.number} — no action taken.[/]") + get_console().print(f" [info]Skipping PR {_pr_link(pr)} — no action taken.[/]") total_skipped_action += 1 continue if action == TriageAction.READY: get_console().print( - f" [info]Marking PR #{pr.number} as ready — adding '{_READY_FOR_REVIEW_LABEL}' label.[/]" + f" [info]Marking PR {_pr_link(pr)} as ready — adding '{_READY_FOR_REVIEW_LABEL}' label.[/]" ) if _add_label(token, github_repository, pr.node_id, _READY_FOR_REVIEW_LABEL): get_console().print( - f" [success]Label '{_READY_FOR_REVIEW_LABEL}' added to PR #{pr.number}.[/]" + f" [success]Label '{_READY_FOR_REVIEW_LABEL}' added to PR {_pr_link(pr)}.[/]" ) total_ready += 1 else: - get_console().print(f" [warning]Failed to add label to PR #{pr.number}.[/]") + get_console().print(f" [warning]Failed to add label to PR {_pr_link(pr)}.[/]") continue if action == TriageAction.DRAFT: - get_console().print(f" Converting PR #{pr.number} to draft...") + get_console().print(f" Converting PR {_pr_link(pr)} to draft...") if _convert_pr_to_draft(token, pr.node_id): - get_console().print(f" [success]PR #{pr.number} converted to draft.[/]") + get_console().print(f" [success]PR {_pr_link(pr)} converted to draft.[/]") else: - get_console().print(f" [error]Failed to convert PR #{pr.number} to draft.[/]") + get_console().print(f" [error]Failed to convert PR {_pr_link(pr)} to draft.[/]") continue - get_console().print(f" Posting comment on PR #{pr.number}...") + get_console().print(f" Posting comment on PR {_pr_link(pr)}...") if _post_comment(token, pr.node_id, comment): - get_console().print(f" [success]Comment posted on PR #{pr.number}.[/]") + get_console().print(f" [success]Comment posted on PR {_pr_link(pr)}.[/]") total_converted += 1 else: - get_console().print(f" [error]Failed to post comment on PR #{pr.number}.[/]") + get_console().print(f" [error]Failed to post comment on PR {_pr_link(pr)}.[/]") continue if action == TriageAction.CLOSE: - get_console().print(f" Closing PR #{pr.number}...") + get_console().print(f" Closing PR {_pr_link(pr)}...") if _close_pr(token, pr.node_id): - get_console().print(f" [success]PR #{pr.number} closed.[/]") + get_console().print(f" [success]PR {_pr_link(pr)} closed.[/]") else: - get_console().print(f" [error]Failed to close PR #{pr.number}.[/]") + get_console().print(f" [error]Failed to close PR {_pr_link(pr)}.[/]") continue if _add_label(token, github_repository, pr.node_id, _CLOSED_QUALITY_LABEL): get_console().print( - f" [success]Label '{_CLOSED_QUALITY_LABEL}' added to PR #{pr.number}.[/]" + f" [success]Label '{_CLOSED_QUALITY_LABEL}' added to PR {_pr_link(pr)}.[/]" ) else: - get_console().print(f" [warning]Failed to add label to PR #{pr.number}.[/]") + get_console().print(f" [warning]Failed to add label to PR {_pr_link(pr)}.[/]") - get_console().print(f" Posting comment on PR #{pr.number}...") + get_console().print(f" Posting comment on PR {_pr_link(pr)}...") if _post_comment(token, pr.node_id, close_comment): - get_console().print(f" [success]Comment posted on PR #{pr.number}.[/]") + get_console().print(f" [success]Comment posted on PR {_pr_link(pr)}.[/]") total_closed += 1 else: - get_console().print(f" [error]Failed to post comment on PR #{pr.number}.[/]") + get_console().print(f" [error]Failed to post comment on PR {_pr_link(pr)}.[/]") # Phase 6: Present NOT_RUN PRs for workflow approval total_workflows_approved = 0 @@ -1642,7 +1643,7 @@ def auto_triage( continue action = prompt_triage_action( - f"Action for PR #{pr.number}?", + f"Action for PR {_pr_link(pr)}?", default=TriageAction.CLOSE, forced_answer=answer_triage, ) @@ -1651,27 +1652,27 @@ def auto_triage( quit_early = True break if action == TriageAction.SKIP: - get_console().print(f" [info]Skipping PR #{pr.number} — no action taken.[/]") + get_console().print(f" [info]Skipping PR {_pr_link(pr)} — no action taken.[/]") continue if action == TriageAction.CLOSE: - get_console().print(f" Closing PR #{pr.number}...") + get_console().print(f" Closing PR {_pr_link(pr)}...") if _close_pr(token, pr.node_id): - get_console().print(f" [success]PR #{pr.number} closed.[/]") + get_console().print(f" [success]PR {_pr_link(pr)} closed.[/]") else: - get_console().print(f" [error]Failed to close PR #{pr.number}.[/]") + get_console().print(f" [error]Failed to close PR {_pr_link(pr)}.[/]") continue if _add_label(token, github_repository, pr.node_id, _CLOSED_QUALITY_LABEL): get_console().print( - f" [success]Label '{_CLOSED_QUALITY_LABEL}' added to PR #{pr.number}.[/]" + f" [success]Label '{_CLOSED_QUALITY_LABEL}' added to PR {_pr_link(pr)}.[/]" ) else: - get_console().print(f" [warning]Failed to add label to PR #{pr.number}.[/]") - get_console().print(f" Posting comment on PR #{pr.number}...") + get_console().print(f" [warning]Failed to add label to PR {_pr_link(pr)}.[/]") + get_console().print(f" Posting comment on PR {_pr_link(pr)}...") if _post_comment(token, pr.node_id, close_comment): - get_console().print(f" [success]Comment posted on PR #{pr.number}.[/]") + get_console().print(f" [success]Comment posted on PR {_pr_link(pr)}.[/]") total_closed += 1 else: - get_console().print(f" [error]Failed to post comment on PR #{pr.number}.[/]") + get_console().print(f" [error]Failed to post comment on PR {_pr_link(pr)}.[/]") continue # For DRAFT or READY, fall through to normal workflow approval # (approve workflows first, then triage later) @@ -1682,13 +1683,13 @@ def auto_triage( if not pending_runs: get_console().print( - f" [dim]No pending workflow runs found for PR #{pr.number}. " + f" [dim]No pending workflow runs found for PR {_pr_link(pr)}. " f"Workflows may need to be triggered manually.[/]" ) continue answer = user_confirm( - f"Review diff for PR #{pr.number} before approving workflows?", + f"Review diff for PR {_pr_link(pr)} before approving workflows?", forced_answer=answer_triage, ) if answer == Answer.QUIT: @@ -1696,10 +1697,10 @@ def auto_triage( quit_early = True break if answer == Answer.NO: - get_console().print(f" [info]Skipping workflow approval for PR #{pr.number}.[/]") + get_console().print(f" [info]Skipping workflow approval for PR {_pr_link(pr)}.[/]") continue - get_console().print(f" Fetching diff for PR #{pr.number}...") + get_console().print(f" Fetching diff for PR {_pr_link(pr)}...") diff_text = _fetch_pr_diff(token, github_repository, pr.number) if diff_text: from rich.syntax import Syntax @@ -1707,18 +1708,18 @@ def auto_triage( get_console().print( Panel( Syntax(diff_text, "diff", theme="monokai", word_wrap=True), - title=f"Diff for PR #{pr.number}", + title=f"Diff for PR {_pr_link(pr)}", border_style="bright_cyan", ) ) else: get_console().print( - f" [warning]Could not fetch diff for PR #{pr.number}. " + f" [warning]Could not fetch diff for PR {_pr_link(pr)}. " f"Review manually at: {pr.url}/files[/]" ) answer = user_confirm( - f"No suspicious changes found in PR #{pr.number}? " + f"No suspicious changes found in PR {_pr_link(pr)}? " f"Approve {len(pending_runs)} workflow {'runs' if len(pending_runs) != 1 else 'run'}?", forced_answer=answer_triage, ) @@ -1728,7 +1729,7 @@ def auto_triage( break if answer == Answer.NO: get_console().print( - f"\n [bold red]Suspicious changes detected in PR #{pr.number} by {pr.author_login}.[/]" + f"\n [bold red]Suspicious changes detected in PR {_pr_link(pr)} by {pr.author_login}.[/]" ) get_console().print(f" Fetching all open PRs by {pr.author_login}...") author_prs = _fetch_author_open_prs(token, github_repository, pr.author_login) @@ -1774,11 +1775,12 @@ def auto_triage( if approved: get_console().print( f" [success]Approved {approved}/{len(pending_runs)} workflow " - f"{'runs' if len(pending_runs) != 1 else 'run'} for PR #{pr.number}.[/]" + f"{'runs' if len(pending_runs) != 1 else 'run'} for PR " + f"{_pr_link(pr)}.[/]" ) total_workflows_approved += 1 else: - get_console().print(f" [error]Failed to approve workflow runs for PR #{pr.number}.[/]") + get_console().print(f" [error]Failed to approve workflow runs for PR {_pr_link(pr)}.[/]") # Summary get_console().print() From 6f48835c1bac7cf91e74958895cfd6bd29037b7c Mon Sep 17 00:00:00 2001 From: yuseok89 Date: Tue, 10 Mar 2026 20:52:56 +0900 Subject: [PATCH 089/280] Fix RenderedJsonField not displaying in table cells (#63245) --- .../ui/src/components/RenderedJsonField.tsx | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) 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 ? ( From c71db6f727be39166c87d8eb0199f338f31814fd Mon Sep 17 00:00:00 2001 From: Pierre Jeambrun Date: Tue, 10 Mar 2026 14:16:21 +0100 Subject: [PATCH 090/280] Upgrade UI core dependencies (#63252) * chore(deps): bump the core-ui-package-updates group across 1 directory with 32 updates Bumps the core-ui-package-updates group with 32 updates in the /airflow-core/src/airflow/ui directory: | Package | From | To | | --- | --- | --- | | [@chakra-ui/react](https://github.com/chakra-ui/chakra-ui/tree/HEAD/packages/react) | `3.20.0` | `3.34.0` | | [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.12` | `5.90.21` | | [@tanstack/react-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual) | `3.13.12` | `3.13.19` | | [@xyflow/react](https://github.com/xyflow/xyflow/tree/HEAD/packages/react) | `12.10.0` | `12.10.1` | | [anser](https://github.com/IonicaBizau/anser) | `2.3.3` | `2.3.5` | | [axios](https://github.com/axios/axios) | `1.13.5` | `1.13.6` | | [elkjs](https://github.com/kieler/elkjs) | `0.11.0` | `0.11.1` | | [i18next](https://github.com/i18next/i18next) | `25.7.1` | `25.8.13` | | [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector) | `8.2.0` | `8.2.1` | | [react-chartjs-2](https://github.com/reactchartjs/react-chartjs-2) | `5.3.0` | `5.3.1` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.56.2` | `7.71.2` | | [react-icons](https://github.com/react-icons/react-icons) | `5.5.0` | `5.6.0` | | [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.12.0` | `7.13.1` | | [use-debounce](https://github.com/xnimorz/use-debounce) | `10.0.4` | `10.1.0` | | [yaml](https://github.com/eemeli/yaml) | `2.8.0` | `2.8.2` | | [zustand](https://github.com/pmndrs/zustand) | `5.0.4` | `5.0.11` | | [@playwright/test](https://github.com/microsoft/playwright) | `1.57.0` | `1.58.2` | | [@tanstack/eslint-plugin-query](https://github.com/TanStack/query/tree/HEAD/packages/eslint-plugin-query) | `5.91.2` | `5.91.4` | | [@testing-library/react](https://github.com/testing-library/react-testing-library) | `16.3.0` | `16.3.2` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.7` | `19.2.14` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.49.0` | `8.56.1` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.49.0` | `8.56.1` | | [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) | `8.49.0` | `8.56.1` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `5.1.2` | `5.1.4` | | [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react-swc) | `4.2.2` | `4.2.3` | | [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) | `5.5.4` | `5.5.5` | | [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.4.24` | `0.5.2` | | [happy-dom](https://github.com/capricorn86/happy-dom) | `20.0.11` | `20.8.3` | | [msw](https://github.com/mswjs/msw) | `2.12.4` | `2.12.10` | | [prettier](https://github.com/prettier/prettier) | `3.7.4` | `3.8.1` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.48.1` | `8.56.1` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.2.6` | `7.3.1` | Updates `@chakra-ui/react` from 3.20.0 to 3.34.0 - [Release notes](https://github.com/chakra-ui/chakra-ui/releases) - [Changelog](https://github.com/chakra-ui/chakra-ui/blob/main/packages/react/CHANGELOG.md) - [Commits](https://github.com/chakra-ui/chakra-ui/commits/@chakra-ui/react@3.34.0/packages/react) Updates `@tanstack/react-query` from 5.90.12 to 5.90.21 - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.21/packages/react-query) Updates `@tanstack/react-virtual` from 3.13.12 to 3.13.19 - [Release notes](https://github.com/TanStack/virtual/releases) - [Changelog](https://github.com/TanStack/virtual/blob/main/packages/react-virtual/CHANGELOG.md) - [Commits](https://github.com/TanStack/virtual/commits/@tanstack/react-virtual@3.13.19/packages/react-virtual) Updates `@xyflow/react` from 12.10.0 to 12.10.1 - [Release notes](https://github.com/xyflow/xyflow/releases) - [Changelog](https://github.com/xyflow/xyflow/blob/main/packages/react/CHANGELOG.md) - [Commits](https://github.com/xyflow/xyflow/commits/@xyflow/react@12.10.1/packages/react) Updates `anser` from 2.3.3 to 2.3.5 - [Release notes](https://github.com/IonicaBizau/anser/releases) - [Commits](https://github.com/IonicaBizau/anser/compare/2.3.3...2.3.5) Updates `axios` from 1.13.5 to 1.13.6 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.13.5...v1.13.6) Updates `elkjs` from 0.11.0 to 0.11.1 - [Release notes](https://github.com/kieler/elkjs/releases) - [Commits](https://github.com/kieler/elkjs/compare/0.11.0...0.11.1) Updates `i18next` from 25.7.1 to 25.8.13 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v25.7.1...v25.8.13) Updates `i18next-browser-languagedetector` from 8.2.0 to 8.2.1 - [Changelog](https://github.com/i18next/i18next-browser-languageDetector/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next-browser-languageDetector/compare/v8.2.0...v8.2.1) Updates `react-chartjs-2` from 5.3.0 to 5.3.1 - [Release notes](https://github.com/reactchartjs/react-chartjs-2/releases) - [Changelog](https://github.com/reactchartjs/react-chartjs-2/blob/master/CHANGELOG.md) - [Commits](https://github.com/reactchartjs/react-chartjs-2/compare/v5.3.0...v5.3.1) Updates `react-hook-form` from 7.56.2 to 7.71.2 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.56.2...v7.71.2) Updates `react-icons` from 5.5.0 to 5.6.0 - [Release notes](https://github.com/react-icons/react-icons/releases) - [Commits](https://github.com/react-icons/react-icons/compare/v5.5.0...v5.6.0) Updates `react-router-dom` from 7.12.0 to 7.13.1 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.13.1/packages/react-router-dom) Updates `use-debounce` from 10.0.4 to 10.1.0 - [Release notes](https://github.com/xnimorz/use-debounce/releases) - [Changelog](https://github.com/xnimorz/use-debounce/blob/master/CHANGELOG.md) - [Commits](https://github.com/xnimorz/use-debounce/commits) Updates `yaml` from 2.8.0 to 2.8.2 - [Release notes](https://github.com/eemeli/yaml/releases) - [Commits](https://github.com/eemeli/yaml/compare/v2.8.0...v2.8.2) Updates `zustand` from 5.0.4 to 5.0.11 - [Release notes](https://github.com/pmndrs/zustand/releases) - [Commits](https://github.com/pmndrs/zustand/compare/v5.0.4...v5.0.11) Updates `@playwright/test` from 1.57.0 to 1.58.2 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.57.0...v1.58.2) Updates `@tanstack/eslint-plugin-query` from 5.91.2 to 5.91.4 - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/eslint-plugin-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/eslint-plugin-query@5.91.4/packages/eslint-plugin-query) Updates `@testing-library/react` from 16.3.0 to 16.3.2 - [Release notes](https://github.com/testing-library/react-testing-library/releases) - [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md) - [Commits](https://github.com/testing-library/react-testing-library/compare/v16.3.0...v16.3.2) Updates `@types/react` from 19.2.7 to 19.2.14 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `@typescript-eslint/eslint-plugin` from 8.49.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.49.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/parser) Updates `@typescript-eslint/utils` from 8.49.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/utils) Updates `@vitejs/plugin-react` from 5.1.2 to 5.1.4 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.1.4/packages/plugin-react) Updates `@vitejs/plugin-react-swc` from 4.2.2 to 4.2.3 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react-swc@4.2.3/packages/plugin-react-swc) Updates `eslint-plugin-prettier` from 5.5.4 to 5.5.5 - [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases) - [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.5.4...v5.5.5) Updates `eslint-plugin-react-refresh` from 0.4.24 to 0.5.2 - [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases) - [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md) - [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.4.24...v0.5.2) Updates `happy-dom` from 20.0.11 to 20.8.3 - [Release notes](https://github.com/capricorn86/happy-dom/releases) - [Commits](https://github.com/capricorn86/happy-dom/compare/v20.0.11...v20.8.3) Updates `msw` from 2.12.4 to 2.12.10 - [Release notes](https://github.com/mswjs/msw/releases) - [Changelog](https://github.com/mswjs/msw/blob/main/CHANGELOG.md) - [Commits](https://github.com/mswjs/msw/compare/v2.12.4...v2.12.10) Updates `prettier` from 3.7.4 to 3.8.1 - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.7.4...3.8.1) Updates `typescript-eslint` from 8.48.1 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/typescript-eslint) Updates `vite` from 7.2.6 to 7.3.1 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite) --- updated-dependencies: - dependency-name: "@chakra-ui/react" dependency-version: 3.34.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: "@tanstack/react-query" dependency-version: 5.90.21 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@tanstack/react-virtual" dependency-version: 3.13.19 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@xyflow/react" dependency-version: 12.10.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: anser dependency-version: 2.3.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: axios dependency-version: 1.13.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: elkjs dependency-version: 0.11.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: i18next dependency-version: 25.8.13 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: i18next-browser-languagedetector dependency-version: 8.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: react-chartjs-2 dependency-version: 5.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: react-hook-form dependency-version: 7.71.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: react-icons dependency-version: 5.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: react-router-dom dependency-version: 7.13.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: use-debounce dependency-version: 10.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: yaml dependency-version: 2.8.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: zustand dependency-version: 5.0.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@playwright/test" dependency-version: 1.58.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: "@tanstack/eslint-plugin-query" dependency-version: 5.91.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@testing-library/react" dependency-version: 16.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@types/react" dependency-version: 19.2.14 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: "@typescript-eslint/parser" dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: "@typescript-eslint/utils" dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: "@vitejs/plugin-react" dependency-version: 5.1.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@vitejs/plugin-react-swc" dependency-version: 4.2.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: eslint-plugin-prettier dependency-version: 5.5.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: eslint-plugin-react-refresh dependency-version: 0.5.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: happy-dom dependency-version: 20.8.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: msw dependency-version: 2.12.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: prettier dependency-version: 3.8.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: typescript-eslint dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: vite dependency-version: 7.3.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates ... Signed-off-by: dependabot[bot] * Fix CI --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- airflow-core/src/airflow/ui/package.json | 64 +- airflow-core/src/airflow/ui/pnpm-lock.yaml | 3070 +++++++++-------- .../ui/src/components/AnsiRenderer.tsx | 1 + .../ui/src/components/DataTable/types.ts | 2 +- .../ui/src/components/renderStructuredLog.tsx | 1 + .../src/pages/TaskInstance/Logs/Logs.test.tsx | 2 +- .../TaskInstance/Logs/TaskLogContent.tsx | 2 +- .../src/airflow/ui/src/queries/useLogs.tsx | 1 + .../src/airflow/ui/src/utils/slots.tsx | 2 + 9 files changed, 1660 insertions(+), 1485 deletions(-) diff --git a/airflow-core/src/airflow/ui/package.json b/airflow-core/src/airflow/ui/package.json index 1c61c86b4352b..97c6685793ea3 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.19", "@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" diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml b/airflow-core/src/airflow/ui/pnpm-lock.yaml index ab7277e16e60e..4a698017fa63c 100644 --- a/airflow-core/src/airflow/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml @@ -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.19 + version: 3.13.19(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==} @@ -1048,68 +1114,68 @@ 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] - '@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] - '@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] - '@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] - '@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 +1186,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 +1216,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.19': + resolution: {integrity: sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ==} 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 +1226,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.19': + resolution: {integrity: sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g==} '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -1167,8 +1237,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 +1369,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 +1378,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 +1391,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 +1409,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 +1497,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 +1547,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.35.3': + resolution: {integrity: sha512-wmw6yo5Zr6ShiKGTc5ICEOJCurWAOSGubIpGISiHi3cZ4tlxKF/vpATIUT3eq8xzdB56YK57yKCujs/WmwqqoA==} + + '@zag-js/anatomy@1.35.3': + resolution: {integrity: sha512-oqU9iLNNylrtJMBX5Xu4DsxnPNvtZLiobryv2oNtsDI1mi1Fca/XHghQC9K5aYT0qNsmHj1M3W5WAWTaOtPLkQ==} - '@zag-js/accordion@1.15.0': - resolution: {integrity: sha512-EKNeuKx+lOQ/deCe/ApCjVPxpxpDwT2NXvMPL+YvqXmSv7hAnTLs9fDKjbDUQUMmsyx32BsBd8t6d17DL3rPXg==} + '@zag-js/angle-slider@1.35.3': + resolution: {integrity: sha512-HXRlmsbNEJSBT53fq9XQKL/vwZWwJC3nprskI7s4f/jy8a4uXPTlv7N7zuBYjew+ScTMzZah6fLWzUztBehmSg==} - '@zag-js/anatomy@1.15.0': - resolution: {integrity: sha512-r0l5I7mSsF35HdwXm22TppNhfVftFuqvKfHvTUw+wQZhni4eUL93HypJD0Fl7mDhtP5zfVGfBwR048OzD0+tCw==} + '@zag-js/aria-hidden@1.35.3': + resolution: {integrity: sha512-dk5POebn10WneQfLrEgbTzwolaXWpCSHL6F3jCTinW9IbOx7BXghzJD21iU5Iun+y9CorqJPW3p7LplYNUMO5Q==} - '@zag-js/angle-slider@1.15.0': - resolution: {integrity: sha512-xIZBa9V6d05uK7+XQVhfdsThqbZKimSYVxtMOWJfG0sKn63N9VGPxL1OtOMq7FA4IP3SyvlelsGt+3t82TUiyA==} + '@zag-js/async-list@1.35.3': + resolution: {integrity: sha512-SXX3wGzLK/maKS1PJ3XfLIGWbu0022f/OhcFsT1PbiHnoFZTH7h2fBhirrCBfy2TYFQ6r5uxgjkhPUNkuaeYnA==} - '@zag-js/aria-hidden@1.15.0': - resolution: {integrity: sha512-3ogglAasycekTHI34ph16mqwM+VtHCOMtrFHWzPwB16itV5oDEeeMNdQXenHSSyQ/07nJ2QsRGFFjGhPm1kWNg==} + '@zag-js/auto-resize@1.35.3': + resolution: {integrity: sha512-ufG8HSqzLd9h5rnos8aumj8iORlRskeR/gbpJu1NHrnHBWIrpuXm6KJJR2oZhTFY1BUMMk8eYIBA2QkVuiJzWA==} - '@zag-js/auto-resize@1.15.0': - resolution: {integrity: sha512-EXgrsU7OWxc7obSOt8Okh0144H8DQi1S84OsOUY04Uni11Dnp5/X8+t6mvBbkw4/Qyz5UBjChjocwBcO+HHV8w==} + '@zag-js/avatar@1.35.3': + resolution: {integrity: sha512-lbQ2Q4Va8AAScKULOHw2tCQez+0JRYGHSMFq6i+dJmeT3dlSgRanm69ra6K2po6hM9E4v6pRe+xOVE+9QMDnuA==} - '@zag-js/avatar@1.15.0': - resolution: {integrity: sha512-EHGxzXb1mLf3n6x0z/rqFl1mghDB/gyfPAeaFUoA/cacmmMk8YB3aDUXkS9pTgN9stYJBM5f6T4xB1ZUhrP8tg==} + '@zag-js/carousel@1.35.3': + resolution: {integrity: sha512-F+b8HzUeZfB+xUkAkLG4r0Ubui8pj7pSgZhi26ZiWgsM7tsd7cD+xRMXkvPEITN5Fd5QCe3KlVBuE00w5byjmg==} - '@zag-js/carousel@1.15.0': - resolution: {integrity: sha512-ZI9H34f2utdJ2Ek6GZa+iuRH4eC99GHD/VEOKLdGani8uadpT2v8M5kUwPGrlAJq9SiPbQ2UuXBmCkmurPQqdA==} + '@zag-js/cascade-select@1.35.3': + resolution: {integrity: sha512-Nifdx77hEuAdXqr1wpZSPjLXqygRhq/WvnPjGhCeSqFPpy62uT4JZ3avyjUZ4I0UhvIpkleUcXtFwQ3cSMh4ww==} - '@zag-js/checkbox@1.15.0': - resolution: {integrity: sha512-6lQvPQNJXt7R0xxdpOuh2qtmAkzdBdqSvFIH7fE6GJzJ/AWiRZh0X+9deLQ76CN4EDUdxizEe7MlQfTI3a56aw==} + '@zag-js/checkbox@1.35.3': + resolution: {integrity: sha512-8XBt/Wg2zSQWqV2ZFqZBQUjYRkOYHA2O3IEi0VVYtds3S1n7Pu/HqkZT5qDw+E/SY2+X9Uyx4hO7h2XrlsiZQQ==} - '@zag-js/clipboard@1.15.0': - resolution: {integrity: sha512-Q3kh0fHvOEAJUywQm3zAWyltrYyiI8OpeZQ18k5Mf3/M+bq3gSphZL0+AYsgGbKUg5O2+hJ1SfiErAjyhRtBQA==} + '@zag-js/clipboard@1.35.3': + resolution: {integrity: sha512-obTwynBpp6c17fLHe5tg//FQ497QsyCEry+K3bTdlrivWW200wvfHxZ6RKVbKwDAwhH+ye0bI1xkYAId8j7sdA==} - '@zag-js/collapsible@1.15.0': - resolution: {integrity: sha512-GX0kdMlKk4Yk5k/2wN0prudf21k+TfArGr4EHqimTDR0vQE3dSdb3pYyPjw20fLzceKHBBCLsoi2v+YnS75gHA==} + '@zag-js/collapsible@1.35.3': + resolution: {integrity: sha512-IweG8JOBCerJwLO6QzTZGEMlsYUmQfQSeD0jniFguMM8vcunvGVSrM+AaL8pDbmXd+snXokaGyJpGO3vzMW6Fw==} - '@zag-js/collection@1.15.0': - resolution: {integrity: sha512-oC3i6c/oP/FuNPsfgoC1reSXbAvDBGXl0HU3CcvXiNLHbjg2ek8J7kbow6MNuXK6chiksiOHbzKxHl2Oo0Ox7A==} + '@zag-js/collection@1.35.3': + resolution: {integrity: sha512-BYoWJ4b7ma2PgiuQbRSnP603f2DlK6se5JtViUHTamZScLLLWnWHuQ6zFa1KS5kiIkbb7CFM6/bJ3WNYLch8Ig==} - '@zag-js/color-picker@1.15.0': - resolution: {integrity: sha512-DGujS24h1OWkYL+TWyd+xukOO8NBgcSfFCINffa4ivkHtNx3nC28qkwLPRASbl7AK69pbrcuO6bx1Sy/JQJw0Q==} + '@zag-js/color-picker@1.35.3': + resolution: {integrity: sha512-i9roSgtqeA1b4Q+jWqnxjXB//BQXMP5m1FQ4YcZVq/0yT14A53JIknchuqrh3wC3yPsJMXFqCoKg+NET2+OVig==} - '@zag-js/color-utils@1.15.0': - resolution: {integrity: sha512-SKo+p5Fu0TBtdDua8UHVjptOkwLLBFoD499Z1FER/gr0R/97L03Kdir0YTxvKn5pXWXYY1EQn4hpTuTITN16lQ==} + '@zag-js/color-utils@1.35.3': + resolution: {integrity: sha512-vxkEVgz4YdSbdaPvjiRI1VsJAdwzu/dUNvzqOaiVcPDrHr/FFgmUbv0SOFjnfSb2QWGI8EDEMn02RW9ym+BzGw==} - '@zag-js/combobox@1.15.0': - resolution: {integrity: sha512-HBck3wcEeIOa7IQMsUkUKbm9cAU7bjoklIyq2zFGn90k7DcDa++oXK9Z2pmcd4TPoBYiyVuuXucaCcjmLX8V/Q==} + '@zag-js/combobox@1.35.3': + resolution: {integrity: sha512-s1qmttTGJTMjlDakL+uvWSEggpafKr1vhOeZCh8j+N4eFt9bLAwaffjuh/1JzWBvzovw7WoMVkizdTXPlN8oYg==} - '@zag-js/core@1.15.0': - resolution: {integrity: sha512-P/8F3IXabMhpFnc6hC7GDg3rvUnvY27cuZU04hxjUqTH6+SfORIA/Uvqd4ekhC+dIprL9jicnFrmGgcyelyxfQ==} + '@zag-js/core@1.35.3': + resolution: {integrity: sha512-fGAHyqOYSEFmo52t7wI4dvbFfLyJmUlyf7wknsiUlzUHlrn3yv5PAZYZ2TibpOD1hwXIp4AoCjbiIPPZBxirZw==} - '@zag-js/date-picker@1.15.0': - resolution: {integrity: sha512-IZD0V9MAljp1QhxYbST80AonryuDnyx7hvEy/RrBY/VOx6I4STtKfcSJ5ZZgVIzJfH8Yyaed4+IwcenqG7W5YQ==} + '@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/dismissable@1.15.0': - resolution: {integrity: sha512-yv575KWy8gA1p4aajOiY5l/nBQ3Xw+Mrjpungp1+wiGd/98eNAIKJ6/adldfbE1Ygd/Q4Dx2VQ7D1AmiTdwUSw==} + '@zag-js/drawer@1.35.3': + resolution: {integrity: sha512-DN5bwa7bDCDaUSbNzFxMc2U/WmbLcXvPSQjyOpKI6CC3VbW2kKaOnjJ5qQG+W5YBO0FpmJBtaxRV7lke4sZH2w==} - '@zag-js/dom-query@1.15.0': - resolution: {integrity: sha512-z8H/j/Zs0eZEsGpbonScmlKSv0jEXKiAwUCrvQ9Mt6Gz9n0CQRM3MkFclSsM8aeiSv6qKLlhPfkzjl18OLkbgA==} + '@zag-js/editable@1.35.3': + resolution: {integrity: sha512-HcjeacS61vQXfNT9IalZj/+oS45yW5bIDO2NjJWV7zNe5AG29NCceUnvBhy+hrUKPnKcjfDocdW5rCL+Lvs/CQ==} - '@zag-js/editable@1.15.0': - resolution: {integrity: sha512-F14HKZuDsfkpfIkaF/ZDYPkz/pFf6VHrvoV0rdhj8wb8QJQ4nB+lgBv2APSwkEaFb/gGrnE19v3Ojlt5tqpPsw==} + '@zag-js/file-upload@1.35.3': + resolution: {integrity: sha512-oIYwnDct4ERo2mfmcxsBIJnlmpzjrzYx82SQsXWD3NGKx3cgdh2lwBX+ebItaLH1jkgzBa3z0TWxc6rfvcUXbw==} - '@zag-js/file-upload@1.15.0': - resolution: {integrity: sha512-2hAlQr9qdT8EH4XnmkNkEIDCCsmp2SMoMAjq6nJKYO8UJNQGRanU2B5S8jV3quJBz0vIY43SwyvqiZ3+1VrJSg==} + '@zag-js/file-utils@1.35.3': + resolution: {integrity: sha512-Tb05RCzx4swc156hd4jLiO7z+Gxg/HQ+JCds03jgTbrFJAz2D56YaMeI7gSDc1m4Xre3nyqQpSo9AeX5nzbE/w==} - '@zag-js/file-utils@1.15.0': - resolution: {integrity: sha512-tahJt3JmrXaOtGiknH5PxIiOyyNvroMfjiBqOqnNksIPzDoWmVNxHOEme/ts7dJlkRD8U2qm2NFC2VS0bKerzg==} + '@zag-js/floating-panel@1.35.3': + resolution: {integrity: sha512-nTZypcS0X46Oo1kpCQTnP5UlzjhypOAj3B4dq2z/3bAOC0TntYTnFkj8PbEJtExk7364xfMyxfgZOiv7Aqq01w==} - '@zag-js/floating-panel@1.15.0': - resolution: {integrity: sha512-AYYFseA1MeQUZl+zjNoKUu4j0kwz8EyJd4oJjs8uJIR6KG8u8QhpWYIBUny63M6AtZTCSYQAgBEcEh+mrbEyyQ==} + '@zag-js/focus-trap@1.35.3': + resolution: {integrity: sha512-evErLlGFdDVCI8xipNS5k0rAvO+KFRA9g273bbfWAL1+mT54mcB/XHa85nC3QpPgMNrSh+6LUNq9fapyOGoyYg==} - '@zag-js/focus-trap@1.15.0': - resolution: {integrity: sha512-N8m/JpNe1gHUPJlr0hyGUdHg6pAuyJKkBaX0s38cyVntlo2CJhyAWZGuUdocpT2Q3HNPql666FNnH986rYPDKQ==} + '@zag-js/focus-visible@1.35.3': + resolution: {integrity: sha512-g4F8PRGIoFoKBrHiQ1HQh5AjCS7brFRXHvpbDNb9+T11FGlF5Turb+6OVRoNV8MmiuqMltO2I28l36YsGc//uQ==} - '@zag-js/focus-visible@1.15.0': - resolution: {integrity: sha512-TPXBf47tj6L0hhZNl9AWhuLoVzfPaNPM+/Gw8t9l9Whvy6v9rk/rqUCidY5LsrQuPiKTi7s5WI5J+Wod8ib3gw==} + '@zag-js/highlight-word@1.35.3': + resolution: {integrity: sha512-K+mvEBbf3SUFjQeMeJQYb3cjri3x6sPaPhcKWayalelSLB/StWEGqcpmz+a6uUYrCUAK5kEi3Hn0YLGfn0GOig==} - '@zag-js/highlight-word@1.15.0': - resolution: {integrity: sha512-Rwr/rRm8BaF2xW9BAEJeA2wpFVx6HzoezfYQX7GFPPgw3N8nBMAYNjx+i1YIwIEcNyad2rbaBB+pSd2fZLIniA==} + '@zag-js/hover-card@1.35.3': + resolution: {integrity: sha512-xVoKOtvrnzhYzciZ1csgiV76IQ4DRtx1lsJeFSrfg5MH0kYWeC/pcmm3yCd2+Qh/45J7DbSXeZneqxpyiF5Vvw==} - '@zag-js/hover-card@1.15.0': - resolution: {integrity: sha512-j6BsE+metdnv/C/Ls0TZzAMN78rtS2r8M1ccHY5FFTGyUvZnlE8BY/QPNyCSSSCUpynymzMYh3IMYlxbJgfpSQ==} + '@zag-js/i18n-utils@1.35.3': + resolution: {integrity: sha512-k7UcNxbnC2jvGwCoHYAkFD3ZaRSMQNVHfuy8TujZQ+ci3IJovwgWLveZoRfFbXHkTLfhmbpE2tFXBdpwOVZutg==} - '@zag-js/i18n-utils@1.15.0': - resolution: {integrity: sha512-anxSbT8kLbJaFJFSb0Ork2j/Lp+XVfMNCIgiBR2BuqUlfX72k23TIJvRxAfwNIkUfs0L8ikaSgLss9OwS4mAnw==} + '@zag-js/image-cropper@1.35.3': + resolution: {integrity: sha512-1PH6bg8JAQESHzNqjka2TJ0QGNBGBAO6rb7AZ+9CaCCLw0pIzbUJhqPMkwd9GhdWGKGP+e7wFitnjcT4W5Js8g==} - '@zag-js/interact-outside@1.15.0': - resolution: {integrity: sha512-OwBf/iesQGU9Oq3xe/tcK7gu7xipiGWsmwl2CcScr0fTp3BIMbQywHS928IgPk1DxA8KTHodY8wBjoY1dskfRA==} + '@zag-js/interact-outside@1.35.3': + resolution: {integrity: sha512-tOcuo/IztzpU7UKXtjVrLZtXzzcbhP4n2WynKwDRkTkq3mRCp61xXJp1csIBycI3JHm/CMeAEcPdRIioxIT/Zw==} - '@zag-js/listbox@1.15.0': - resolution: {integrity: sha512-Gcg76uWZwUAyMFZzGWpHnFCU/aaquNbXmVnyzzBgE3Co2snkv02rK1yG9iBwemZe3e5+VBifMMAtLLPAQJdz+g==} + '@zag-js/json-tree-utils@1.35.3': + resolution: {integrity: sha512-nOv2dPJf+1mxsobYiSlYt96hR1MK7iHKG1iDLoO5wLggS6GQA3ix1BerHJK0zdehoEZ71R45el5ghCG1HB9VzQ==} - '@zag-js/live-region@1.15.0': - resolution: {integrity: sha512-Xy1PqLZD9AKzKuTKCMo9miL1Xizk/N8qFvj64iybBKUYnKr89/af3w7hRFqd2BDX+q3zrNxPp9rZ6L7MlOc7kA==} + '@zag-js/listbox@1.35.3': + resolution: {integrity: sha512-FE6FOuBr6aWtOb8U8oDvAvcUzD6JKLXAe8WngiLFG+b2yyW4nlaz2AcKRG1bjjB066UMxMo9/+2p4D0Kf5Id1Q==} - '@zag-js/menu@1.15.0': - resolution: {integrity: sha512-GbEBVYu0w7+88xrGX2GrjXfnwWuX5jLhoLiEcuxvxJQal/nahKrH4AGXJvHXNaRbj+53V3nWAh3u70C9210PWw==} + '@zag-js/live-region@1.35.3': + resolution: {integrity: sha512-64rWcfggYpyr2Fn4pdrB/lljMgm3quwn9is+vdDN85Vv3WShKWoz08T4njidm0hwcIbzas0bRqQYWDLLsAoSJQ==} - '@zag-js/number-input@1.15.0': - resolution: {integrity: sha512-+kK8kyXJhIAbEUnswoMDR+DSJUmvDNIOW0ffuZ9pbfukN3p6zaA3/dCp2Dtg3bQS7hGrFWgtrdejJ8l+mVvUAA==} + '@zag-js/marquee@1.35.3': + resolution: {integrity: sha512-bKZVpmAJWPDORP7WOWnS+65W5ZQBQmRs8zvV33ZfCpFbkXjhRiqKSzIj223/VOc2NEDjyWagz2vioAxrFYVzww==} - '@zag-js/pagination@1.15.0': - resolution: {integrity: sha512-Z62Q41fQPWqk59QyJk+9J0Ad3H9DCqZ0zZutI6iH8DdzT0A0xxmT6zhup6DM/8C8h0OLlaHFTWQnj0RdRNrnXg==} + '@zag-js/menu@1.35.3': + resolution: {integrity: sha512-KyY0EZXkIU57Mjt+Lg+pupiePk3LcnQcB3Gl05Vva61bNjBjdKV71qwCQru/OxPZEwYgPo46L7TDIb56kfK/VQ==} - '@zag-js/password-input@1.15.0': - resolution: {integrity: sha512-oHuZKDRJIbycqWpTVznufy4L7K2g8kwcEaZ4runkwO2ocF00zP8HVmOZQzmhkUgTny0azErQydg8XE0VR5OfYg==} + '@zag-js/navigation-menu@1.35.3': + resolution: {integrity: sha512-8cCHx0X/KjEpr2BaMOxJS5LiA6fs/CNqVTF/sTTgZAv7Dm+MH0yNuKm4kpPvcLaVeBpVE09bnyCHrNKzZes+Fw==} - '@zag-js/pin-input@1.15.0': - resolution: {integrity: sha512-IykjogZBG+BfbFXymSa+KGpOi5CrV9kl8HRm6G2V2Sr3NA5jEwMFaGSd/QrcHS9vh23D1Smx/io4pvF7c3q0kg==} + '@zag-js/number-input@1.35.3': + resolution: {integrity: sha512-uqawVybAcLcefVEHMVONuAA5kDSDPP5TsROr5PnAyFlhM1iD85+r3KAfCueoDX5w2X4ibbu9o2tdV6zTFKD/nQ==} - '@zag-js/popover@1.15.0': - resolution: {integrity: sha512-cdzEed3zcGbjSgPQnQnrsuXo2hVVslmSNwQbU5dHcNzG1uxxmtPCIMVeBUmGyJbAFF5XQpKCq/7mIr26dT73vw==} + '@zag-js/pagination@1.35.3': + resolution: {integrity: sha512-fKm4s5KAd12RiCI/EDmmGKjPQ+i2qS/UsJPdMe65yb/4mY5OibwV2zyHcVeFsOD4gBZpnU6kYlDAGSttmLWLlQ==} - '@zag-js/popper@1.15.0': - resolution: {integrity: sha512-Ra/0Ko423KN+8D4+mIFFkeTn9uaHfpxn6UUNIWwZKoiJQvED8DH4dPbLbmvGEoKp6qmisnRHAzi71NLgEhk0Mw==} + '@zag-js/password-input@1.35.3': + resolution: {integrity: sha512-etd0gm6ELAm3y+cFhPU+TYm8khm9cL5Mg5m2DcZxu1Mqpj7JY0LsXZ8SFOdCZgTIHuMEhKBiYfnuyMAd4CJztA==} - '@zag-js/presence@1.15.0': - resolution: {integrity: sha512-hoxXis50pm79PpkY2kA1wdhh4AEo7t7pBv0VsQYZYjmzuFh4V5IMw9oa1EOfBlC6f/A+EMZ9E+xg+EVsB68a8w==} + '@zag-js/pin-input@1.35.3': + resolution: {integrity: sha512-ZFt+WIHMdVlSg29BrQLFq5ijabiUO3tXMhoKhjjzTSe/tLqfNeu3UxFB6y/FYpn8+Cvn6xwvhu3lgnORYmI0zQ==} - '@zag-js/progress@1.15.0': - resolution: {integrity: sha512-/Mz26GR2rOAuoErNOiSGRpvwckTmbCD5nWGDE/aYlVRID13HcsmN15Zk2Jfa4LadqK88aIN8Iy0Sk4elG0+Efw==} + '@zag-js/popover@1.35.3': + resolution: {integrity: sha512-+MIEENPsbKPxzoNuDI/C5d5ZN9uxnfZ+MBDc5C5XSgjjg9FcvMXClNq7IFM1aZi24peRXg9cMNf//lApVRT37w==} - '@zag-js/qr-code@1.15.0': - resolution: {integrity: sha512-GkGy5k5tk6DIui9lGjDO8+e8TsSVOxEGp1lblPiaRm1ggIh10GhIfCQWGe/x78ezdie8WzxlSrma89suTpaiAQ==} + '@zag-js/popper@1.35.3': + resolution: {integrity: sha512-gpB7Xn9WtlfrUsIVbSgNQGDwgNOL/cSGt0Id3wEQKArmqVC704EWtPvXzOMMybBEdm8YW2hQrXuo+o66abI1Sg==} - '@zag-js/radio-group@1.15.0': - resolution: {integrity: sha512-+KTebHUtMsE/YDyGE8wF5VnWfZQp+f2WoAwwzBjfhPpRxXbOUMDo0pZEEr3yxkSvQ9hgCcBhMKH8pEk0SPxvjQ==} + '@zag-js/presence@1.35.3': + resolution: {integrity: sha512-ev5E7+U9IZAGvEaflpdVLHaZl8ZaQMhGB3ypd0yKhPwXeM51obV8w3+5HjzTqHPl8TKuoHWL31YaiUBd5EuS6w==} - '@zag-js/rating-group@1.15.0': - resolution: {integrity: sha512-omGKN97FhplFwBX9J/Mj7BCZuwFXSXssSVTKU7Yp2d1Cmxhez4+Ju7KdSRNnIoWB4OxFCxwZyaAPTcg3E0Pjrg==} + '@zag-js/progress@1.35.3': + resolution: {integrity: sha512-u0GxQN1AfXMAgzYOUMxKQA12DyuAP0svh2S//KvOorTSv7d5hAa8nZXi2cEv5abYsyfKJ6/bc1Z56byzW1jVZw==} - '@zag-js/react@1.15.0': - resolution: {integrity: sha512-YSp9QBkdeBfZt4nVhJW+CUd5sNEEVAuwkmoZWDFUoDoWSAXwzSKuHCmTm5/8DaXg1IZD2bMrXgMNDqZv2x0hZw==} + '@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.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 +1803,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 +1822,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 +1900,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 +1920,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 +1939,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 +1952,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 +1996,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 +2156,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 +2267,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 +2345,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 +2357,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 +2402,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 +2468,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 +2488,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 +2521,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 +2593,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 +2742,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 +2751,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 +2827,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 +3164,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 +3354,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 +3371,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 +3421,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 +3540,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 +3564,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 +3585,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 +3603,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 +3653,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 +3664,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 +3692,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 +3725,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 +3834,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 +3873,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 +4011,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 +4043,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} @@ -4018,11 +4085,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 +4113,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 +4132,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 +4140,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 +4160,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 +4184,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 +4214,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 +4229,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 +4244,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 +4274,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 +4407,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 +4438,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 +4455,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 +4468,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 +4483,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 +4529,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 +4619,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 +4641,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 +4669,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 +4683,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 +4699,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 +4724,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 +4736,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 +4752,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 +4784,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 +4797,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 +4823,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 +4844,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 +4856,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 +4883,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 +4931,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 +4949,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 +4959,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 +4973,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 +5061,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 +5117,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 +5161,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 @@ -5055,7 +5227,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 +5269,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 +5287,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 +5381,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 +5391,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 +5448,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 +5470,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.19(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.19 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.19': {} '@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 +5500,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 +5518,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 +5532,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 +5575,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 +5614,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 +5640,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 +5648,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': 19.2.14 - '@types/react-transition-group@4.4.12(@types/react@19.2.7)': + '@types/react-transition-group@4.4.12(@types/react@19.2.14)': dependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 - '@types/react@18.3.19': - dependencies: - '@types/prop-types': 15.7.14 - csstype: 3.1.3 - - '@types/react@19.2.7': + '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -5509,188 +5674,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)': - 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/project-service@8.49.0(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': + '@typescript-eslint/scope-manager@8.56.1': dependencies: - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 - '@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.49.0(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/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) + ts-api-utils: 2.4.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)': + '@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.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(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/utils@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/visitor-keys@8.56.1': 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: 9.39.1(jiti@1.21.7) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.48.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 +5780,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 +5794,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 +5827,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 +5862,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 +5874,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 +5909,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 +5932,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/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.15.0': {} - - '@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/highlight-word@1.35.3': {} - '@zag-js/listbox@1.15.0': + '@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.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.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/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 +6507,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 +6519,7 @@ snapshots: ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} anymatch@3.1.3: dependencies: @@ -6486,7 +6618,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 +6630,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 +6642,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 +6657,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 +6672,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 +6724,8 @@ snapshots: caniuse-lite@1.0.30001707: {} + caniuse-lite@1.0.30001777: {} + ccount@2.0.1: {} chai@5.3.3: @@ -6592,12 +6736,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 +6875,6 @@ snapshots: css.escape@1.5.1: {} - csstype@3.1.3: {} - csstype@3.2.3: {} d3-array@3.2.1: @@ -6837,6 +6979,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 +7031,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 +7046,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 +7162,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 +7256,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 +7283,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 +7340,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 +7433,6 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-safe-stringify@2.1.1: {} - fault@1.0.4: dependencies: format: 0.2.2 @@ -7418,9 +7568,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 +7587,7 @@ snapshots: graphemer@1.4.0: {} - graphql@16.12.0: {} + graphql@16.13.1: {} handlebars@4.7.8: dependencies: @@ -7448,11 +7598,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 +7694,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 +7704,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 +8022,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 +8042,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.1 + semver: 7.7.4 markdown-table@3.0.4: {} @@ -8219,7 +8375,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 +8404,7 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.2 + brace-expansion: 5.0.4 minimatch@3.1.5: dependencies: @@ -8262,6 +8418,8 @@ snapshots: minipass@7.1.2: {} + minipass@7.1.3: {} + minizlib@3.1.0: dependencies: minipass: 7.1.2 @@ -8279,24 +8437,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 +8485,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 +8622,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 +8640,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 +8657,7 @@ snapshots: perfect-debounce@1.0.0: {} - perfect-freehand@1.2.2: {} + perfect-freehand@1.2.3: {} picocolors@1.1.1: {} @@ -8513,11 +8673,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 +8685,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 +8693,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 +8734,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 +8744,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 +8753,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 +8801,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 +8815,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 +8844,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 +8961,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 +9023,8 @@ snapshots: semver@7.7.1: {} + semver@7.7.4: {} + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: @@ -8971,7 +9133,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 +9194,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,6 +9224,10 @@ 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 @@ -9097,11 +9263,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 +9277,7 @@ snapshots: tough-cookie@6.0.0: dependencies: - tldts: 7.0.19 + tldts: 7.0.25 tr46@0.0.3: {} @@ -9119,7 +9285,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 +9302,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 +9343,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 +9368,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 +9411,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 +9425,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 +9459,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 +9480,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 +9520,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 +9624,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 +9640,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.0: {} + yaml@2.8.2: {} yargs-parser@21.1.1: {} @@ -9486,7 +9656,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 +9664,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/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/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/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/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/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"; From c96242b3eac93a6f1a8a8c9514a564e1c021cdce Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Tue, 10 Mar 2026 14:25:21 +0100 Subject: [PATCH 091/280] Fix test_configuration.py to use real deprecated options from shared config (#63263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tests were registering fake deprecated option mappings at module level (core.sql_alchemy_conn → database.sql_alchemy_conn) that had been removed from the shared configuration project. This mutation of the class-level deprecated_options dict caused side-effects when tests ran under xdist in different order, as the shared mutable state leaked across test modules. Replace the removed sql_alchemy_conn deprecated mapping with the real webserver.secret_key → api.secret_key mapping that is already defined in the shared configuration's deprecated_options. Co-authored-by: Claude Opus 4.6 --- .../unit/config_templates/deprecated.cfg | 4 +- .../unit/config_templates/deprecated_cmd.cfg | 4 +- .../config_templates/deprecated_secret.cfg | 4 +- .../tests/unit/core/test_configuration.py | 180 ++++++++---------- 4 files changed, 86 insertions(+), 106 deletions(-) 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..e591421e68b58 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", @@ -1183,38 +1182,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 +1314,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 +1336,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 +1360,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 +1417,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 +1441,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 +1466,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 +1488,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 +1513,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 +1540,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 +1556,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 +1566,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 +1796,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"), From 8f1525ed6fb87f97f33e2d5cf4d95dfab657dfa1 Mon Sep 17 00:00:00 2001 From: Ash Berlin-Taylor Date: Tue, 10 Mar 2026 13:33:18 +0000 Subject: [PATCH 092/280] Restructure Execution API security to better use FastAPI's Security scopes (#62582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this change, `JWTBearer` in deps.py does everything: crypto validation, sub-claim matching, and it runs twice per request on ti:self routes because FastAPI includes scopes in dependency cache keys for `HTTPBearer` subclasses, defeating dedup. In a PR that is already created (but not yet merged) we want per-endpoint token type policies (e.g. the /run endpoint will need to accept workload tokens while other routes stay execution-only). This changes is the "foundation" that enables that to work in a nice clear fashion `SecurityScopes` can't express this directly because FastAPI resolves outer router deps before inner ones -- a `token:workload` scope on an endpoint needs to *relax* the default restriction, but `SecurityScopes` only accumulate additively. The fix is a new security.py with a three-layer split: - `JWTBearer` (`_jwt_bearer`) now does only crypto validation and caches the result on the ASGI request scope. It never looks at scopes or token types. - `require_auth` is a plain function (not an `HTTPBearer` subclass) used via `Security(require_auth)` on routers. Because plain functions have `_uses_scopes=False` in FastAPI's dependency system, `_jwt_bearer` (its sub-dep) deduplicates correctly across multiple Security resolutions. It enforces `ti:self` via `SecurityScopes` and reads allowed token types from the matched route object. - `ExecutionAPIRoute` is a custom `APIRoute` subclass that precomputes `allowed_token_types` from `token:*` Security scopes at route registration time — after `include_router` has merged all parent and child dependencies. This sidesteps the resolution ordering problem entirely. To opt a route into workload tokens, it's now a one-liner: ```python @ti_id_router.patch( "/{task_instance_id}/run", dependencies=[Security(require_auth, scopes=["token:execution", "token:workload"])], ) ``` Nothing uses the workload-scoped tokens just yet -- this PR lays the foundation; a follow-up PR will add token:workload to /run. Also cleaned up the module boundaries: security.py owns all auth-related deps (CurrentTIToken, get_team_name_dep, require_auth); deps.py is just the svcs DepContainer. Renamed JWTBearerDep to CurrentTIToken to match the FastAPI current_user convention. I tried _lots_ of different approaches to get this merge/override behaviour, and the cleanest was a custom route class --- .codespellignorelines | 1 + .../api_fastapi/execution_api/AGENTS.md | 4 + .../airflow/api_fastapi/execution_api/app.py | 26 +- .../airflow/api_fastapi/execution_api/deps.py | 90 +------ .../execution_api/routes/__init__.py | 6 +- .../execution_api/routes/connections.py | 4 +- .../execution_api/routes/task_instances.py | 10 +- .../execution_api/routes/variables.py | 4 +- .../api_fastapi/execution_api/routes/xcoms.py | 4 +- .../api_fastapi/execution_api/security.py | 243 ++++++++++++++++++ .../api_fastapi/execution_api/conftest.py | 60 ++--- .../api_fastapi/execution_api/test_app.py | 17 ++ .../execution_api/test_security.py | 138 ++++++++++ .../versions/head/test_task_instances.py | 116 +++++++-- .../versions/head/test_variables.py | 4 +- .../execution_api/versions/head/test_xcoms.py | 4 +- 16 files changed, 563 insertions(+), 168 deletions(-) create mode 100644 airflow-core/src/airflow/api_fastapi/execution_api/security.py create mode 100644 airflow-core/tests/unit/api_fastapi/execution_api/test_security.py 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/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/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/task_instances.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py index 53e3bbb2a9f9c..9273cc8b3d487 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,7 +28,7 @@ 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.engine import CursorResult @@ -59,7 +59,7 @@ 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 @@ -78,10 +78,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"]), + ], ) 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/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..d9ec3916187ee 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 @@ -76,13 +76,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 +93,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,15 +109,10 @@ 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() @@ -2925,3 +2916,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 554c2ad2c8437..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( From c9e9e3535b50f016e1e7ce62bc035fe498025187 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Tue, 10 Mar 2026 14:43:46 +0100 Subject: [PATCH 093/280] Make example_xcom resistant to escaping issues. (#63200) --- .../src/airflow/example_dags/example_xcom.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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, ) From c3c1ac703200bd7f3e97dc89249b08d07a7c664a Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Tue, 10 Mar 2026 21:56:56 +0800 Subject: [PATCH 094/280] fix(migration): disable disable_sqlite_fkeys for migration 0087 (#63256) --- airflow-core/docs/img/airflow_erd.sha256 | 2 +- airflow-core/docs/img/airflow_erd.svg | 788 +++++++++--------- ...hange_signed_url_template_from_varchar_.py | 38 +- 3 files changed, 415 insertions(+), 413 deletions(-) diff --git a/airflow-core/docs/img/airflow_erd.sha256 b/airflow-core/docs/img/airflow_erd.sha256 index a75e1304429ad..62481785d172d 100644 --- a/airflow-core/docs/img/airflow_erd.sha256 +++ b/airflow-core/docs/img/airflow_erd.sha256 @@ -1 +1 @@ -615d06d0b6fd8cf40d0e4f6f7f9eda2c56013fa390b437f569c9eb1a627f40c3 \ No newline at end of file +a1be27dc622032d8c01e617348bc82c07e5f86448e109a3c15b6753e2aba9bd5 \ No newline at end of file diff --git a/airflow-core/docs/img/airflow_erd.svg b/airflow-core/docs/img/airflow_erd.svg index f9fc89140cce3..63a3c13c26a27 100644 --- a/airflow-core/docs/img/airflow_erd.svg +++ b/airflow-core/docs/img/airflow_erd.svg @@ -4,11 +4,11 @@ - - + + %3 - + dag_bundle @@ -678,7 +678,7 @@ [TIMESTAMP] - + trigger:id--task_instance:trigger_id 0..N @@ -796,7 +796,7 @@ NOT NULL - + asset_alias:id--asset_alias_asset_event:alias_id 0..N @@ -947,7 +947,7 @@ NOT NULL - + asset:id--dag_schedule_asset_reference:asset_id 0..N @@ -1104,7 +1104,7 @@ NOT NULL - + asset_event:id--asset_alias_asset_event:event_id 0..N @@ -1127,7 +1127,7 @@ NOT NULL - + asset_event:id--dagrun_asset_event:event_id 0..N @@ -1411,7 +1411,7 @@ 1 - + dag:dag_id--dag_schedule_asset_reference:dag_id 0..N @@ -1725,7 +1725,7 @@ NOT NULL - + dag_version:id--dag_run:created_dag_version_id 0..N @@ -1869,14 +1869,14 @@ NOT NULL - + log_template:id--dag_run:log_template_id 0..N 1 - + dag_run:id--dagrun_asset_event:dag_run_id 0..N @@ -1926,18 +1926,18 @@ -dag_run:run_id--task_instance:run_id - -0..N -1 - - - dag_run:dag_id--task_instance:dag_id 0..N 1 + + +dag_run:run_id--task_instance:run_id + +0..N +1 + backfill_dag_run @@ -2268,31 +2268,31 @@ -task_instance:run_id--task_map:run_id - -0..N -1 +task_instance:task_id--task_map:task_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:run_id--task_map:run_id + +0..N +1 + + -task_instance:task_id--task_map:task_id - -0..N -1 +task_instance:dag_id--task_map:dag_id + +0..N +1 @@ -2384,31 +2384,31 @@ -task_instance:run_id--xcom:run_id - -0..N -1 +task_instance:task_id--xcom:task_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:run_id--xcom:run_id + +0..N +1 + + -task_instance:task_id--xcom:task_id - -0..N -1 +task_instance:dag_id--xcom:dag_id + +0..N +1 @@ -2621,18 +2621,18 @@ -task_instance:dag_id--task_instance_history:dag_id - -0..N -1 - - - task_instance:run_id--task_instance_history:run_id 0..N 1 + + +task_instance:dag_id--task_instance_history:dag_id + +0..N +1 + rendered_task_instance_fields @@ -2670,10 +2670,10 @@ -task_instance:run_id--rendered_task_instance_fields:run_id - -0..N -1 +task_instance:dag_id--rendered_task_instance_fields:dag_id + +0..N +1 @@ -2685,16 +2685,16 @@ 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 +task_instance:run_id--rendered_task_instance_fields:run_id + +0..N +1 @@ -2885,466 +2885,470 @@ 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)] + +edge_worker + +worker_name + + [VARCHAR(64)] + NOT NULL + +concurrency + + [INTEGER] + +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 + +alembic_version_edge3 + +version_num + + [VARCHAR(32)] + NOT NULL ab_user - -ab_user + +ab_user + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +active + + [BOOLEAN] -active - - [BOOLEAN] +changed_by_fk + + [INTEGER] -changed_by_fk - - [INTEGER] +changed_on + + [TIMESTAMP] -changed_on - - [TIMESTAMP] +created_by_fk + + [INTEGER] -created_by_fk - - [INTEGER] +created_on + + [TIMESTAMP] -created_on - - [TIMESTAMP] +email + + [VARCHAR(512)] + NOT NULL -email - - [VARCHAR(512)] - NOT NULL +fail_login_count + + [INTEGER] -fail_login_count - - [INTEGER] +first_name + + [VARCHAR(256)] + NOT NULL -first_name - - [VARCHAR(256)] - NOT NULL +last_login + + [TIMESTAMP] -last_login - - [TIMESTAMP] +last_name + + [VARCHAR(256)] + NOT NULL -last_name - - [VARCHAR(256)] - NOT NULL +login_count + + [INTEGER] -login_count - - [INTEGER] +password + + [VARCHAR(256)] -password - - [VARCHAR(256)] - -username - - [VARCHAR(512)] - NOT NULL +username + + [VARCHAR(512)] + NOT NULL ab_user:id--ab_user:changed_by_fk - -0..N -{0,1} + +0..N +{0,1} ab_user:id--ab_user:created_by_fk - -0..N -{0,1} + +0..N +{0,1} ab_user_role - -ab_user_role + +ab_user_role + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +role_id + + [INTEGER] -role_id - - [INTEGER] - -user_id - - [INTEGER] +user_id + + [INTEGER] ab_user:id--ab_user_role:user_id - -0..N -{0,1} + +0..N +{0,1} ab_user_group - -ab_user_group + +ab_user_group + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +group_id + + [INTEGER] -group_id - - [INTEGER] - -user_id - - [INTEGER] +user_id + + [INTEGER] - + ab_user:id--ab_user_group:user_id - -0..N -{0,1} + +0..N +{0,1} ab_register_user - -ab_register_user + +ab_register_user + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +email + + [VARCHAR(512)] + NOT NULL -email - - [VARCHAR(512)] - NOT NULL +first_name + + [VARCHAR(256)] + NOT NULL -first_name - - [VARCHAR(256)] - NOT NULL +last_name + + [VARCHAR(256)] + NOT NULL -last_name - - [VARCHAR(256)] - NOT NULL +password + + [VARCHAR(256)] -password - - [VARCHAR(256)] +registration_date + + [TIMESTAMP] -registration_date - - [TIMESTAMP] +registration_hash + + [VARCHAR(256)] -registration_hash - - [VARCHAR(256)] - -username - - [VARCHAR(512)] - NOT NULL +username + + [VARCHAR(512)] + NOT NULL ab_group - -ab_group + +ab_group + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +description + + [VARCHAR(512)] -description - - [VARCHAR(512)] +label + + [VARCHAR(150)] -label - - [VARCHAR(150)] - -name - - [VARCHAR(100)] - NOT NULL +name + + [VARCHAR(100)] + NOT NULL ab_group_role - -ab_group_role + +ab_group_role + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +group_id + + [INTEGER] -group_id - - [INTEGER] - -role_id - - [INTEGER] +role_id + + [INTEGER] - + ab_group:id--ab_group_role:group_id - -0..N -{0,1} + +0..N +{0,1} - + ab_group:id--ab_user_group:group_id - -0..N -{0,1} + +0..N +{0,1} ab_role - -ab_role + +ab_role + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(64)] - NOT NULL +name + + [VARCHAR(64)] + NOT NULL - + ab_role:id--ab_group_role:role_id - -0..N -{0,1} + +0..N +{0,1} ab_role:id--ab_user_role:role_id - -0..N -{0,1} + +0..N +{0,1} ab_permission_view_role - -ab_permission_view_role + +ab_permission_view_role + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +permission_view_id + + [INTEGER] -permission_view_id - - [INTEGER] - -role_id - - [INTEGER] +role_id + + [INTEGER] - + ab_role:id--ab_permission_view_role:role_id - -0..N -{0,1} + +0..N +{0,1} ab_permission - -ab_permission + +ab_permission + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(100)] - NOT NULL +name + + [VARCHAR(100)] + NOT NULL ab_permission_view - -ab_permission_view + +ab_permission_view + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +permission_id + + [INTEGER] + NOT NULL -permission_id - - [INTEGER] - NOT NULL - -view_menu_id - - [INTEGER] - NOT NULL +view_menu_id + + [INTEGER] + NOT NULL - + ab_permission:id--ab_permission_view:permission_id - -0..N -1 + +0..N +1 - + ab_permission_view:id--ab_permission_view_role:permission_view_id - -0..N -{0,1} + +0..N +{0,1} ab_view_menu - -ab_view_menu + +ab_view_menu + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(250)] - NOT NULL +name + + [VARCHAR(250)] + NOT NULL - + ab_view_menu:id--ab_permission_view:view_menu_id - -0..N -1 + +0..N +1 alembic_version_fab - -alembic_version_fab - -version_num - - [VARCHAR(32)] - NOT NULL + +alembic_version_fab + +version_num + + [VARCHAR(32)] + NOT NULL session - -session + +session + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +data + + [BYTEA] -data - - [BYTEA] +expiry + + [TIMESTAMP] -expiry - - [TIMESTAMP] - -session_id - - [VARCHAR(255)] +session_id + + [VARCHAR(255)] 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, + ) From 44ad5f347f2974c15453af8110283b1917b234eb Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Tue, 10 Mar 2026 22:10:20 +0800 Subject: [PATCH 095/280] Filter backfills list by readable DAGs (#63003) --- .../core_api/routes/ui/backfills.py | 5 ++-- .../airflow/api_fastapi/core_api/security.py | 10 +++++++ .../core_api/routes/ui/test_backfills.py | 26 ++++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) 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/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"} From c533e0b811ed89d985b53fa2528fa70446c7104d Mon Sep 17 00:00:00 2001 From: Elad Kalif <45845474+eladkal@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:16:23 +0200 Subject: [PATCH 096/280] Add kacpermuda to triage team (#63271) --- .asf.yaml | 1 + 1 file changed, 1 insertion(+) 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 From dabd6bdd602c945770d2a19acc21ef80369c3273 Mon Sep 17 00:00:00 2001 From: Mathieu Monet <60776491+stegololz@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:19:39 +0100 Subject: [PATCH 097/280] Add TTL cache with single-flight dedup to Keycloak filter_authorized_dag_ids (#63184) --- .../providers/keycloak/auth_manager/cache.py | 84 ++++++++++ .../auth_manager/keycloak_auth_manager.py | 19 +++ .../unit/keycloak/auth_manager/test_cache.py | 137 ++++++++++++++++ .../test_keycloak_auth_manager.py | 146 ++++++++++++++++++ 4 files changed, 386 insertions(+) create mode 100644 providers/keycloak/src/airflow/providers/keycloak/auth_manager/cache.py create mode 100644 providers/keycloak/tests/unit/keycloak/auth_manager/test_cache.py diff --git a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cache.py b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cache.py new file mode 100644 index 0000000000000..75ccbf99c51d1 --- /dev/null +++ b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cache.py @@ -0,0 +1,84 @@ +# 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 threading +import time +from collections.abc import Callable + +_CACHE_TTL_SECONDS = 30 +_SINGLE_FLIGHT_TIMEOUT_SECONDS = 60 + +# Maps cache keys to (timestamp, result) pairs for TTL-based expiration. +_cache: dict[tuple, tuple[float, frozenset[str]]] = {} +# Tracks in-flight requests: maps cache keys to events that waiting threads block on. +_pending_requests: dict[tuple, threading.Event] = {} +_cache_lock = threading.Lock() + + +def _cache_get(key: tuple) -> frozenset[str] | None: + entry = _cache.get(key) + if entry and (time.monotonic() - entry[0]) < _CACHE_TTL_SECONDS: + return entry[1] + return None + + +def _cache_set(key: tuple, value: frozenset[str]) -> None: + with _cache_lock: + _cache[key] = (time.monotonic(), value) + now = time.monotonic() + for k in [k for k, (ts, _) in _cache.items() if now - ts > _CACHE_TTL_SECONDS * 2]: + _cache.pop(k, None) + + +def single_flight(cache_key: tuple, query_keycloak: Callable[[], set[str]]) -> set[str]: + """Return cached result, wait for a pending request, or run the query ourselves.""" + # Fast path: check cache without lock + cached = _cache_get(cache_key) + if cached is not None: + return set(cached) + + with _cache_lock: + cached = _cache_get(cache_key) + if cached is not None: + return set(cached) + + event = _pending_requests.get(cache_key) + if event is not None: + is_worker = False + else: + event = threading.Event() + _pending_requests[cache_key] = event + is_worker = True + + if not is_worker: + # Wait for the other thread to finish + event.wait(timeout=_SINGLE_FLIGHT_TIMEOUT_SECONDS) + cached = _cache_get(cache_key) + if cached is not None: + return set(cached) + # If the other thread failed, fall through and do the work ourselves + + try: + result = query_keycloak() + _cache_set(cache_key, frozenset(result)) + return result + finally: + with _cache_lock: + event = _pending_requests.pop(cache_key, None) + if event is not None: + event.set() diff --git a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py index bc39802084428..9844873c031a6 100644 --- a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py +++ b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py @@ -49,6 +49,7 @@ except ModuleNotFoundError: from airflow.configuration import conf from airflow.exceptions import AirflowException +from airflow.providers.keycloak.auth_manager.cache import single_flight from airflow.providers.keycloak.auth_manager.constants import ( CONF_CLIENT_ID_KEY, CONF_CLIENT_SECRET_KEY, @@ -440,6 +441,24 @@ def _is_authorized( ) raise AirflowException(f"Unexpected error: {resp.status_code} - {resp.text}") + def filter_authorized_dag_ids( + self, + *, + dag_ids: set[str], + user: KeycloakAuthManagerUser, + method: ResourceMethod = "GET", + team_name: str | None = None, + ) -> set[str]: + cache_key = (user.get_id(), method, team_name, frozenset(dag_ids)) + + def query_keycloak() -> set[str]: + kwargs: dict = dict(dag_ids=dag_ids, user=user, method=method) + if team_name is not None: + kwargs["team_name"] = team_name + return super(KeycloakAuthManager, self).filter_authorized_dag_ids(**kwargs) + + return single_flight(cache_key, query_keycloak) + def _is_batch_authorized( self, *, diff --git a/providers/keycloak/tests/unit/keycloak/auth_manager/test_cache.py b/providers/keycloak/tests/unit/keycloak/auth_manager/test_cache.py new file mode 100644 index 0000000000000..9125d8d4b72c4 --- /dev/null +++ b/providers/keycloak/tests/unit/keycloak/auth_manager/test_cache.py @@ -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. +from __future__ import annotations + +import threading +import time + +import pytest + +from airflow.providers.keycloak.auth_manager import cache as cache_module +from airflow.providers.keycloak.auth_manager.cache import single_flight + + +@pytest.fixture(autouse=True) +def _clear_cache(): + cache_module._cache.clear() + cache_module._pending_requests.clear() + yield + cache_module._cache.clear() + cache_module._pending_requests.clear() + + +class TestSingleFlight: + def test_returns_query_result(self): + result = single_flight(("key",), lambda: {"a", "b"}) + assert result == {"a", "b"} + + def test_cache_hit(self): + call_count = 0 + + def query(): + nonlocal call_count + call_count += 1 + return {"a"} + + single_flight(("key",), query) + single_flight(("key",), query) + + assert call_count == 1 + + def test_different_keys_not_cached(self): + call_count = 0 + + def query(): + nonlocal call_count + call_count += 1 + return {"a"} + + single_flight(("key1",), query) + single_flight(("key2",), query) + + assert call_count == 2 + + def test_cache_expires(self): + call_count = 0 + + def query(): + nonlocal call_count + call_count += 1 + return {"a"} + + single_flight(("key",), query) + + # Expire the cache entry by backdating its timestamp + for k in cache_module._cache: + ts, val = cache_module._cache[k] + cache_module._cache[k] = (ts - cache_module._CACHE_TTL_SECONDS - 1, val) + + single_flight(("key",), query) + assert call_count == 2 + + def test_concurrent_dedup(self): + """Multiple threads with the same key coalesce into one call.""" + gate = threading.Event() + call_count = 0 + + def slow_query(): + nonlocal call_count + call_count += 1 + gate.wait(timeout=5) + return {"a"} + + results = [None] * 5 + errors = [] + + def run(index): + try: + results[index] = single_flight(("key",), slow_query) + except Exception as e: + errors.append(e) + + threads = [threading.Thread(target=run, args=(i,)) for i in range(5)] + for t in threads: + t.start() + + time.sleep(0.1) + gate.set() + + for t in threads: + t.join(timeout=5) + + assert not errors, f"Threads raised errors: {errors}" + for r in results: + assert r == {"a"} + assert call_count == 1 + + def test_failed_query_allows_retry(self): + """If the worker thread fails, another thread can retry.""" + call_count = 0 + + def failing_then_ok(): + nonlocal call_count + call_count += 1 + if call_count == 1: + raise ValueError("boom") + return {"a"} + + with pytest.raises(ValueError, match="boom"): + single_flight(("key",), failing_then_ok) + + result = single_flight(("key",), failing_then_ok) + assert result == {"a"} + assert call_count == 2 diff --git a/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py b/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py index b1c28714f6bcb..9c8ed9dd5b610 100644 --- a/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py +++ b/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py @@ -51,6 +51,7 @@ from airflow.providers.common.compat.sdk import AirflowException except ModuleNotFoundError: from airflow.exceptions import AirflowException +from airflow.providers.keycloak.auth_manager import cache as cache_module from airflow.providers.keycloak.auth_manager.constants import ( CONF_CLIENT_ID_KEY, CONF_CLIENT_SECRET_KEY, @@ -109,6 +110,16 @@ def user(): return user +@pytest.fixture(autouse=True) +def _clear_filter_cache(): + """Clear module-level single-flight cache between tests.""" + cache_module._cache.clear() + cache_module._pending_requests.clear() + yield + cache_module._cache.clear() + cache_module._pending_requests.clear() + + class TestKeycloakAuthManager: def test_deserialize_user(self, auth_manager): result = auth_manager.deserialize_user( @@ -960,3 +971,138 @@ def test_get_teams(self, mock_get_keycloak_client, auth_manager_multi_team): headers={"Authorization": "Bearer pat-token"}, timeout=5, ) + + @pytest.mark.parametrize( + ("status_codes", "expected"), + [ + ([200, 200, 200], True), + ([200, 403, 200], False), + ([401, 200, 200], False), + ([200, 200, 401], False), + ], + ) + def test_batch_is_authorized_dag(self, status_codes, expected, auth_manager, user): + return_values = [code == 200 for code in status_codes] + + requests = [{"method": "GET", "details": DagDetails(id=f"dag_{i}")} for i in range(len(status_codes))] + + with patch.object(KeycloakAuthManager, "_is_authorized", side_effect=return_values): + result = auth_manager.batch_is_authorized_dag(requests, user=user) + + assert result == expected + + @pytest.mark.parametrize( + ("status_codes", "expected"), + [ + ([200, 200], True), + ([200, 403], False), + ], + ) + def test_batch_is_authorized_connection(self, status_codes, expected, auth_manager, user): + return_values = [code == 200 for code in status_codes] + + requests = [ + {"method": "GET", "details": ConnectionDetails(conn_id=f"conn_{i}")} + for i in range(len(status_codes)) + ] + + with patch.object(KeycloakAuthManager, "_is_authorized", side_effect=return_values): + result = auth_manager.batch_is_authorized_connection(requests, user=user) + + assert result == expected + + @pytest.mark.parametrize( + ("status_codes", "expected"), + [ + ([200, 200], True), + ([403, 200], False), + ], + ) + def test_batch_is_authorized_pool(self, status_codes, expected, auth_manager, user): + return_values = [code == 200 for code in status_codes] + + requests = [ + {"method": "GET", "details": PoolDetails(name=f"pool_{i}")} for i in range(len(status_codes)) + ] + + with patch.object(KeycloakAuthManager, "_is_authorized", side_effect=return_values): + result = auth_manager.batch_is_authorized_pool(requests, user=user) + + assert result == expected + + @pytest.mark.parametrize( + ("status_codes", "expected"), + [ + ([200, 200], True), + ([200, 401], False), + ], + ) + def test_batch_is_authorized_variable(self, status_codes, expected, auth_manager, user): + return_values = [code == 200 for code in status_codes] + + requests = [ + {"method": "GET", "details": VariableDetails(key=f"var_{i}")} for i in range(len(status_codes)) + ] + + with patch.object(KeycloakAuthManager, "_is_authorized", side_effect=return_values): + result = auth_manager.batch_is_authorized_variable(requests, user=user) + + assert result == expected + + def test_batch_is_authorized_dag_empty_requests(self, auth_manager, user): + result = auth_manager.batch_is_authorized_dag([], user=user) + assert result is True + + def test_batch_is_authorized_dag_with_access_entity(self, auth_manager, user): + requests = [ + { + "method": "GET", + "access_entity": DagAccessEntity.TASK_INSTANCE, + "details": DagDetails(id="dag_1"), + } + ] + + with patch.object(KeycloakAuthManager, "_is_authorized", return_value=True) as mock_is_authorized: + result = auth_manager.batch_is_authorized_dag(requests, user=user) + + assert result is True + # Verify the call included the dag_entity attribute + call_kwargs = mock_is_authorized.call_args + assert call_kwargs.kwargs["attributes"] == {"dag_entity": "TASK_INSTANCE"} + + @patch.object( + KeycloakAuthManager, + "is_authorized_dag", + side_effect=lambda *, details, **kw: {"dag_0": True, "dag_1": False, "dag_2": True}[details.id], + ) + def test_filter_authorized_dag_ids(self, mock_is_authorized, auth_manager, user): + result = auth_manager.filter_authorized_dag_ids( + dag_ids={"dag_0", "dag_1", "dag_2"}, user=user, method="GET" + ) + + assert result == {"dag_0", "dag_2"} + assert mock_is_authorized.call_count == 3 + + def test_filter_authorized_dag_ids_empty(self, auth_manager, user): + result = auth_manager.filter_authorized_dag_ids(dag_ids=set(), user=user, method="GET") + assert result == set() + + @patch.object(KeycloakAuthManager, "is_authorized_dag", return_value=False) + def test_filter_authorized_dag_ids_all_denied(self, mock_is_authorized, auth_manager, user): + result = auth_manager.filter_authorized_dag_ids(dag_ids={"dag_0", "dag_1"}, user=user, method="GET") + + assert result == set() + assert mock_is_authorized.call_count == 2 + + @patch.object(KeycloakAuthManager, "is_authorized_dag", return_value=True) + def test_filter_authorized_dag_ids_cache_hit(self, mock_is_authorized, auth_manager, user): + """Second call with same args should return cached result without hitting Keycloak.""" + dag_ids = {"dag_0", "dag_1"} + + result1 = auth_manager.filter_authorized_dag_ids(dag_ids=dag_ids, user=user, method="GET") + result2 = auth_manager.filter_authorized_dag_ids(dag_ids=dag_ids, user=user, method="GET") + + assert result1 == dag_ids + assert result2 == dag_ids + # is_authorized_dag should only be called for the first invocation (2 dag_ids × 1 call) + assert mock_is_authorized.call_count == 2 From 293d3fe54a98051a77838dfc66d0ee2ca2920b28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:45:43 +0100 Subject: [PATCH 098/280] chore(deps): bump @tanstack/react-virtual (#63264) Bumps the core-ui-package-updates group with 1 update in the /airflow-core/src/airflow/ui directory: [@tanstack/react-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual). Updates `@tanstack/react-virtual` from 3.13.19 to 3.13.21 - [Release notes](https://github.com/TanStack/virtual/releases) - [Changelog](https://github.com/TanStack/virtual/blob/main/packages/react-virtual/CHANGELOG.md) - [Commits](https://github.com/TanStack/virtual/commits/@tanstack/react-virtual@3.13.21/packages/react-virtual) --- updated-dependencies: - dependency-name: "@tanstack/react-virtual" dependency-version: 3.13.21 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- airflow-core/src/airflow/ui/package.json | 2 +- airflow-core/src/airflow/ui/pnpm-lock.yaml | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/airflow-core/src/airflow/ui/package.json b/airflow-core/src/airflow/ui/package.json index 97c6685793ea3..4ec9f5c0fb188 100644 --- a/airflow-core/src/airflow/ui/package.json +++ b/airflow-core/src/airflow/ui/package.json @@ -33,7 +33,7 @@ "@monaco-editor/react": "^4.7.0", "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.19", + "@tanstack/react-virtual": "^3.13.21", "@visx/group": "^3.12.0", "@visx/shape": "^3.12.0", "@xyflow/react": "^12.10.1", diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml b/airflow-core/src/airflow/ui/pnpm-lock.yaml index 4a698017fa63c..bfaefb374b46e 100644 --- a/airflow-core/src/airflow/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: 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.19 - version: 3.13.19(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) @@ -1216,8 +1216,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.19': - resolution: {integrity: sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ==} + '@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 @@ -1226,8 +1226,8 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.19': - resolution: {integrity: sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g==} + '@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==} @@ -5470,15 +5470,15 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@tanstack/react-virtual@3.13.19(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.19 + '@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.19': {} + '@tanstack/virtual-core@3.13.21': {} '@testing-library/dom@10.4.0': dependencies: From 1fe39a75ebf8eacd1308cf239d2754395ab230de Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Tue, 10 Mar 2026 15:02:40 +0000 Subject: [PATCH 099/280] Add `breeze registry backfill` command for older provider versions (#63269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new breeze subcommand that extracts runtime parameters and connection types for previously released provider versions using `uv run --with` — no Docker or breeze CI image needed. Also includes: - Unit tests for all helper functions (16 tests) - Breeze docs for the backfill command - GitHub Actions workflow (registry-backfill.yml) that runs providers in parallel via matrix strategy, then publishes versions.json - Fix providerVersions.js to use runtime module_counts from modules.json instead of AST-based counts from providers.json Two issues: - `tomllib` is Python 3.11+; use try/except fallback to `tomli` (same pattern as other breeze modules) - `TestReadProviderYamlInfo` tests used real filesystem paths that depend on `tomllib`; replaced with `tmp_path`-based mock files --- .github/workflows/registry-backfill.yml | 266 ++++++++++++++++++ dev/breeze/doc/11_registry_tasks.rst | 41 +++ dev/breeze/doc/images/output_registry.svg | 24 +- dev/breeze/doc/images/output_registry.txt | 2 +- .../doc/images/output_registry_backfill.svg | 126 +++++++++ .../doc/images/output_registry_backfill.txt | 1 + ...utput_setup_check-all-params-in-groups.svg | 4 +- ...utput_setup_check-all-params-in-groups.txt | 2 +- ...output_setup_regenerate-command-images.svg | 2 +- ...output_setup_regenerate-command-images.txt | 2 +- .../commands/registry_commands.py | 191 ++++++++++++- .../commands/registry_commands_config.py | 10 + dev/breeze/tests/test_registry_backfill.py | 189 +++++++++++++ registry/src/_data/providerVersions.js | 11 + 14 files changed, 857 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/registry-backfill.yml create mode 100644 dev/breeze/doc/images/output_registry_backfill.svg create mode 100644 dev/breeze/doc/images/output_registry_backfill.txt create mode 100644 dev/breeze/tests/test_registry_backfill.py diff --git a/.github/workflows/registry-backfill.yml b/.github/workflows/registry-backfill.yml new file mode 100644 index 0000000000000..5a0b39d661f05 --- /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@bd01e18f51369d5765a7df3681d34498e332e27e # v6.3.1 + + - 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/dev/breeze/doc/11_registry_tasks.rst b/dev/breeze/doc/11_registry_tasks.rst index c64828a8b7b07..6b01d9065dcd6 100644 --- a/dev/breeze/doc/11_registry_tasks.rst +++ b/dev/breeze/doc/11_registry_tasks.rst @@ -50,6 +50,47 @@ 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 + +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 .......................... diff --git a/dev/breeze/doc/images/output_registry.svg b/dev/breeze/doc/images/output_registry.svg index 80d5d4def0813..e4b4f92c4f861 100644 --- a/dev/breeze/doc/images/output_registry.svg +++ b/dev/breeze/doc/images/output_registry.svg @@ -1,4 +1,4 @@ - + diff --git a/dev/breeze/doc/images/output_registry.txt b/dev/breeze/doc/images/output_registry.txt index 6888f6b8f6fb7..dae5504430be8 100644 --- a/dev/breeze/doc/images/output_registry.txt +++ b/dev/breeze/doc/images/output_registry.txt @@ -1 +1 @@ -94b4d28badb1f32f4e3c2d24bf337d78 +297843509448a55e7941eed3c0485df8 diff --git a/dev/breeze/doc/images/output_registry_backfill.svg b/dev/breeze/doc/images/output_registry_backfill.svg new file mode 100644 index 0000000000000..12b49bb040261 --- /dev/null +++ b/dev/breeze/doc/images/output_registry_backfill.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Command: registry backfill + + + + + + + + + + +Usage:breeze registry backfill[OPTIONS] + +Extract runtime parameters and connections for older provider versions. Uses 'uv run --with' to install the specific  +version in a temporary environment and runs extract_parameters.py + extract_connections.py. No Docker needed. + +╭─ Backfill flags ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ +*--providerProvider ID (e.g. 'amazon', 'google', 'microsoft-azure'). [required](TEXT) +*--version Version(s) to extract. Can be specified multiple times: --version 9.21.0 --version 9.20.0 [required] +(TEXT) +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Common options ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--verbose-vPrint verbose information about performed steps. +--dry-run-DIf dry-run is set, commands are only printed, not executed. +--help   -hShow this message and exit. +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + diff --git a/dev/breeze/doc/images/output_registry_backfill.txt b/dev/breeze/doc/images/output_registry_backfill.txt new file mode 100644 index 0000000000000..78e2c611d7680 --- /dev/null +++ b/dev/breeze/doc/images/output_registry_backfill.txt @@ -0,0 +1 @@ +e83ed21dca79179e4d064a17f8cd08be diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg index 34ca9601a7928..b33ae7f03e5f7 100644 --- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg +++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg @@ -203,8 +203,8 @@ | k8s:create-cluster | k8s:delete-cluster | k8s:deploy-airflow | k8s:dev | k8s:k9s | k8s:logs |  k8s:run-complete-tests | k8s:setup-env | k8s:shell | k8s:status | k8s:tests | k8s:upload-k8s-image | pr | pr:auto-triage | prod-image | prod-image:build | prod-image:load | prod-image:pull | prod-image:save |  -prod-image:verify | registry | registry:extract-data | registry:publish-versions | release-management |  -release-management:add-back-references | release-management:check-release-files |  +prod-image:verify | registry | registry:backfill | registry:extract-data | registry:publish-versions |  +release-management | release-management:add-back-references | release-management:check-release-files |  release-management:clean-old-provider-artifacts | release-management:constraints-version-check |  release-management:create-minor-branch | release-management:generate-constraints |  release-management:generate-issue-content-core | release-management:generate-issue-content-helm-chart |  diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt index 71800db5118f5..4b4b042063c05 100644 --- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt +++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt @@ -1 +1 @@ -37967154c159533e69675ebf1a2ad104 +acfe23e5b4622df765994caf52f455d2 diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg index b08e9e63156aa..d44be5b47add9 100644 --- a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg +++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg @@ -223,7 +223,7 @@ k8s:deploy-airflow | k8s:dev | k8s:k9s | k8s:logs | k8s:run-complete-tests | k8s:setup-env | k8s:shell | k8s:status | k8s:tests | k8s:upload-k8s-image | pr | pr:auto-triage | prod-image | prod-image:build  | prod-image:load | prod-image:pull | prod-image:save | prod-image:verify | registry |  -registry:extract-data | registry:publish-versions | release-management |  +registry:backfill | registry:extract-data | registry:publish-versions | release-management |  release-management:add-back-references | release-management:check-release-files |  release-management:clean-old-provider-artifacts | release-management:constraints-version-check |  release-management:create-minor-branch | release-management:generate-constraints |  diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt index ac78480ec0c36..5cb537dfca986 100644 --- a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt +++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt @@ -1 +1 @@ -5bd6b8a7281b30045aadce6f7c2576c6 +cd2b1818493fedb67afad21a6a46f98d diff --git a/dev/breeze/src/airflow_breeze/commands/registry_commands.py b/dev/breeze/src/airflow_breeze/commands/registry_commands.py index 68b74f6a1139a..b09b4be4c183b 100644 --- a/dev/breeze/src/airflow_breeze/commands/registry_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/registry_commands.py @@ -16,8 +16,10 @@ # under the License. from __future__ import annotations +import json import sys import uuid +from pathlib import Path import click @@ -27,6 +29,8 @@ from airflow_breeze.utils.ci_group import ci_group from airflow_breeze.utils.click_utils import BreezeGroup from airflow_breeze.utils.docker_command_utils import execute_command_in_shell, fix_ownership_using_docker +from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH +from airflow_breeze.utils.run_utils import run_command @click.group(cls=BreezeGroup, name="registry", help="Tools for the Airflow Provider Registry") @@ -96,9 +100,192 @@ def extract_data(python: str, provider: str | None): help="Path to providers.json. Auto-detected if not provided.", ) def publish_versions(s3_bucket: str, providers_json: str | None): - from pathlib import Path - from airflow_breeze.utils.publish_registry_versions import publish_versions as _publish_versions providers_path = Path(providers_json) if providers_json else None _publish_versions(s3_bucket, providers_json_path=providers_path) + + +PROVIDERS_DIR = AIRFLOW_ROOT_PATH / "providers" +DEV_REGISTRY_DIR = AIRFLOW_ROOT_PATH / "dev" / "registry" + +PROVIDERS_JSON_PATH = DEV_REGISTRY_DIR / "providers.json" + +EXTRACT_SCRIPTS = [ + DEV_REGISTRY_DIR / "extract_parameters.py", + DEV_REGISTRY_DIR / "extract_connections.py", +] + + +def _find_provider_yaml(provider_id: str) -> Path: + """Find provider.yaml for a given provider ID (e.g. 'amazon', 'apache-beam', 'microsoft-azure').""" + # Provider ID uses hyphens; directory structure uses slashes (e.g. microsoft-azure -> microsoft/azure) + parts = provider_id.split("-") + # Try nested first (e.g. 'microsoft/azure'), then single directory (e.g. 'amazon') + candidates = [PROVIDERS_DIR / provider_id / "provider.yaml"] + if len(parts) >= 2: + candidates.insert(0, PROVIDERS_DIR / "/".join(parts) / "provider.yaml") + for candidate in candidates: + if candidate.exists(): + return candidate + raise click.ClickException( + f"provider.yaml not found for '{provider_id}'. Tried: {', '.join(str(c) for c in candidates)}" + ) + + +def _read_provider_yaml_info(provider_id: str) -> tuple[str, list[str]]: + """Read package name from provider.yaml and extras from pyproject.toml.""" + try: + import tomllib + except ImportError: + import tomli as tomllib + + import yaml + + provider_yaml_path = _find_provider_yaml(provider_id) + with open(provider_yaml_path) as f: + data = yaml.safe_load(f) + package_name = data["package-name"] + + pyproject = provider_yaml_path.parent / "pyproject.toml" + extras: list[str] = [] + if pyproject.exists(): + with open(pyproject, "rb") as f: + toml_data = tomllib.load(f) + optional_deps = toml_data.get("project", {}).get("optional-dependencies", {}) + extras = sorted(optional_deps.keys()) + + return package_name, extras + + +def _build_pip_spec(package_name: str, extras: list[str], version: str) -> str: + """Build pip install spec, e.g. 'apache-airflow-providers-amazon[pandas,s3fs]==9.21.0'.""" + if extras: + extras_str = ",".join(extras) + return f"{package_name}[{extras_str}]=={version}" + return f"{package_name}=={version}" + + +def _ensure_providers_json(provider_id: str, package_name: str) -> Path: + """Ensure dev/registry/providers.json exists with the target provider. + + The extraction scripts read this to determine which version to tag output with. + If it exists (from a previous extract-data or S3 download), use it. + If the provider is missing from an existing file, append it rather than replacing. + + NOTE: Does NOT touch registry/src/_data/providers.json, which is used by + the Eleventy build and must contain all providers. + """ + PROVIDERS_JSON_PATH.parent.mkdir(parents=True, exist_ok=True) + + if PROVIDERS_JSON_PATH.exists(): + with open(PROVIDERS_JSON_PATH) as f: + data = json.load(f) + if any(p["id"] == provider_id for p in data.get("providers", [])): + return PROVIDERS_JSON_PATH + # Provider not in file — append it rather than replacing + data["providers"].append({"id": provider_id, "package_name": package_name, "version": "0.0.0"}) + click.echo(f"Added {provider_id} to existing {PROVIDERS_JSON_PATH}") + else: + data = {"providers": [{"id": provider_id, "package_name": package_name, "version": "0.0.0"}]} + click.echo(f"Created minimal {PROVIDERS_JSON_PATH}") + + with open(PROVIDERS_JSON_PATH, "w") as f: + json.dump(data, f, indent=2) + return PROVIDERS_JSON_PATH + + +def _patch_providers_json(providers_json_path: Path, provider_id: str, version: str) -> str: + """Patch providers.json to set the target version. Returns the original version.""" + with open(providers_json_path) as f: + data = json.load(f) + for p in data["providers"]: + if p["id"] == provider_id: + original_version = p["version"] + p["version"] = version + with open(providers_json_path, "w") as f: + json.dump(data, f, indent=2) + return original_version + raise click.ClickException(f"Provider '{provider_id}' not found in {providers_json_path}") + + +# TODO: The backfill command processes versions sequentially because extract_parameters.py +# and extract_connections.py write to shared files (modules.json, providers.json). +# To parallelize, each provider would need its own isolated output directory so that +# concurrent runs don't clobber each other. See also the registry-backfill.yml workflow +# which uses a GitHub Actions matrix to run providers in parallel CI jobs. + + +@registry_group.command( + name="backfill", + help="Extract runtime parameters and connections for older provider versions. " + "Uses 'uv run --with' to install the specific version in a temporary environment " + "and runs extract_parameters.py + extract_connections.py. No Docker needed.", +) +@click.option( + "--provider", + required=True, + help="Provider ID (e.g. 'amazon', 'google', 'microsoft-azure').", +) +@click.option( + "--version", + "versions", + required=True, + multiple=True, + help="Version(s) to extract. Can be specified multiple times: --version 9.21.0 --version 9.20.0", +) +@option_verbose +@option_dry_run +def backfill(provider: str, versions: tuple[str, ...]): + package_name, extras = _read_provider_yaml_info(provider) + providers_json_path = _ensure_providers_json(provider, package_name) + + click.echo(f"Provider: {provider} ({package_name})") + click.echo(f"Versions: {', '.join(versions)}") + if extras: + click.echo(f"Extras: {', '.join(extras)}") + click.echo() + + failed: list[str] = [] + + for version in versions: + click.echo(f"{'=' * 60}") + click.echo(f"Extracting {provider} {version}") + click.echo(f"{'=' * 60}") + + original_version = _patch_providers_json(providers_json_path, provider, version) + + try: + pip_spec = _build_pip_spec(package_name, extras, version) + base_spec = f"{package_name}=={version}" + for script in EXTRACT_SCRIPTS: + click.echo(f"\nRunning {script.name} with {pip_spec}...") + result = run_command( + ["uv", "run", "--with", pip_spec, "python", str(script)], + check=False, + cwd=str(AIRFLOW_ROOT_PATH), + ) + if result.returncode != 0 and pip_spec != base_spec: + click.echo(f"Retrying {script.name} without extras...") + result = run_command( + ["uv", "run", "--with", base_spec, "python", str(script)], + check=False, + cwd=str(AIRFLOW_ROOT_PATH), + ) + if result.returncode != 0: + click.echo(f"WARNING: {script.name} failed for {version} (exit {result.returncode})") + failed.append(f"{version}/{script.name}") + finally: + _patch_providers_json(providers_json_path, provider, original_version) + + click.echo(f"\n{'=' * 60}") + if failed: + click.echo(f"Completed with failures: {', '.join(failed)}") + sys.exit(1) + else: + click.echo(f"Successfully extracted {len(versions)} version(s) for {provider}") + click.echo( + f"\nOutput written to:\n" + f" registry/src/_data/versions/{provider}//parameters.json\n" + f" registry/src/_data/versions/{provider}//connections.json" + ) diff --git a/dev/breeze/src/airflow_breeze/commands/registry_commands_config.py b/dev/breeze/src/airflow_breeze/commands/registry_commands_config.py index 2e3f579e50b6f..fdd156d45a34b 100644 --- a/dev/breeze/src/airflow_breeze/commands/registry_commands_config.py +++ b/dev/breeze/src/airflow_breeze/commands/registry_commands_config.py @@ -20,6 +20,7 @@ "name": "Registry commands", "commands": [ "extract-data", + "backfill", "publish-versions", ], } @@ -34,6 +35,15 @@ ], }, ], + "breeze registry backfill": [ + { + "name": "Backfill flags", + "options": [ + "--provider", + "--version", + ], + }, + ], "breeze registry publish-versions": [ { "name": "Publish versions flags", diff --git a/dev/breeze/tests/test_registry_backfill.py b/dev/breeze/tests/test_registry_backfill.py new file mode 100644 index 0000000000000..2eb4b732eb550 --- /dev/null +++ b/dev/breeze/tests/test_registry_backfill.py @@ -0,0 +1,189 @@ +# 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. +"""Unit tests for the registry backfill command helpers.""" + +from __future__ import annotations + +import json +from unittest.mock import patch + +import pytest + +from airflow_breeze.commands.registry_commands import ( + _build_pip_spec, + _ensure_providers_json, + _find_provider_yaml, + _patch_providers_json, + _read_provider_yaml_info, +) + + +# --------------------------------------------------------------------------- +# _find_provider_yaml +# --------------------------------------------------------------------------- +class TestFindProviderYaml: + def test_simple_provider(self): + path = _find_provider_yaml("amazon") + assert path.name == "provider.yaml" + assert "providers/amazon" in str(path) + + def test_hyphenated_provider(self): + path = _find_provider_yaml("microsoft-azure") + assert path.name == "provider.yaml" + assert "providers/microsoft/azure" in str(path) + + def test_triple_hyphenated_provider(self): + path = _find_provider_yaml("apache-beam") + assert path.name == "provider.yaml" + assert "providers/apache/beam" in str(path) or "providers/apache-beam" in str(path) + + def test_unknown_provider_raises(self): + with pytest.raises(Exception, match="provider.yaml not found"): + _find_provider_yaml("nonexistent-provider-xyz") + + +# --------------------------------------------------------------------------- +# _read_provider_yaml_info +# --------------------------------------------------------------------------- +class TestReadProviderYamlInfo: + def test_reads_package_name_and_extras(self, tmp_path): + provider_dir = tmp_path / "providers" / "amazon" + provider_dir.mkdir(parents=True) + (provider_dir / "provider.yaml").write_text("package-name: apache-airflow-providers-amazon\n") + (provider_dir / "pyproject.toml").write_text( + '[project.optional-dependencies]\npandas = ["pandas>=2.1.2"]\ns3fs = ["s3fs>=2024.6.1"]\n' + ) + with patch("airflow_breeze.commands.registry_commands.PROVIDERS_DIR", tmp_path / "providers"): + package_name, extras = _read_provider_yaml_info("amazon") + assert package_name == "apache-airflow-providers-amazon" + assert extras == ["pandas", "s3fs"] + + def test_no_pyproject_returns_empty_extras(self, tmp_path): + provider_dir = tmp_path / "providers" / "ftp" + provider_dir.mkdir(parents=True) + (provider_dir / "provider.yaml").write_text("package-name: apache-airflow-providers-ftp\n") + with patch("airflow_breeze.commands.registry_commands.PROVIDERS_DIR", tmp_path / "providers"): + package_name, extras = _read_provider_yaml_info("ftp") + assert package_name == "apache-airflow-providers-ftp" + assert extras == [] + + def test_pyproject_without_optional_deps(self, tmp_path): + provider_dir = tmp_path / "providers" / "sqlite" + provider_dir.mkdir(parents=True) + (provider_dir / "provider.yaml").write_text("package-name: apache-airflow-providers-sqlite\n") + (provider_dir / "pyproject.toml").write_text("[project]\nname = 'test'\n") + with patch("airflow_breeze.commands.registry_commands.PROVIDERS_DIR", tmp_path / "providers"): + _, extras = _read_provider_yaml_info("sqlite") + assert extras == [] + + +# --------------------------------------------------------------------------- +# _build_pip_spec +# --------------------------------------------------------------------------- +class TestBuildPipSpec: + def test_with_extras(self): + result = _build_pip_spec("apache-airflow-providers-amazon", ["pandas", "s3fs"], "9.21.0") + assert result == "apache-airflow-providers-amazon[pandas,s3fs]==9.21.0" + + def test_without_extras(self): + result = _build_pip_spec("apache-airflow-providers-ftp", [], "1.0.0") + assert result == "apache-airflow-providers-ftp==1.0.0" + + def test_single_extra(self): + result = _build_pip_spec("apache-airflow-providers-google", ["leveldb"], "10.0.0") + assert result == "apache-airflow-providers-google[leveldb]==10.0.0" + + +# --------------------------------------------------------------------------- +# _ensure_providers_json +# --------------------------------------------------------------------------- +class TestEnsureProvidersJson: + def test_creates_new_file(self, tmp_path): + providers_json = tmp_path / "dev" / "registry" / "providers.json" + with patch( + "airflow_breeze.commands.registry_commands.PROVIDERS_JSON_PATH", + providers_json, + ): + result = _ensure_providers_json("amazon", "apache-airflow-providers-amazon") + + assert result == providers_json + data = json.loads(providers_json.read_text()) + assert len(data["providers"]) == 1 + assert data["providers"][0]["id"] == "amazon" + assert data["providers"][0]["package_name"] == "apache-airflow-providers-amazon" + + def test_appends_to_existing_file(self, tmp_path): + providers_json = tmp_path / "providers.json" + providers_json.write_text( + json.dumps({"providers": [{"id": "google", "package_name": "pkg-google", "version": "1.0.0"}]}) + ) + with patch( + "airflow_breeze.commands.registry_commands.PROVIDERS_JSON_PATH", + providers_json, + ): + _ensure_providers_json("amazon", "apache-airflow-providers-amazon") + + data = json.loads(providers_json.read_text()) + assert len(data["providers"]) == 2 + ids = [p["id"] for p in data["providers"]] + assert "google" in ids + assert "amazon" in ids + + def test_skips_if_provider_already_present(self, tmp_path): + providers_json = tmp_path / "providers.json" + original = {"providers": [{"id": "amazon", "package_name": "pkg", "version": "1.0.0"}]} + providers_json.write_text(json.dumps(original)) + with patch( + "airflow_breeze.commands.registry_commands.PROVIDERS_JSON_PATH", + providers_json, + ): + _ensure_providers_json("amazon", "pkg") + + # File should be unchanged + data = json.loads(providers_json.read_text()) + assert len(data["providers"]) == 1 + + +# --------------------------------------------------------------------------- +# _patch_providers_json +# --------------------------------------------------------------------------- +class TestPatchProvidersJson: + def test_patches_version(self, tmp_path): + providers_json = tmp_path / "providers.json" + providers_json.write_text(json.dumps({"providers": [{"id": "amazon", "version": "9.22.0"}]})) + original = _patch_providers_json(providers_json, "amazon", "9.15.0") + assert original == "9.22.0" + + data = json.loads(providers_json.read_text()) + assert data["providers"][0]["version"] == "9.15.0" + + def test_raises_for_missing_provider(self, tmp_path): + providers_json = tmp_path / "providers.json" + providers_json.write_text(json.dumps({"providers": [{"id": "google", "version": "1.0.0"}]})) + with pytest.raises(Exception, match="not found"): + _patch_providers_json(providers_json, "amazon", "9.15.0") + + def test_restores_original_version(self, tmp_path): + providers_json = tmp_path / "providers.json" + providers_json.write_text(json.dumps({"providers": [{"id": "amazon", "version": "9.22.0"}]})) + # Patch to target version + _patch_providers_json(providers_json, "amazon", "9.15.0") + # Restore + _patch_providers_json(providers_json, "amazon", "9.22.0") + + data = json.loads(providers_json.read_text()) + assert data["providers"][0]["version"] == "9.22.0" diff --git a/registry/src/_data/providerVersions.js b/registry/src/_data/providerVersions.js index 52bad7160d60c..774a0342e5ffd 100644 --- a/registry/src/_data/providerVersions.js +++ b/registry/src/_data/providerVersions.js @@ -72,6 +72,17 @@ module.exports = function () { const latestAirflow = provider.airflow_versions && provider.airflow_versions.length > 0 ? provider.airflow_versions[provider.airflow_versions.length - 1] : null; + + // Compute module_counts from modules.json (runtime discovery) when available, + // since providers.json may only have AST-based counts which undercount. + if (latestModules.length > 0) { + const counts = {}; + for (const m of latestModules) { + counts[m.type] = (counts[m.type] || 0) + 1; + } + provider.module_counts = counts; + } + result.push({ provider, version: provider.version, From 101c537ae5be47597d097bb207af22ec53fcd168 Mon Sep 17 00:00:00 2001 From: GPK Date: Tue, 10 Mar 2026 15:59:49 +0000 Subject: [PATCH 100/280] Fix JWTBearerTIPathDep import errors in HITL routes (#63277) --- .../api_fastapi/execution_api/routes/hitl.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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__) From 11dbd7260902e1c2717ec035e23aa397d1722dff Mon Sep 17 00:00:00 2001 From: Shivam Rastogi <6463385+shivaam@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:15:09 -0700 Subject: [PATCH 101/280] fix: Replace expunge_all with expunge in MetastoreBackend (#63080) MetastoreBackend.get_connection() and get_variable() called session.expunge_all() to detach the returned object from the session. This removed all objects from the shared scoped session, including unrelated pending objects added by other code sharing the same thread-local session. This caused team-scoped DAG bundles to silently fail to persist when sync_bundles_to_db triggered a connection lookup through S3DagBundle's view_url_template, which initializes an S3Hook and calls get_connection. Replace expunge_all() with expunge(obj) to only detach the specific queried Connection or Variable, leaving all other session state intact. closes: #62244 --- airflow-core/src/airflow/secrets/metastore.py | 5 +- .../unit/always/test_secrets_metastore.py | 110 ++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 airflow-core/tests/unit/always/test_secrets_metastore.py 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/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" From 61bf03d4fa87521e12e5393ecf93580ad31a5700 Mon Sep 17 00:00:00 2001 From: Saksham Singhal Date: Tue, 10 Mar 2026 21:45:48 +0530 Subject: [PATCH 102/280] fix: add null checks for dag_version access in scheduler (#62225) * fix: add null checks for dag_version access in scheduler * Fix nullable dag_version for scheduler callbacks (AIP-66) Replace silent skip-on-None guards with a proper fallback strategy for all four TaskCallbackRequest / EmailRequest creation sites in scheduler_job_runner.py. Problem: When ti.dag_version is None (tasks migrated from Airflow 2 to 3), the scheduler crashed with AttributeError when accessing ti.dag_version.bundle_name / bundle_version, and the previous fix silently suppressed callbacks entirely for legacy tasks. Fix: Use an inline ternary fallback, mirroring the existing pattern in dagrun.py (DagCallbackRequest): bundle_name <- dag_version.bundle_name OR dag_model.bundle_name bundle_version <- dag_version.bundle_version OR dag_run.bundle_version DagModel.bundle_name is NOT NULL so the fallback is always safe. DagRun.bundle_version is nullable (str | None), matching the type expected by BaseCallbackRequest. Affected sites: 1. process_executor_events - TaskCallbackRequest (externally killed) 2. process_executor_events - EmailRequest (email on failure/retry) 3. _maybe_requeue_stuck_ti - TaskCallbackRequest (stuck-in-queued) 4. _purge_task_instances_without_heartbeats - TaskCallbackRequest Tests: Added TestSchedulerCallbackBundleInfoDagVersionNullable to test_scheduler_job.py with parameterized cases covering: - dag_version present -> bundle info from dag_version - dag_version None -> bundle info from dag_model / dag_run - no AttributeError crash in either case - correct precedence of dag_version over fallback values * fix: backfill dag_version_id for legacy tasks to avoid Pydantic ValidationError The Pydantic TaskInstance datamodel requires dag_version_id to be a strict uuid.UUID (not None). After removing the old 'skip when dag_version is None' guard, legacy tasks migrated from Airflow 2 triggered a ValidationError when constructing TaskCallbackRequest. Fix: Add _ensure_ti_has_dag_version_id() helper that: 1. Returns True immediately if dag_version_id is already set 2. Backfills from DagVersion.get_latest_version() when missing 3. Returns False (skip with warning) only when no DagVersion exists at all for the dag_id This helper is called at all 4 callback creation sites, right before constructing TaskCallbackRequest / EmailRequest. It ensures: - Callbacks are sent whenever possible (backfill succeeds) - Clean skip with warning only in edge case (no DagVersion at all) - Pydantic validation never sees None for dag_version_id Updated test_purge_without_heartbeat_skips_when_missing_dag_version to verify the new backfill behavior: since dag_maker creates a DagVersion, the backfill succeeds and the callback IS sent. * fix: replace invalid 'continue' with 'if' guard in _maybe_requeue_stuck_ti The 'continue' statement was inside _maybe_requeue_stuck_ti() method, which is not a loop - causing a SyntaxError at import time. This broke all CI jobs that import scheduler_job_runner. Restructured the logic to use an if-guard: when dag_version_id cannot be backfilled, we skip the callback but still let the finally block mark the task as FAILED. * fixed static check --- .../src/airflow/jobs/scheduler_job_runner.py | 122 +++++++++++--- .../tests/unit/jobs/test_scheduler_job.py | 155 +++++++++++++++++- 2 files changed, 243 insertions(+), 34 deletions(-) diff --git a/airflow-core/src/airflow/jobs/scheduler_job_runner.py b/airflow-core/src/airflow/jobs/scheduler_job_runner.py index 24dbf8d1e98c0..2d58f295c6ecc 100644 --- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py +++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py @@ -167,6 +167,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. @@ -1344,10 +1379,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=( @@ -1381,10 +1426,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", @@ -2670,21 +2726,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) @@ -3025,17 +3093,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( diff --git a/airflow-core/tests/unit/jobs/test_scheduler_job.py b/airflow-core/tests/unit/jobs/test_scheduler_job.py index 57536416caba6..3aeebcedbdc2c 100644 --- a/airflow-core/tests/unit/jobs/test_scheduler_job.py +++ b/airflow-core/tests/unit/jobs/test_scheduler_job.py @@ -2956,20 +2956,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): @@ -9157,3 +9155,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" From cec26cb46984b60b1047d588fc624b6761b5efa7 Mon Sep 17 00:00:00 2001 From: Ash Berlin-Taylor Date: Tue, 10 Mar 2026 16:30:34 +0000 Subject: [PATCH 103/280] Make start_date optional for @continuous schedule (#61405) When start_date is not specified, continuous DAGs will begin running immediately when unpaused, using the current time as the starting point. Previously if you didn't have a start date (the default in 3.0) it would do nothing when it was unpaused. This aligns with Airflow 3.0's philosophy of making start_date optional for schedules that don't require historical backfilling. --- ...inuous-optional-start-date.improvement.rst | 1 + airflow-core/src/airflow/timetables/simple.py | 10 ++++------ .../timetables/test_continuous_timetable.py | 20 +++++++++++++++++-- 3 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 airflow-core/newsfragments/continuous-optional-start-date.improvement.rst 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/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/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) From 435031a2983e48394fdfc3c3d529d234ff39ddae Mon Sep 17 00:00:00 2001 From: deepinsight coder <32898216+Vamsi-klu@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:45:58 -0700 Subject: [PATCH 104/280] Validate update_mask fields in PATCH endpoints against Pydantic models (#62657) * Validate update_mask fields in PATCH endpoints against partial Pydantic models When update_mask is provided in PATCH endpoints, validation was either skipped or run against full models with required fields, causing failures. Now uses partial Pydantic models (all fields Optional) for update_mask validation, while keeping full-model validation when no update_mask. - Add make_partial_model() helper that creates a subclass with all fields Optional while preserving validators, model_config, and field metadata - Create ConnectionBodyPartial, VariableBodyPartial, DAGPatchBodyPartial - Use partial models in connections, dags, and variables PATCH endpoints - Add tests for make_partial_model and endpoint-level partial validation Co-Authored-By: Claude Opus 4.6 * Fix mypy and ruff CI failures in make_partial_model - Replace `Optional[ann]` with `ann | None` to satisfy ruff UP045/UP007 - Add type: ignore for mypy since runtime type manipulation can't be statically analyzed - Remove unused `Optional` import Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .../src/airflow/api_fastapi/core_api/base.py | 24 +++++- .../core_api/datamodels/connections.py | 5 +- .../api_fastapi/core_api/datamodels/dags.py | 5 +- .../core_api/datamodels/variables.py | 5 +- .../core_api/routes/public/connections.py | 16 +++- .../core_api/routes/public/dags.py | 5 ++ .../core_api/services/public/variables.py | 16 +++- .../routes/public/test_connections.py | 28 +++++++ .../core_api/routes/public/test_variables.py | 17 ++++ .../unit/api_fastapi/core_api/test_base.py | 78 +++++++++++++++++++ 10 files changed, 186 insertions(+), 13 deletions(-) create mode 100644 airflow-core/tests/unit/api_fastapi/core_api/test_base.py 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/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/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/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/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/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_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/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" From 5f4fc632ea8945745d3c6f8268a66ec5eaeba5f6 Mon Sep 17 00:00:00 2001 From: Kunal Bhattacharya Date: Tue, 10 Mar 2026 22:21:10 +0530 Subject: [PATCH 105/280] Remove remaining session query usages (#62758) * Remove remaining session.query usages * Remove remaining session.query usages * Change as per review comment * Minor comment update in compat.sdk --- airflow-core/tests/unit/models/test_deadline.py | 2 +- .../compat/src/airflow/providers/common/compat/sdk.py | 2 +- .../standard/tests/unit/standard/utils/test_skipmixin.py | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/airflow-core/tests/unit/models/test_deadline.py b/airflow-core/tests/unit/models/test_deadline.py index 379998efbb88c..f4f435291ce09 100644 --- a/airflow-core/tests/unit/models/test_deadline.py +++ b/airflow-core/tests/unit/models/test_deadline.py @@ -166,7 +166,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) diff --git a/providers/common/compat/src/airflow/providers/common/compat/sdk.py b/providers/common/compat/src/airflow/providers/common/compat/sdk.py index f64fc215cf3dd..93174df7b2a28 100644 --- a/providers/common/compat/src/airflow/providers/common/compat/sdk.py +++ b/providers/common/compat/src/airflow/providers/common/compat/sdk.py @@ -326,7 +326,7 @@ # Module map: module_name -> module_path(s) # For entire modules that have been moved (e.g., timezone) -# Usage: from airflow.providers.common.compat.lazy_compat import timezone +# Usage: from airflow.providers.common.compat.sdk import timezone _MODULE_MAP: dict[str, str | tuple[str, ...]] = { "timezone": ("airflow.sdk.timezone", "airflow.utils.timezone"), "io": ("airflow.sdk.io", "airflow.io"), diff --git a/providers/standard/tests/unit/standard/utils/test_skipmixin.py b/providers/standard/tests/unit/standard/utils/test_skipmixin.py index 58e9b8e9a202a..10b46f80a5e99 100644 --- a/providers/standard/tests/unit/standard/utils/test_skipmixin.py +++ b/providers/standard/tests/unit/standard/utils/test_skipmixin.py @@ -26,7 +26,11 @@ from airflow.models.taskinstance import TaskInstance as TI from airflow.providers.common.compat.sdk import AirflowException, SkipMixin from airflow.providers.standard.operators.empty import EmptyOperator -from airflow.utils import timezone + +try: + from airflow.providers.common.compat.sdk import timezone +except ImportError: # Fallback for Airflow < 3.1 + from airflow.utils import timezone # type: ignore[attr-defined,no-redef] from airflow.utils.state import State from airflow.utils.types import DagRunType @@ -104,7 +108,7 @@ def test_skip_none_tasks(self): else: session = Mock() assert SkipMixin().skip(dag_run=None, execution_date=None, tasks=[]) is None - assert not session.query.called + assert not session.scalars.called assert not session.commit.called @pytest.mark.skipif(not AIRFLOW_V_3_0_PLUS, reason="Airflow 2 had a different implementation") From ec3fec26d9b24d335a88c3e78aeb7cd296bdd624 Mon Sep 17 00:00:00 2001 From: Yunho Jung <87285536+yunhobb@users.noreply.github.com> Date: Wed, 11 Mar 2026 02:09:18 +0900 Subject: [PATCH 106/280] Add skip_on_exit_code support to EcsRunTaskOperator (#63274) Allow users to specify exit codes that should raise an AirflowSkipException (marking the task as skipped) via the new `skip_on_exit_code` parameter. This is consistent with the existing behavior in DockerOperator and KubernetesPodOperator. --- .../providers/amazon/aws/operators/ecs.py | 25 +++++++++++-- .../unit/amazon/aws/operators/test_ecs.py | 37 ++++++++++++++++++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/providers/amazon/src/airflow/providers/amazon/aws/operators/ecs.py b/providers/amazon/src/airflow/providers/amazon/aws/operators/ecs.py index 5e4a4b04f900b..05f0102152d4a 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/operators/ecs.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/operators/ecs.py @@ -18,7 +18,7 @@ from __future__ import annotations import re -from collections.abc import Sequence +from collections.abc import Container, Sequence from datetime import timedelta from functools import cached_property from time import sleep @@ -39,7 +39,7 @@ from airflow.providers.amazon.aws.utils.identifiers import generate_uuid from airflow.providers.amazon.aws.utils.mixins import aws_template_fields from airflow.providers.amazon.aws.utils.task_log_fetcher import AwsTaskLogFetcher -from airflow.providers.common.compat.sdk import AirflowException, conf +from airflow.providers.common.compat.sdk import AirflowException, AirflowSkipException, conf from airflow.utils.helpers import prune_dict if TYPE_CHECKING: @@ -394,6 +394,9 @@ class EcsRunTaskOperator(EcsBaseOperator): :param deferrable: If True, the operator will wait asynchronously for the job to complete. This implies waiting for completion. This mode requires aiobotocore module to be installed. (default: False) + :param skip_on_exit_code: If task exits with this exit code, leave the task + in ``skipped`` state (default: None). If set to ``None``, any non-zero + exit code will be treated as a failure. Can be an int or a container of ints. :param do_xcom_push: If True, the operator will push the ECS task ARN to XCom with key 'ecs_task_arn'. Additionally, if logs are fetched, the last log message will be pushed to XCom with the key 'return_value'. (default: False) :param stop_task_on_failure: If True, attempt to stop the ECS task if the Airflow task fails @@ -461,6 +464,7 @@ def __init__( # Set the default waiter duration to 70 days (attempts*delay) # Airflow execution_timeout handles task timeout deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False), + skip_on_exit_code: int | Container[int] | None = None, stop_task_on_failure: bool = True, **kwargs, ): @@ -500,6 +504,13 @@ def __init__( self.waiter_delay = waiter_delay self.waiter_max_attempts = waiter_max_attempts self.deferrable = deferrable + self.skip_on_exit_code = ( + skip_on_exit_code + if isinstance(skip_on_exit_code, Container) + else [skip_on_exit_code] + if skip_on_exit_code is not None + else [] + ) self.stop_task_on_failure = stop_task_on_failure if self._aws_logs_enabled() and not self.wait_for_completion: @@ -763,15 +774,21 @@ def _check_success_task(self) -> None: containers = task["containers"] for container in containers: if container.get("lastStatus") == "STOPPED" and container.get("exitCode", 1) != 0: + exit_code = container.get("exitCode", 1) + if exit_code in self.skip_on_exit_code: + exception_cls: type[AirflowException] = AirflowSkipException + else: + exception_cls = AirflowException + if self.task_log_fetcher: last_logs = "\n".join( self.task_log_fetcher.get_last_log_messages(self.number_logs_exception) ) - raise AirflowException( + raise exception_cls( f"This task is not in success state - last {self.number_logs_exception} " f"logs from Cloudwatch:\n{last_logs}" ) - raise AirflowException(f"This task is not in success state {task}") + raise exception_cls(f"This task is not in success state {task}") if container.get("lastStatus") == "PENDING": raise AirflowException(f"This task is still pending {task}") if "error" in container.get("reason", "").lower(): diff --git a/providers/amazon/tests/unit/amazon/aws/operators/test_ecs.py b/providers/amazon/tests/unit/amazon/aws/operators/test_ecs.py index 5a866e36e115e..946b086e0fe32 100644 --- a/providers/amazon/tests/unit/amazon/aws/operators/test_ecs.py +++ b/providers/amazon/tests/unit/amazon/aws/operators/test_ecs.py @@ -38,7 +38,7 @@ from airflow.providers.amazon.aws.triggers.ecs import TaskDoneTrigger from airflow.providers.amazon.aws.utils.task_log_fetcher import AwsTaskLogFetcher from airflow.providers.amazon.version_compat import NOTSET -from airflow.providers.common.compat.sdk import AirflowException, TaskDeferred +from airflow.providers.common.compat.sdk import AirflowException, AirflowSkipException, TaskDeferred from unit.amazon.aws.utils.test_template_fields import validate_template_fields @@ -603,6 +603,41 @@ def test_check_success_task_not_raises(self, client_mock): self.ecs._check_success_task() client_mock.describe_tasks.assert_called_once_with(cluster="c", tasks=["arn"]) + @mock.patch.object(EcsBaseOperator, "client") + def test_check_success_task_raises_skip_exception(self, client_mock): + self.ecs.arn = "arn" + self.ecs.skip_on_exit_code = [2] + client_mock.describe_tasks.return_value = { + "tasks": [{"containers": [{"name": "container-name", "lastStatus": "STOPPED", "exitCode": 2}]}] + } + with pytest.raises(AirflowSkipException): + self.ecs._check_success_task() + + @mock.patch.object(EcsBaseOperator, "client") + @mock.patch("airflow.providers.amazon.aws.utils.task_log_fetcher.AwsTaskLogFetcher") + def test_check_success_task_skip_exception_with_logs(self, log_fetcher_mock, client_mock): + self.ecs.arn = "arn" + self.ecs.skip_on_exit_code = [2] + self.ecs.task_log_fetcher = log_fetcher_mock + log_fetcher_mock.get_last_log_messages.return_value = ["log1", "log2"] + client_mock.describe_tasks.return_value = { + "tasks": [{"containers": [{"name": "container-name", "lastStatus": "STOPPED", "exitCode": 2}]}] + } + with pytest.raises(AirflowSkipException, match="This task is not in success state"): + self.ecs._check_success_task() + + @mock.patch.object(EcsBaseOperator, "client") + def test_check_success_task_unmatched_exit_code_raises_airflow_exception(self, client_mock): + """Exit codes not in skip_on_exit_code raise AirflowException.""" + self.ecs.arn = "arn" + self.ecs.skip_on_exit_code = [2] + client_mock.describe_tasks.return_value = { + "tasks": [{"containers": [{"name": "container-name", "lastStatus": "STOPPED", "exitCode": 1}]}] + } + with pytest.raises(AirflowException) as ctx: + self.ecs._check_success_task() + assert type(ctx.value) is AirflowException + @pytest.mark.parametrize( ("launch_type", "tags"), [ From dfadbf13a5a3b38f877b16bca150231878a19229 Mon Sep 17 00:00:00 2001 From: Yoann <60654707+YoannAbriel@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:23:41 -0700 Subject: [PATCH 107/280] fix: pass through user-provided password in HiveServer2Hook for all auth modes (#62888) * fix: pass through user-provided password in HiveServer2Hook regardless of auth mechanism Previously, HiveServer2Hook.get_conn() only set the password when auth_mechanism was LDAP, CUSTOM, or PLAIN. For all other auth modes (including the default NONE), user-provided passwords were silently dropped and pyhive would default to sending 'x' as the password. Now the password is passed through whenever the user has explicitly set one in the connection, regardless of the configured auth mechanism. Fixes apache/airflow#62338 * ci: retrigger CI --- .../providers/apache/hive/hooks/hive.py | 7 +++++-- .../tests/unit/apache/hive/hooks/test_hive.py | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py b/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py index efa0397c0d7ea..e5ce0cc604778 100644 --- a/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py +++ b/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py @@ -885,8 +885,11 @@ def get_conn(self, schema: str | None = None) -> Any: auth_mechanism = db.extra_dejson.get("auth_mechanism", "KERBEROS") kerberos_service_name = db.extra_dejson.get("kerberos_service_name", "hive") - # Password should be set if in LDAP, CUSTOM or PLAIN mode - if auth_mechanism in ("LDAP", "CUSTOM", "PLAIN"): + # Pass through the password whenever the user has explicitly set one. + # Previously this was restricted to LDAP/CUSTOM/PLAIN, which caused + # user-provided passwords to be silently dropped for other auth modes + # (pyhive defaults to sending "x" when password is None). + if db.password: password = db.password from pyhive.hive import connect diff --git a/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py b/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py index 94a573a2259c8..1508b2030127f 100644 --- a/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py +++ b/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py @@ -697,6 +697,27 @@ def test_get_conn_with_password_plain(self, mock_connect): database="default", ) + @mock.patch("pyhive.hive.connect") + def test_get_conn_with_password_none_auth(self, mock_connect): + """Test that password is passed through even when auth_mechanism is NONE.""" + conn_id = "conn_none_with_password" + conn_env = CONN_ENV_PREFIX + conn_id.upper() + + with mock.patch.dict( + "os.environ", + {conn_env: "jdbc+hive2://user:mypassword@localhost:10000/default"}, + ): + HiveServer2Hook(hiveserver2_conn_id=conn_id).get_conn() + mock_connect.assert_called_once_with( + host="localhost", + port=10000, + auth="NONE", + kerberos_service_name=None, + username="user", + password="mypassword", + database="default", + ) + @pytest.mark.parametrize( ("host", "port", "schema", "message"), [ From 950d02a19ccf847406c0be0cb183d73e6fab6313 Mon Sep 17 00:00:00 2001 From: Eason09053360 Date: Wed, 11 Mar 2026 01:24:58 +0800 Subject: [PATCH 108/280] docs: add DAG documentation for example_bash_decorator (#62948) * add_DAG_documentation_for_example_bash_decorator * run prek --- .../standard/example_dags/example_bash_decorator.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/providers/standard/src/airflow/providers/standard/example_dags/example_bash_decorator.py b/providers/standard/src/airflow/providers/standard/example_dags/example_bash_decorator.py index e5b005ce40c48..1cd8d5b61ee7b 100644 --- a/providers/standard/src/airflow/providers/standard/example_dags/example_bash_decorator.py +++ b/providers/standard/src/airflow/providers/standard/example_dags/example_bash_decorator.py @@ -28,6 +28,19 @@ @dag(schedule=None, start_date=pendulum.datetime(2023, 1, 1, tz="UTC"), catchup=False) def example_bash_decorator(): + """ + ### Bash TaskFlow Decorator Example + This DAG demonstrates the `@task.bash` decorator for running shell commands from TaskFlow tasks. + It includes: + - Basic bash tasks and loops using `override` + - Jinja templating and context variables + - Skip behavior via non-zero exit codes and conditional branching + - Parameterized environment variables and dynamic command construction + + For details, see the Bash decorator documentation + [here](https://airflow.apache.org/docs/apache-airflow/stable/howto/operator/bash.html). + """ + @task.bash def run_me(sleep_seconds: int, task_instance_key_str: str) -> str: return f"echo {task_instance_key_str} && sleep {sleep_seconds}" From a97a1e90e387998d6cb362715f5a474eb7cd27b7 Mon Sep 17 00:00:00 2001 From: Kushal Bohra Date: Tue, 10 Mar 2026 10:37:59 -0700 Subject: [PATCH 109/280] fix(fab): recover from first idle MySQL disconnect in token auth (#62919) * fix(fab): recover from first idle MySQL disconnect in token auth Retry user deserialization once after clearing the poisoned scoped session so the first request after a server-side idle timeout does not return 500. Add regression coverage for transient disconnect recovery and factorize deserialization lookup logic to avoid duplication. * test(fab): rename retry mock for clarity --- .../fab/auth_manager/fab_auth_manager.py | 21 +++++++++++++---- .../fab/auth_manager/test_fab_auth_manager.py | 23 +++++++++++++++++-- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py index 99fcbeff134da..2ad86559a74ea 100644 --- a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py +++ b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py @@ -267,16 +267,29 @@ def session(self): @cachedmethod(lambda self: self.cache, key=lambda _, token: int(token["sub"])) def deserialize_user(self, token: dict[str, Any]) -> User: + user_id = int(token["sub"]) + + def _fetch_user() -> User: + try: + return self.session.scalars(select(User).where(User.id == user_id)).one() + except NoResultFound: + raise ValueError(f"User with id {token['sub']} not found") + try: - return self.session.scalars(select(User).where(User.id == int(token["sub"]))).one() - except NoResultFound: - raise ValueError(f"User with id {token['sub']} not found") + return _fetch_user() except SQLAlchemyError: # Discard the poisoned scoped session so the next request gets a # fresh connection from the pool instead of a PendingRollbackError. with suppress(Exception): self.session.remove() - raise + try: + return _fetch_user() + except SQLAlchemyError: + # If retry also fails, remove the scoped session again to keep + # future requests from reusing a broken transaction state. + with suppress(Exception): + self.session.remove() + raise def serialize_user(self, user: User) -> dict[str, Any]: return {"sub": str(user.id)} diff --git a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py index 3b3ebd4bbbe3e..99f7dc24edc23 100644 --- a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py +++ b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py @@ -996,7 +996,7 @@ def _patched_session(auth_manager, mock_session): ids=["operational_error", "pending_rollback_error"], ) def test_db_error_calls_session_remove(self, auth_manager_with_appbuilder, raised_exc): - """session.remove() is called on SQLAlchemy errors so the next request recovers.""" + """session.remove() is called on SQLAlchemy errors before and after retry.""" mock_session = MagicMock(spec=["scalars", "remove"]) mock_session.scalars.side_effect = raised_exc auth_manager_with_appbuilder.cache.pop(99997, None) @@ -1005,7 +1005,7 @@ def test_db_error_calls_session_remove(self, auth_manager_with_appbuilder, raise with pytest.raises(type(raised_exc)): auth_manager_with_appbuilder.deserialize_user({"sub": "99997"}) - mock_session.remove.assert_called_once() + assert mock_session.remove.call_count == 2 def test_db_error_propagates_when_session_remove_raises(self, auth_manager_with_appbuilder): """The original SQLAlchemyError propagates even if session.remove() itself raises.""" @@ -1021,6 +1021,25 @@ def test_db_error_propagates_when_session_remove_raises(self, auth_manager_with_ with pytest.raises(OperationalError): auth_manager_with_appbuilder.deserialize_user({"sub": "99997"}) + assert mock_session.remove.call_count == 2 + + def test_db_error_retries_once_and_recovers(self, auth_manager_with_appbuilder): + """A transient DB disconnect is recovered by removing session and retrying once.""" + user = Mock() + user.id = 99996 + original_exc = OperationalError("connection dropped", None, Exception()) + retry_query_result = Mock() + retry_query_result.one.return_value = user + + mock_session = MagicMock(spec=["scalars", "remove"]) + mock_session.scalars.side_effect = [original_exc, retry_query_result] + auth_manager_with_appbuilder.cache.pop(user.id, None) + + with self._patched_session(auth_manager_with_appbuilder, mock_session): + result = auth_manager_with_appbuilder.deserialize_user({"sub": str(user.id)}) + + assert result == user + assert mock_session.scalars.call_count == 2 mock_session.remove.assert_called_once() From a6179703463a488047fdeec0c3d0c244a2ead8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ahlert?= Date: Tue, 10 Mar 2026 14:49:42 -0300 Subject: [PATCH 110/280] Improve translation completeness checker (required keys, coverage, unused) (#62983) * Improve translation completeness checker: required keys, coverage, unused - Required keys: EN base + plural forms for locale when EN has {{count}} - Merge Extra and Unused into single Unused (keys not required) - Table: Required (base EN), Required (plural), Total required, Translated, Missing, Coverage (translated/total), TODOs, Unused - Coverage uses only real translations (exclude TODO placeholders) - Rename --remove-extra to --remove-unused; remove_unused_translations() - Update docs, SKILL, README, and help SVG * Add translation:pt-BR section to boring-cyborg config * Revert "Add translation:pt-BR section to boring-cyborg config" This reverts commit dda93a9ef391741157ce150d2bf2d1256da198d6. * Apply ruff format to ui_commands.py * Update breeze docs: regenerate check-translation-completeness output * Update ui_commands.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(breeze): translation checker row style (red for missing keys) Fix unreachable red branch in style logic: use red for missing keys, yellow for todos/unused, bold green when all clear. Also remove unused missing_counts param and rename extra_keys to unused_keys. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/airflow/ui/public/i18n/README.md | 6 +- dev/breeze/doc/10_ui_tasks.rst | 6 +- ...tput_ui_check-translation-completeness.svg | 6 +- ...tput_ui_check-translation-completeness.txt | 2 +- .../airflow_breeze/commands/ui_commands.py | 246 ++++++++++-------- .../commands/ui_commands_config.py | 2 +- dev/breeze/tests/test_ui_commands.py | 77 +++++- 7 files changed, 218 insertions(+), 127 deletions(-) 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/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/images/output_ui_check-translation-completeness.svg b/dev/breeze/doc/images/output_ui_check-translation-completeness.svg index de811978b2f1e..7815ad487199e 100644 --- a/dev/breeze/doc/images/output_ui_check-translation-completeness.svg +++ b/dev/breeze/doc/images/output_ui_check-translation-completeness.svg @@ -105,9 +105,9 @@ Check completeness of UI translations. ╭─ Translation options ────────────────────────────────────────────────────────────────────────────────────────────────╮ ---language    -lShow summary for a single language (e.g. en, de, pl, etc.) (TEXT) ---add-missing Add missing translations for all languages except English, prefixed with 'TODO: translate:'. ---remove-extraRemove extra translations that are present in the language but missing in English. +--language     -lShow summary for a single language (e.g. en, de, pl, etc.) (TEXT) +--add-missing  Add missing translations for all languages except English, prefixed with 'TODO: translate:'. +--remove-unusedRemove unused translations (keys present in the language but not required). ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Common options ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ --verbose-vPrint verbose information about performed steps. diff --git a/dev/breeze/doc/images/output_ui_check-translation-completeness.txt b/dev/breeze/doc/images/output_ui_check-translation-completeness.txt index 68771c83bbefa..acab51d294f70 100644 --- a/dev/breeze/doc/images/output_ui_check-translation-completeness.txt +++ b/dev/breeze/doc/images/output_ui_check-translation-completeness.txt @@ -1 +1 @@ -f9abfd3676b04ed17b81796c9bcd79a9 +4d8c7e8b2fe193a4b8e8cf25a429ff7e diff --git a/dev/breeze/src/airflow_breeze/commands/ui_commands.py b/dev/breeze/src/airflow_breeze/commands/ui_commands.py index 2051c64d571a2..eb1843c4d2a01 100644 --- a/dev/breeze/src/airflow_breeze/commands/ui_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/ui_commands.py @@ -98,15 +98,15 @@ def char_key(c: str) -> tuple: class LocaleSummary(NamedTuple): """ - Summary of missing and extra translation keys for a file, per locale. + Summary of missing and unused translation keys for a file, per locale. Attributes: - missing_keys: A dictionary mapping locale codes to lists of missing translation keys. - extra_keys: A dictionary mapping locale codes to lists of extra translation keys. + missing_keys: Required keys that the locale does not have. + unused_keys: Keys present in the locale that are not required (never used at runtime). """ missing_keys: dict[str, list[str]] - extra_keys: dict[str, list[str]] + unused_keys: dict[str, list[str]] class LocaleFiles(NamedTuple): @@ -142,9 +142,17 @@ def get_plural_base(key: str, suffixes: list[str]) -> str | None: return None -def expand_plural_keys(keys: set[str], lang: str) -> set[str]: +COUNT_PLACEHOLDER = "{{count}}" + + +def expand_plural_keys(keys: set[str], lang: str, en_key_to_value: dict[str, str] | None = None) -> set[str]: """ - For a set of keys, expand all plural bases to include all required suffixes for the language. + For a set of keys, expand plural bases to include required suffixes for the language. + + When en_key_to_value is provided, only expand a base to all plural forms when at least + one of the English values for that base contains {{count}}. Keys without {{count}} + (e.g. fixed "1 Error") are not expanded, so locales are not required to have + unused forms like error_other when the source only has error_one. """ console = get_console() suffixes = PLURAL_SUFFIXES.get(lang) @@ -162,6 +170,11 @@ def expand_plural_keys(keys: set[str], lang: str) -> set[str]: base_to_suffixes.setdefault(base, set()).add(key[len(base) :]) expanded = set(keys) for base in base_to_suffixes.keys(): + if en_key_to_value is not None: + en_keys_for_base = [k for k in keys if get_plural_base(k, suffixes) == base] + any_has_count = any(COUNT_PLACEHOLDER in en_key_to_value.get(k, "") for k in en_keys_for_base) + if not any_has_count: + continue for suffix in suffixes: expanded.add(base + suffix) return expanded @@ -192,6 +205,18 @@ def flatten_keys(d: dict, prefix: str = "") -> list[str]: return keys +def flatten_keys_to_values(d: dict, prefix: str = "") -> dict[str, str]: + """Build a flat key -> value map for leaf string values (for i18n JSON).""" + result: dict[str, str] = {} + for k, v in d.items(): + full_key = f"{prefix}.{k}" if prefix else k + if isinstance(v, dict): + result.update(flatten_keys_to_values(v, full_key)) + elif isinstance(v, str): + result[full_key] = v + return result + + def compare_keys( locale_files: list[LocaleFiles], ) -> tuple[dict[str, LocaleSummary], dict[str, dict[str, int]]]: @@ -199,7 +224,7 @@ def compare_keys( Compare all non-English locales with English locale only. Returns a tuple: - - summary dict: filename -> LocaleSummary(missing, extra) + - summary dict: filename -> LocaleSummary(missing_keys, unused_keys) - missing_counts dict: filename -> {locale: count_of_missing_keys} """ all_files: set[str] = set() @@ -221,25 +246,32 @@ def compare_keys( key_sets.append(LocaleKeySet(locale=lf.locale, keys=keys)) keys_by_locale = {ks.locale: ks.keys for ks in key_sets} en_keys = keys_by_locale.get("en", set()) or set() - # Expand English keys for all required plural forms in each language - expanded_en_keys = {lang: expand_plural_keys(en_keys, lang) for lang in keys_by_locale.keys()} + en_key_to_value: dict[str, str] = {} + en_path = LOCALES_DIR / "en" / filename + if en_path.exists(): + try: + en_data = load_json(en_path) + en_key_to_value = flatten_keys_to_values(en_data) + except Exception as e: + get_console().print(f"Error loading en values from {en_path}: {e}") + # Required = EN keys plus, for bases with {{count}} in EN, all plural forms for the locale missing_keys: dict[str, list[str]] = {} - extra_keys: dict[str, list[str]] = {} + unused_keys: dict[str, list[str]] = {} missing_counts[filename] = {} for ks in key_sets: if ks.locale == "en": continue - required_keys = expanded_en_keys.get(ks.locale, en_keys) + required_keys = expand_plural_keys(en_keys, ks.locale, en_key_to_value) if ks.keys is None: missing_keys[ks.locale] = list(required_keys) - extra_keys[ks.locale] = [] + unused_keys[ks.locale] = [] missing_counts[filename][ks.locale] = len(required_keys) else: missing = list(required_keys - ks.keys) missing_keys[ks.locale] = missing - extra_keys[ks.locale] = list(ks.keys - required_keys) + unused_keys[ks.locale] = sorted(ks.keys - required_keys) missing_counts[filename][ks.locale] = len(missing) - summary[filename] = LocaleSummary(missing_keys=missing_keys, extra_keys=extra_keys) + summary[filename] = LocaleSummary(missing_keys=missing_keys, unused_keys=unused_keys) return summary, missing_counts @@ -296,15 +328,15 @@ def print_language_summary(locale_files: list[LocaleFiles], summary: dict[str, L for lf in sorted(locale_files): locale = lf.locale file_missing: dict[str, list[str]] = {} - file_extra: dict[str, list[str]] = {} + file_unused: dict[str, list[str]] = {} for filename, diff in sorted(summary.items()): missing_keys = diff.missing_keys.get(locale, []) - extra_keys = diff.extra_keys.get(locale, []) + unused_keys = diff.unused_keys.get(locale, []) if missing_keys: file_missing[filename] = missing_keys - if extra_keys: - file_extra[filename] = extra_keys - if file_missing or file_extra: + if unused_keys: + file_unused[filename] = unused_keys + if file_missing or file_unused: if locale == "en": continue console.print(Panel(f"[bold yellow]{locale}[/bold yellow]", style="blue")) @@ -315,34 +347,38 @@ def print_language_summary(locale_files: list[LocaleFiles], summary: dict[str, L console.print(f" [magenta]{filename}[/magenta]:") for k in keys: console.print(f" [yellow]{k}[/yellow]") - if file_extra: - found_difference = True - console.print("[red] Extra keys (need to be removed/updated):[/red]") - for filename, keys in file_extra.items(): + if file_unused: + console.print("[yellow] Unused keys (not required, can be removed):[/yellow]") + for filename, keys in file_unused.items(): console.print(f" [magenta]{filename}[/magenta]:") for k in keys: - console.print(f" [yellow]{k}[/yellow]") + console.print(f" [dim]{k}[/dim]") return found_difference +def is_todo_value(s: str) -> bool: + """Return True if the string is a TODO placeholder (e.g. 'TODO: translate: ...').""" + return isinstance(s, str) and s.strip().startswith("TODO: translate") + + def count_todos(obj) -> int: """Count TODO: translate entries in a dict or list.""" if isinstance(obj, dict): return sum(count_todos(v) for v in obj.values()) if isinstance(obj, list): return sum(count_todos(v) for v in obj) - if isinstance(obj, str) and obj.strip().startswith("TODO: translate"): + if is_todo_value(obj): return 1 return 0 -def print_translation_progress(locale_files, missing_counts, summary): +def print_translation_progress(locale_files, summary): console = get_console() from rich.table import Table tables = defaultdict(lambda: Table(show_lines=True)) all_files = set() - coverage_per_language = {} # Collect total coverage per language + coverage_per_language = {} for lf in locale_files: all_files.update(lf.files) @@ -355,91 +391,94 @@ def print_translation_progress(locale_files, missing_counts, summary): table = tables[lang] table.title = f"Translation Progress: {lang}" table.add_column("File", style="bold cyan") - table.add_column("Missing", style="red") - table.add_column("Extra", style="yellow") - table.add_column("TODOs", style="magenta") + table.add_column("Required (base EN)", style="dim") + table.add_column("Required (plural)", style="dim") + table.add_column("Total required", style="bold") table.add_column("Translated", style="green") - table.add_column("Total", style="bold") + table.add_column("Missing", style="red") table.add_column("Coverage", style="bold") - table.add_column("Completed", style="bold") + table.add_column("TODOs", style="magenta") + table.add_column("Unused", style="yellow") + total_required_base = 0 + total_required_plural = 0 + total_required = 0 + total_translated = 0 total_missing = 0 - total_extra = 0 total_todos = 0 - total_translated = 0 - total_total = 0 + total_unused = 0 for filename in sorted(all_files): - file_path = Path(LOCALES_DIR / lang / filename) - # Always get total from English version en_path = Path(LOCALES_DIR / "en" / filename) - if en_path.exists(): - with open(en_path) as f: - en_data = json.load(f) - file_total = sum(1 for _ in flatten_keys(en_data)) - else: - file_total = 0 + file_path = Path(LOCALES_DIR / lang / filename) + diff = summary.get(filename, LocaleSummary({}, {})) + if not en_path.exists(): + continue + with open(en_path) as f: + en_data = json.load(f) + en_keys = set(flatten_keys(en_data)) + en_key_to_value = flatten_keys_to_values(en_data) + required_keys = expand_plural_keys(en_keys, lang, en_key_to_value) + required_base_en = len(en_keys) + required_plural = len(required_keys) - required_base_en + total_req = len(required_keys) + file_missing = len(diff.missing_keys.get(lang, [])) + file_unused = len(diff.unused_keys.get(lang, [])) if file_path.exists(): with open(file_path) as f: - data = json.load(f) - file_missing = missing_counts.get(filename, {}).get(lang, 0) - file_extra = len(summary.get(filename, LocaleSummary({}, {})).extra_keys.get(lang, [])) - - file_todos = count_todos(data) + lang_data = json.load(f) + lang_key_to_value = flatten_keys_to_values(lang_data) + file_translated = sum( + 1 + for k in required_keys + if k in lang_key_to_value and not is_todo_value(lang_key_to_value[k]) + ) + file_todos = sum( + 1 for k in required_keys if k in lang_key_to_value and is_todo_value(lang_key_to_value[k]) + ) if file_todos > 0: has_todos = True - file_translated = file_total - file_missing - # Coverage: translated / total - file_coverage_percent = 100 * file_translated / file_total if file_total else 100 - # Complete percent: (translated - todos) / translated - file_actual_translated = file_translated - file_todos - complete_percent = 100 * file_actual_translated / file_translated if file_translated else 100 - style = ( - "bold green" - if file_missing == 0 and file_extra == 0 and file_todos == 0 - else ( - "yellow" if file_missing < file_total or file_extra > 0 or file_todos > 0 else "red" - ) - ) else: - file_missing = file_total - file_extra = len(summary.get(filename, LocaleSummary({}, {})).extra_keys.get(lang, [])) - file_todos = 0 file_translated = 0 - file_coverage_percent = 0 - complete_percent = 0 + file_todos = 0 + file_coverage = 100 * file_translated / total_req if total_req else 100.0 + if file_missing > 0: style = "red" + elif file_todos > 0 or file_unused > 0: + style = "yellow" + else: + style = "bold green" table.add_row( f"[bold reverse]{filename}[/bold reverse]", + str(required_base_en), + str(required_plural), + str(total_req), + str(file_translated), str(file_missing), - str(file_extra), + f"{file_coverage:.1f}%", str(file_todos), - str(file_translated), - str(file_total), - f"{file_coverage_percent:.1f}%", - f"{complete_percent:.1f}%", + str(file_unused), style=style, ) + total_required_base += required_base_en + total_required_plural += required_plural + total_required += total_req + total_translated += file_translated total_missing += file_missing - total_extra += file_extra total_todos += file_todos - total_translated += file_translated - total_total += file_total - - # Calculate totals for this language - total_coverage_percent = 100 * total_translated / total_total if total_total else 100 - total_actual_translated = total_translated - total_todos - total_complete_percent = 100 * total_actual_translated / total_translated if total_translated else 100 + total_unused += file_unused - coverage_per_language[lang] = total_coverage_percent + total_coverage = 100 * total_translated / total_required if total_required else 100.0 + coverage_per_language[lang] = total_coverage table.add_row( "All files", + str(total_required_base), + str(total_required_plural), + str(total_required), + str(total_translated), str(total_missing), - str(total_extra), + f"{total_coverage:.1f}%", str(total_todos), - str(total_translated), - str(total_total), - f"{total_coverage_percent:.1f}%", - f"{total_complete_percent:.1f}%", - style="red" if total_complete_percent < 100 else "bold green", + str(total_unused), + style="red" if total_missing > 0 or total_todos > 0 or total_unused > 0 else "bold green", ) for _lang, table in tables.items(): @@ -512,16 +551,16 @@ def sort_dict_keys(obj): console.print(f"[green]Added missing translations to {lang_path}[/green]") -def remove_extra_translations(language: str, summary: dict[str, LocaleSummary]): +def remove_unused_translations(language: str, summary: dict[str, LocaleSummary]): """ - Remove extra translations for the selected language. + Remove unused translations for the selected language. - Removes keys that are present in the language file but missing in the English file. + Removes keys that are present in the language file but are not required. """ console = get_console() for filename, diff in summary.items(): - extra_keys = set(diff.extra_keys.get(language, [])) - if not extra_keys: + unused_keys = set(diff.unused_keys.get(language, [])) + if not unused_keys: continue lang_path = LOCALES_DIR / language / filename try: @@ -530,12 +569,12 @@ def remove_extra_translations(language: str, summary: dict[str, LocaleSummary]): console.print(f"[yellow]Failed to load {language} file {lang_path}: {e}[/yellow]") continue - # Helper to recursively remove extra keys + # Helper to recursively remove unused keys def remove_keys(dst, prefix=""): keys_to_remove = [] for k, v in list(dst.items()): full_key = f"{prefix}.{k}" if prefix else k - if full_key in extra_keys: + if full_key in unused_keys: keys_to_remove.append(k) elif isinstance(v, dict): remove_keys(v, full_key) @@ -556,7 +595,7 @@ def sort_dict_keys(obj): with open(lang_path, "w", encoding="utf-8") as f: json.dump(lang_data, f, ensure_ascii=False, indent=2) f.write("\n") # Ensure newline at the end of the file - console.print(f"[green]Removed {len(extra_keys)} extra translations from {lang_path}[/green]") + console.print(f"[green]Removed {len(unused_keys)} unused translations from {lang_path}[/green]") @click.group(cls=BreezeGroup, name="ui", help="Tools for UI development and maintenance") @@ -581,15 +620,15 @@ def ui_group(): help="Add missing translations for all languages except English, prefixed with 'TODO: translate:'.", ) @click.option( - "--remove-extra", + "--remove-unused", is_flag=True, default=False, - help="Remove extra translations that are present in the language but missing in English.", + help="Remove unused translations (keys present in the language but not required).", ) @option_verbose @option_dry_run def check_translation_completeness( - language: str | None = None, add_missing: bool = False, remove_extra: bool = False + language: str | None = None, add_missing: bool = False, remove_unused: bool = False ): locale_files = get_locale_files() console = get_console() @@ -608,13 +647,13 @@ def check_translation_completeness( for filename, diff in summary.items(): filtered_summary[filename] = LocaleSummary( missing_keys={lf.locale: diff.missing_keys.get(lf.locale, [])}, - extra_keys={lf.locale: diff.extra_keys.get(lf.locale, [])}, + unused_keys={lf.locale: diff.unused_keys.get(lf.locale, [])}, ) add_missing_translations(lf.locale, filtered_summary) # After adding, re-run the summary for all languages summary, missing_counts = compare_keys(get_locale_files()) - if remove_extra and language != "en": - # Loop through all languages except 'en' and remove extra translations + if remove_unused and language != "en": + # Loop through all languages except 'en' and remove unused translations if language: language_files = [lf for lf in locale_files if lf.locale == language] else: @@ -624,9 +663,9 @@ def check_translation_completeness( for filename, diff in summary.items(): filtered_summary[filename] = LocaleSummary( missing_keys={lf.locale: diff.missing_keys.get(lf.locale, [])}, - extra_keys={lf.locale: diff.extra_keys.get(lf.locale, [])}, + unused_keys={lf.locale: diff.unused_keys.get(lf.locale, [])}, ) - remove_extra_translations(lf.locale, filtered_summary) + remove_unused_translations(lf.locale, filtered_summary) # After removing, re-run the summary for all languages summary, missing_counts = compare_keys(get_locale_files()) if language: @@ -642,7 +681,7 @@ def check_translation_completeness( for filename, diff in summary.items(): filtered_summary[filename] = LocaleSummary( missing_keys={language: diff.missing_keys.get(language, [])}, - extra_keys={language: diff.extra_keys.get(language, [])}, + unused_keys={language: diff.unused_keys.get(language, [])}, ) lang_diff = print_language_summary( [lf for lf in locale_files if lf.locale == language], filtered_summary @@ -653,7 +692,6 @@ def check_translation_completeness( found_difference = found_difference or lang_diff has_todos, coverage_per_language = print_translation_progress( [lf for lf in locale_files if language is None or lf.locale == language], - missing_counts, summary, ) if not found_difference and not has_todos: diff --git a/dev/breeze/src/airflow_breeze/commands/ui_commands_config.py b/dev/breeze/src/airflow_breeze/commands/ui_commands_config.py index 83f9fd34fbfdc..e50e10e6fd6c1 100644 --- a/dev/breeze/src/airflow_breeze/commands/ui_commands_config.py +++ b/dev/breeze/src/airflow_breeze/commands/ui_commands_config.py @@ -31,7 +31,7 @@ "options": [ "--language", "--add-missing", - "--remove-extra", + "--remove-unused", ], }, ], diff --git a/dev/breeze/tests/test_ui_commands.py b/dev/breeze/tests/test_ui_commands.py index 843d985ed0281..8f7b844be796a 100644 --- a/dev/breeze/tests/test_ui_commands.py +++ b/dev/breeze/tests/test_ui_commands.py @@ -61,6 +61,25 @@ def test_expand_plural_keys_polish(self): assert "message_many" in expanded assert "message_other" in expanded + def test_expand_plural_keys_only_expands_when_en_has_count_placeholder(self): + # When en has error_one without {{count}}, we must not require error_other + keys = {"error_one"} + en_key_to_value = {"error_one": "1 Error"} + expanded = expand_plural_keys(keys, "pt", en_key_to_value) + assert "error_one" in expanded + assert "error_other" not in expanded + + def test_expand_plural_keys_expands_when_en_has_count_placeholder(self): + # Polish has 4 plural forms (_one, _few, _many, _other). Input has only _one and _other; + # expansion must add _few and _many so the test verifies the expansion logic ran. + keys = {"warning_one", "warning_other"} + en_key_to_value = {"warning_one": "1 Warning", "warning_other": "{{count}} Warnings"} + expanded = expand_plural_keys(keys, "pl", en_key_to_value) + assert "warning_one" in expanded + assert "warning_other" in expanded + assert "warning_few" in expanded + assert "warning_many" in expanded + class TestFlattenKeys: def test_flatten_simple_dict(self): @@ -112,7 +131,7 @@ def test_compare_keys_identical(self, tmp_path): assert "test.json" in summary assert summary["test.json"].missing_keys.get("de", []) == [] - assert summary["test.json"].extra_keys.get("de", []) == [] + assert summary["test.json"].unused_keys.get("de", []) == [] finally: ui_commands.LOCALES_DIR = original_locales_dir @@ -171,16 +190,50 @@ def test_compare_keys_with_extra(self, tmp_path): summary, missing_counts = compare_keys(locale_files) assert "test.json" in summary - assert "extra" in summary["test.json"].extra_keys.get("de", []) + assert "extra" in summary["test.json"].unused_keys.get("de", []) + finally: + ui_commands.LOCALES_DIR = original_locales_dir + + def test_compare_keys_optional_plural_unused_when_no_count(self, tmp_path): + """Plural variant of EN base with no {{count}} in EN is reported as unused.""" + en_dir = tmp_path / "en" + en_dir.mkdir() + de_dir = tmp_path / "de" + de_dir.mkdir() + + # EN has only error_one (no {{count}}), so error_other is never used at runtime + en_data = {"dagWarnings": {"error_one": "1 Error"}} + de_data = {"dagWarnings": {"error_one": "1 Fehler", "error_other": "{{count}} Fehler"}} + + (en_dir / "test.json").write_text(json.dumps(en_data)) + (de_dir / "test.json").write_text(json.dumps(de_data)) + + import airflow_breeze.commands.ui_commands as ui_commands + + original_locales_dir = ui_commands.LOCALES_DIR + ui_commands.LOCALES_DIR = tmp_path + + try: + locale_files = [ + LocaleFiles(locale="en", files=["test.json"]), + LocaleFiles(locale="de", files=["test.json"]), + ] + summary, _ = compare_keys(locale_files) + + # EN base has no {{count}}, so error_other is not required and is unused + assert "dagWarnings.error_other" in summary["test.json"].unused_keys.get("de", []) finally: ui_commands.LOCALES_DIR = original_locales_dir class TestLocaleSummary: def test_locale_summary_creation(self): - summary = LocaleSummary(missing_keys={"de": ["key1", "key2"]}, extra_keys={"de": ["key3"]}) + summary = LocaleSummary( + missing_keys={"de": ["key1", "key2"]}, + unused_keys={"de": ["key3"]}, + ) assert summary.missing_keys == {"de": ["key1", "key2"]} - assert summary.extra_keys == {"de": ["key3"]} + assert summary.unused_keys == {"de": ["key3"]} class TestLocaleFiles: @@ -255,7 +308,7 @@ def test_add_missing_translations(self, tmp_path): try: summary = LocaleSummary( missing_keys={"de": ["farewell"]}, - extra_keys={"de": []}, + unused_keys={"de": []}, ) add_missing_translations("de", {"test.json": summary}) @@ -267,9 +320,9 @@ def test_add_missing_translations(self, tmp_path): ui_commands.LOCALES_DIR = original_locales_dir -class TestRemoveExtraTranslations: - def test_remove_extra_translations(self, tmp_path): - from airflow_breeze.commands.ui_commands import remove_extra_translations +class TestRemoveUnusedTranslations: + def test_remove_unused_translations(self, tmp_path): + from airflow_breeze.commands.ui_commands import remove_unused_translations de_dir = tmp_path / "de" de_dir.mkdir() @@ -285,11 +338,11 @@ def test_remove_extra_translations(self, tmp_path): try: summary = LocaleSummary( missing_keys={"de": []}, - extra_keys={"de": ["extra"]}, + unused_keys={"de": ["extra"]}, ) - remove_extra_translations("de", {"test.json": summary}) + remove_unused_translations("de", {"test.json": summary}) - # Check that the extra key was removed + # Check that the unused key was removed de_data_updated = json.loads((de_dir / "test.json").read_text()) assert "extra" not in de_data_updated assert "greeting" in de_data_updated @@ -330,7 +383,7 @@ def test_natural_sort_matches_eslint(self, tmp_path): try: summary = LocaleSummary( missing_keys={"de": list(en_data.keys())}, - extra_keys={"de": []}, + unused_keys={"de": []}, ) add_missing_translations("de", {"test.json": summary}) From 5c0e72354d81811dca693c2582cb2b0bfb368776 Mon Sep 17 00:00:00 2001 From: Nitochkin <62333822+Crowiant@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:11:05 +0100 Subject: [PATCH 111/280] Add operators for Google BidManager API (#62521) Co-authored-by: Anton Nitochkin --- .../marketing_platform/bid_manager.rst | 119 ++++++ .../marketing_platform/display_video.rst | 1 - providers/google/provider.yaml | 15 + .../providers/google/get_provider_info.py | 21 ++ .../marketing_platform/hooks/bid_manager.py | 126 +++++++ .../operators/bid_manager.py | 347 ++++++++++++++++++ .../marketing_platform/sensors/bid_manager.py | 88 +++++ .../sensors/display_video.py | 2 +- .../marketing_platform/example_bid_manager.py | 214 +++++++++++ .../hooks/test_bid_manager.py | 145 ++++++++ .../operators/test_bid_manager.py | 257 +++++++++++++ .../sensors/test_bid_manager.py | 44 +++ 12 files changed, 1377 insertions(+), 2 deletions(-) create mode 100644 providers/google/docs/operators/marketing_platform/bid_manager.rst create mode 100644 providers/google/src/airflow/providers/google/marketing_platform/hooks/bid_manager.py create mode 100644 providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py create mode 100644 providers/google/src/airflow/providers/google/marketing_platform/sensors/bid_manager.py create mode 100644 providers/google/tests/system/google/marketing_platform/example_bid_manager.py create mode 100644 providers/google/tests/unit/google/marketing_platform/hooks/test_bid_manager.py create mode 100644 providers/google/tests/unit/google/marketing_platform/operators/test_bid_manager.py create mode 100644 providers/google/tests/unit/google/marketing_platform/sensors/test_bid_manager.py diff --git a/providers/google/docs/operators/marketing_platform/bid_manager.rst b/providers/google/docs/operators/marketing_platform/bid_manager.rst new file mode 100644 index 0000000000000..c29e2687ed521 --- /dev/null +++ b/providers/google/docs/operators/marketing_platform/bid_manager.rst @@ -0,0 +1,119 @@ + .. 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. + +Google Bid Manager API Operators +======================================= +`Google Bid Manager API `__ is a programmatic interface for the Display & Video 360 reporting feature. +It lets users build and run report queries, and download the resulting report file. + +Prerequisite Tasks +^^^^^^^^^^^^^^^^^^ + +.. include:: /operators/_partials/prerequisite_tasks.rst + +.. _howto/operator:GoogleBidManagerCreateQueryOperator: + +Creating a Query +^^^^^^^^^^^^^^^^ + +To create a query using Bid Manager, use +:class:`~airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerCreateQueryOperator`. + +.. exampleinclude:: /../../google/tests/system/google/marketing_platform/example_bid_manager.py + :language: python + :dedent: 4 + :start-after: [START howto_google_bid_manager_create_query_operator] + :end-before: [END howto_google_bid_manager_create_query_operator] + +Use :ref:`Jinja templating ` with +:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerCreateQueryOperator` +parameters which allow you to dynamically determine values. You can provide body definition using ``.json`` file +as this operator supports this template extension. +The result is saved to :ref:`XCom `, which allows the result to be used by other operators. + +.. _howto/operator:GoogleBidManagerRunQueryOperator: + +Run Query +^^^^^^^^^ + +To run a query using Bid Manager, use +:class:`~airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerRunQueryOperator`. + +.. exampleinclude:: /../../google/tests/system/google/marketing_platform/example_bid_manager.py + :language: python + :dedent: 4 + :start-after: [START howto_google_bid_manager_run_query_report_operator] + :end-before: [END howto_google_bid_manager_run_query_report_operator] + +You can use :ref:`Jinja templating ` with +:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerRunQueryOperator` +parameters which allow you to dynamically determine values. +The result is saved to :ref:`XCom `, which allows the result to be used by other operators. + +.. _howto/operator:GoogleBidManagerDeleteQueryOperator: + +Deleting a Query +^^^^^^^^^^^^^^^^ + +To delete a query using Bid Manager, use +:class:`~airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerDeleteQueryOperator`. + +.. exampleinclude:: /../../google/tests/system/google/marketing_platform/example_bid_manager.py + :language: python + :dedent: 4 + :start-after: [START howto_google_bid_manager_delete_query_operator] + :end-before: [END howto_google_bid_manager_delete_query_operator] + +You can use :ref:`Jinja templating ` with +:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerDeleteQueryOperator` +parameters which allow you to dynamically determine values. + +.. _howto/operator:GoogleBidManagerRunQuerySensor: + +Waiting for query +^^^^^^^^^^^^^^^^^ + +To wait for the report use +:class:`~airflow.providers.google.marketing_platform.sensors.bid_manager.GoogleBidManagerRunQuerySensor`. + +.. exampleinclude:: /../../google/tests/system/google/marketing_platform/example_bid_manager.py + :language: python + :dedent: 4 + :start-after: [START howto_google_bid_manager_wait_run_query_sensor] + :end-before: [END howto_google_bid_manager_wait_run_query_sensor] + +Use :ref:`Jinja templating ` with +:template-fields:`airflow.providers.google.marketing_platform.sensors.bid_manager.GoogleBidManagerRunQuerySensor` +parameters which allow you to dynamically determine values. + +.. _howto/operator:GoogleBidManagerDownloadReportOperator: + +Downloading a report +^^^^^^^^^^^^^^^^^^^^ + +To download a report to GCS bucket use +:class:`~airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerDownloadReportOperator`. + +.. exampleinclude:: /../../google/tests/system/google/marketing_platform/example_bid_manager.py + :language: python + :dedent: 4 + :start-after: [START howto_google_bid_manager_get_report_operator] + :end-before: [END howto_google_bid_manager_get_report_operator] + +Use :ref:`Jinja templating ` with +:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerDownloadReportOperator` +parameters which allow you to dynamically determine values. diff --git a/providers/google/docs/operators/marketing_platform/display_video.rst b/providers/google/docs/operators/marketing_platform/display_video.rst index 04cc9acbd04ac..7ce014303aba0 100644 --- a/providers/google/docs/operators/marketing_platform/display_video.rst +++ b/providers/google/docs/operators/marketing_platform/display_video.rst @@ -43,7 +43,6 @@ Use :ref:`Jinja templating ` with :template-fields:`airflow.providers.google.marketing_platform.operators.display_video.GoogleDisplayVideo360CreateSDFDownloadTaskOperator` parameters which allow you to dynamically determine values. - .. _howto/operator:GoogleDisplayVideo360SDFtoGCSOperator: Save SDF files in the Google Cloud Storage diff --git a/providers/google/provider.yaml b/providers/google/provider.yaml index c12c3e80d2993..04541d5569d92 100644 --- a/providers/google/provider.yaml +++ b/providers/google/provider.yaml @@ -464,6 +464,12 @@ integrations: how-to-guide: - /docs/apache-airflow-providers-google/operators/cloud/ray.rst tags: [gcp] + - integration-name: Google Bid Manager API + external-doc-url: https://developers.google.com/bid-manager + logo: /docs/integration-logos/Google-Search-Ads360.png + how-to-guide: + - /docs/apache-airflow-providers-google/operators/marketing_platform/bid_manager.rst + tags: [gmp] operators: - integration-name: Google Ads @@ -629,6 +635,9 @@ operators: - integration-name: Google Ray python-modules: - airflow.providers.google.cloud.operators.ray + - integration-name: Google Bid Manager API + python-modules: + - airflow.providers.google.marketing_platform.operators.bid_manager sensors: - integration-name: Google BigQuery @@ -694,6 +703,9 @@ sensors: - integration-name: Google Cloud Tasks python-modules: - airflow.providers.google.cloud.sensors.tasks + - integration-name: Google Bid Manager API + python-modules: + - airflow.providers.google.marketing_platform.sensors.bid_manager filesystems: - airflow.providers.google.cloud.fs.gcs @@ -913,6 +925,9 @@ hooks: - integration-name: Google Ray python-modules: - airflow.providers.google.cloud.hooks.ray + - integration-name: Google Bid Manager API + python-modules: + - airflow.providers.google.marketing_platform.hooks.bid_manager bundles: - integration-name: Google Cloud Storage (GCS) diff --git a/providers/google/src/airflow/providers/google/get_provider_info.py b/providers/google/src/airflow/providers/google/get_provider_info.py index cab93aa57c9e1..fec76eb9d5f3b 100644 --- a/providers/google/src/airflow/providers/google/get_provider_info.py +++ b/providers/google/src/airflow/providers/google/get_provider_info.py @@ -473,6 +473,15 @@ def get_provider_info(): "how-to-guide": ["/docs/apache-airflow-providers-google/operators/cloud/ray.rst"], "tags": ["gcp"], }, + { + "integration-name": "Google Bid Manager API", + "external-doc-url": "https://developers.google.com/bid-manager", + "logo": "/docs/integration-logos/Google-Search-Ads360.png", + "how-to-guide": [ + "/docs/apache-airflow-providers-google/operators/marketing_platform/bid_manager.rst" + ], + "tags": ["gmp"], + }, ], "operators": [ { @@ -694,6 +703,10 @@ def get_provider_info(): "integration-name": "Google Ray", "python-modules": ["airflow.providers.google.cloud.operators.ray"], }, + { + "integration-name": "Google Bid Manager API", + "python-modules": ["airflow.providers.google.marketing_platform.operators.bid_manager"], + }, ], "sensors": [ { @@ -780,6 +793,10 @@ def get_provider_info(): "integration-name": "Google Cloud Tasks", "python-modules": ["airflow.providers.google.cloud.sensors.tasks"], }, + { + "integration-name": "Google Bid Manager API", + "python-modules": ["airflow.providers.google.marketing_platform.sensors.bid_manager"], + }, ], "filesystems": ["airflow.providers.google.cloud.fs.gcs"], "asset-uris": [ @@ -1062,6 +1079,10 @@ def get_provider_info(): "integration-name": "Google Ray", "python-modules": ["airflow.providers.google.cloud.hooks.ray"], }, + { + "integration-name": "Google Bid Manager API", + "python-modules": ["airflow.providers.google.marketing_platform.hooks.bid_manager"], + }, ], "bundles": [ { diff --git a/providers/google/src/airflow/providers/google/marketing_platform/hooks/bid_manager.py b/providers/google/src/airflow/providers/google/marketing_platform/hooks/bid_manager.py new file mode 100644 index 0000000000000..4722a7720ae84 --- /dev/null +++ b/providers/google/src/airflow/providers/google/marketing_platform/hooks/bid_manager.py @@ -0,0 +1,126 @@ +# +# 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. +"""This module contains Google Bid Manager API hook.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +from googleapiclient.discovery import Resource, build + +from airflow.providers.google.common.hooks.base_google import GoogleBaseHook + + +class GoogleBidManagerHook(GoogleBaseHook): + """Hook for Google Bid Manager API.""" + + _conn: Resource | None = None + + def __init__( + self, + api_version: str = "v2", + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__( + gcp_conn_id=gcp_conn_id, + impersonation_chain=impersonation_chain, + **kwargs, + ) + self.api_version = api_version + + def get_conn(self) -> Resource: + """Retrieve connection to Bid Manager API.""" + if not self._conn: + http_authorized = self._authorize() + self._conn = build( + "doubleclickbidmanager", + self.api_version, + http=http_authorized, + cache_discovery=False, + ) + return self._conn + + def create_query(self, query: dict[str, Any]) -> dict: + """ + Create a query. + + :param query: Query object to be passed to request body. + """ + response = self.get_conn().queries().create(body=query).execute(num_retries=self.num_retries) + return response + + def delete_query(self, query_id: str) -> None: + """ + Delete a stored query as well as the associated stored reports. + + :param query_id: Query ID to delete. + """ + self.get_conn().queries().delete(queryId=query_id).execute(num_retries=self.num_retries) + + def get_query(self, query_id: str) -> dict: + """ + Retrieve a stored query. + + :param query_id: Query ID to retrieve. + """ + response = self.get_conn().queries().get(queryId=query_id).execute(num_retries=self.num_retries) + return response + + def list_queries(self) -> list[dict]: + """Retrieve stored queries.""" + response = self.get_conn().queries().list().execute(num_retries=self.num_retries) + return response.get("queries", []) + + def run_query(self, query_id: str, params: dict[str, Any] | None) -> dict: + """ + Run a stored query to generate a report. + + :param query_id: Query ID to run. + :param params: Parameters for the report. + """ + return ( + self.get_conn().queries().run(queryId=query_id, body=params).execute(num_retries=self.num_retries) + ) + + def get_report(self, query_id: str, report_id: str) -> dict: + """ + Retrieve a report. + + :param query_id: Query ID for which report was generated. + :param report_id: Report ID to retrieve. + """ + return ( + self.get_conn() + .queries() + .reports() + .get(queryId=query_id, reportId=report_id) + .execute(num_retries=self.num_retries) + ) + + def list_reports(self, query_id: str) -> dict: + """ + Retrieve a list of reports. + + :param query_id: Query ID for which report was generated. + """ + return ( + self.get_conn().queries().reports().list(queryId=query_id).execute(num_retries=self.num_retries) + ) diff --git a/providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py b/providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py new file mode 100644 index 0000000000000..2f4a43ea4b02d --- /dev/null +++ b/providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py @@ -0,0 +1,347 @@ +# +# 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. +"""This module contains operators for Bid Manager API part of the Google Display & Video 360.""" + +from __future__ import annotations + +import json +import shutil +import tempfile +import urllib.request +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any +from urllib.parse import urlsplit + +from airflow.exceptions import AirflowException +from airflow.providers.google.cloud.hooks.gcs import GCSHook +from airflow.providers.google.marketing_platform.hooks.bid_manager import GoogleBidManagerHook +from airflow.providers.google.version_compat import BaseOperator + +if TYPE_CHECKING: + from airflow.providers.common.compat.sdk import Context + + +class GoogleBidManagerCreateQueryOperator(BaseOperator): + """ + Creates a query. + + .. seealso:: + For more information on how to use this operator, take a look at the guide: + :ref:`howto/operator:GoogleBidManagerCreateQueryOperator` + + .. seealso:: + Check also the official API docs: + `https://developers.google.com/bid-manager/v2/queries/create` + + :param body: Report object passed to the request's body as described here: + https://developers.google.com/bid-manager/v2/queries#Query + :param api_version: The version of the api that will be requested for example 'v3'. + :param gcp_conn_id: The connection ID to use when fetching connection info. + :param impersonation_chain: Optional service account to impersonate using short-term + credentials, or chained list of accounts required to get the access_token + of the last account in the list, which will be impersonated in the request. + If set as a string, the account must grant the originating account + the Service Account Token Creator IAM role. + If set as a sequence, the identities from the list must grant + Service Account Token Creator IAM role to the directly preceding identity, with the first + account from the list granting this role to the originating account (templated). + """ + + template_fields: Sequence[str] = ( + "body", + "impersonation_chain", + ) + template_ext: Sequence[str] = (".json",) + + def __init__( + self, + *, + body: dict[str, Any], + api_version: str = "v2", + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.body = body + self.api_version = api_version + self.gcp_conn_id = gcp_conn_id + self.impersonation_chain = impersonation_chain + + def prepare_template(self) -> None: + # If .json is passed then we have to read the file + if isinstance(self.body, str) and self.body.endswith(".json"): + with open(self.body) as file: + self.body = json.load(file) + + def execute(self, context: Context) -> dict: + hook = GoogleBidManagerHook( + gcp_conn_id=self.gcp_conn_id, + api_version=self.api_version, + impersonation_chain=self.impersonation_chain, + ) + self.log.info("Creating Bid Manager API query.") + response = hook.create_query(query=self.body) + query_id = response["queryId"] + context["task_instance"].xcom_push(key="query_id", value=query_id) + self.log.info("Created query with ID: %s", query_id) + return response + + +class GoogleBidManagerRunQueryOperator(BaseOperator): + """ + Runs a stored query to generate a report. + + .. seealso:: + For more information on how to use this operator, take a look at the guide: + :ref:`howto/operator:GoogleBidManagerRunQueryOperator` + + .. seealso:: + Check also the official API docs: + `https://developers.google.com/bid-manager/v2/queries/run` + + :param query_id: Query ID to run. + :param parameters: Parameters for running a report as described here: + https://developers.google.com/bid-manager/v2/queries/run + :param api_version: The version of the api that will be requested for example 'v3'. + :param gcp_conn_id: The connection ID to use when fetching connection info. + :param impersonation_chain: Optional service account to impersonate using short-term + credentials, or chained list of accounts required to get the access_token + of the last account in the list, which will be impersonated in the request. + If set as a string, the account must grant the originating account + the Service Account Token Creator IAM role. + If set as a sequence, the identities from the list must grant + Service Account Token Creator IAM role to the directly preceding identity, with first + account from the list granting this role to the originating account (templated). + """ + + template_fields: Sequence[str] = ( + "query_id", + "parameters", + "impersonation_chain", + ) + + def __init__( + self, + *, + query_id: str, + parameters: dict[str, Any] | None = None, + api_version: str = "v2", + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.query_id = query_id + self.api_version = api_version + self.gcp_conn_id = gcp_conn_id + self.parameters = parameters + self.impersonation_chain = impersonation_chain + + def execute(self, context: Context) -> dict: + hook = GoogleBidManagerHook( + gcp_conn_id=self.gcp_conn_id, + api_version=self.api_version, + impersonation_chain=self.impersonation_chain, + ) + self.log.info( + "Running query %s with the following parameters:\n %s", + self.query_id, + self.parameters, + ) + response = hook.run_query(query_id=self.query_id, params=self.parameters) + context["task_instance"].xcom_push(key="query_id", value=response["key"]["queryId"]) + context["task_instance"].xcom_push(key="report_id", value=response["key"]["reportId"]) + return response + + +class GoogleBidManagerDeleteQueryOperator(BaseOperator): + """ + Deletes a stored query as well as the associated stored reports. + + .. seealso:: + For more information on how to use this operator, take a look at the guide: + :ref:`howto/operator:GoogleBidManagerDeleteQueryOperator` + + .. seealso:: + Check also the official API docs: + `https://developers.google.com/bid-manager/v2/queries/delete` + + :param query_id: Query ID to delete. + :param api_version: The version of the api that will be requested for example 'v3'. + :param gcp_conn_id: The connection ID to use when fetching connection info. + :param impersonation_chain: Optional service account to impersonate using short-term + credentials, or chained list of accounts required to get the access_token + of the last account in the list, which will be impersonated in the request. + If set as a string, the account must grant the originating account + the Service Account Token Creator IAM role. + If set as a sequence, the identities from the list must grant + Service Account Token Creator IAM role to the directly preceding identity, with first + account from the list granting this role to the originating account (templated). + """ + + template_fields: Sequence[str] = ( + "query_id", + "impersonation_chain", + ) + + def __init__( + self, + *, + query_id: str, + api_version: str = "v2", + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.api_version = api_version + self.gcp_conn_id = gcp_conn_id + self.impersonation_chain = impersonation_chain + self.query_id = query_id + + def execute(self, context: Context) -> None: + hook = GoogleBidManagerHook( + gcp_conn_id=self.gcp_conn_id, + api_version=self.api_version, + impersonation_chain=self.impersonation_chain, + ) + self.log.info("Deleting query with id: %s and all connected reports", self.query_id) + hook.delete_query(query_id=self.query_id) + self.log.info("Report deleted.") + + +class GoogleBidManagerDownloadReportOperator(BaseOperator): + """ + Retrieves a stored query. + + .. seealso:: + For more information on how to use this operator, take a look at the guide: + :ref:`howto/operator:GoogleBidManagerDownloadReportOperator` + + .. seealso:: + Check also the official API docs: + `https://developers.google.com/bid-manager/v2/queries/get` + + :param report_id: Report ID to retrieve. + :param query_id: Query ID for which report was generated.. + :param bucket_name: The bucket to upload to. + :param report_name: The report name to set when uploading the local file. + :param chunk_size: File will be downloaded in chunks of this many bytes. + :param gzip: Option to compress local file or file data for upload + :param api_version: The version of the api that will be requested for example 'v3'. + :param gcp_conn_id: The connection ID to use when fetching connection info. + :param impersonation_chain: Optional service account to impersonate using short-term + credentials, or chained list of accounts required to get the access_token + of the last account in the list, which will be impersonated in the request. + If set as a string, the account must grant the originating account + the Service Account Token Creator IAM role. + If set as a sequence, the identities from the list must grant + Service Account Token Creator IAM role to the directly preceding identity, with first + account from the list granting this role to the originating account (templated). + """ + + template_fields: Sequence[str] = ( + "query_id", + "report_id", + "bucket_name", + "report_name", + "impersonation_chain", + ) + + def __init__( + self, + *, + query_id: str, + report_id: str, + bucket_name: str, + report_name: str | None = None, + gzip: bool = True, + chunk_size: int = 10 * 1024 * 1024, + api_version: str = "v2", + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.query_id = query_id + self.report_id = report_id + self.chunk_size = chunk_size + self.gzip = gzip + self.bucket_name = bucket_name + self.report_name = report_name + self.api_version = api_version + self.gcp_conn_id = gcp_conn_id + self.impersonation_chain = impersonation_chain + + def _resolve_file_name(self, name: str) -> str: + new_name = name if name.endswith(".csv") else f"{name}.csv" + new_name = f"{new_name}.gz" if self.gzip else new_name + return new_name + + @staticmethod + def _set_bucket_name(name: str) -> str: + bucket = name if not name.startswith("gs://") else name[5:] + return bucket.strip("/") + + def execute(self, context: Context): + hook = GoogleBidManagerHook( + gcp_conn_id=self.gcp_conn_id, + api_version=self.api_version, + impersonation_chain=self.impersonation_chain, + ) + gcs_hook = GCSHook( + gcp_conn_id=self.gcp_conn_id, + impersonation_chain=self.impersonation_chain, + ) + + resource = hook.get_report(query_id=self.query_id, report_id=self.report_id) + status = resource.get("metadata", {}).get("status", {}).get("state") + if resource and status not in ["DONE", "FAILED"]: + raise AirflowException(f"Report {self.report_id} for query {self.query_id} is still running") + + # If no custom report_name provided, use Bid Manager name + file_url = resource["metadata"]["googleCloudStoragePath"] + if urllib.parse.urlparse(file_url).scheme == "file": + raise AirflowException("Accessing local file is not allowed in this operator") + report_name = self.report_name or urlsplit(file_url).path.split("/")[-1] + report_name = self._resolve_file_name(report_name) + + # Download the report + self.log.info("Starting downloading report %s", self.report_id) + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + with urllib.request.urlopen(file_url) as response: # nosec + shutil.copyfileobj(response, temp_file, length=self.chunk_size) + + temp_file.flush() + # Upload the local file to bucket + bucket_name = self._set_bucket_name(self.bucket_name) + gcs_hook.upload( + bucket_name=bucket_name, + object_name=report_name, + gzip=self.gzip, + filename=temp_file.name, + mime_type="text/csv", + ) + self.log.info( + "Report %s was saved in bucket %s as %s.", + self.report_id, + self.bucket_name, + report_name, + ) + context["task_instance"].xcom_push(key="report_name", value=report_name) diff --git a/providers/google/src/airflow/providers/google/marketing_platform/sensors/bid_manager.py b/providers/google/src/airflow/providers/google/marketing_platform/sensors/bid_manager.py new file mode 100644 index 0000000000000..75c806c6b622b --- /dev/null +++ b/providers/google/src/airflow/providers/google/marketing_platform/sensors/bid_manager.py @@ -0,0 +1,88 @@ +# 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. +"""Sensor for detecting the completion of DV360 Bid Manager reports.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +from airflow.providers.common.compat.sdk import BaseSensorOperator +from airflow.providers.google.marketing_platform.hooks.bid_manager import GoogleBidManagerHook + +if TYPE_CHECKING: + from airflow.providers.common.compat.sdk import Context + + +class GoogleBidManagerRunQuerySensor(BaseSensorOperator): + """ + Sensor for detecting the completion of DV360 Bid Manager reports for API v2. + + .. seealso:: + For more information on how to use this operator, take a look at the guide: + :ref:`howto/operator:GoogleBidManagerRunQuerySensor` + + :param query_id: Query ID for which report was generated + :param report_id: Report ID for which you want to wait + :param api_version: The version of the api that will be requested for example 'v3'. + :param gcp_conn_id: The connection ID to use when fetching connection info. + :param impersonation_chain: Optional service account to impersonate using short-term + credentials, or chained list of accounts required to get the access_token + of the last account in the list, which will be impersonated in the request. + If set as a string, the account must grant the originating account + the Service Account Token Creator IAM role. + If set as a sequence, the identities from the list must grant + Service Account Token Creator IAM role to the directly preceding identity, with first + account from the list granting this role to the originating account (templated). + """ + + template_fields: Sequence[str] = ( + "query_id", + "report_id", + "impersonation_chain", + ) + + def __init__( + self, + *, + query_id: str, + report_id: str, + api_version: str = "v2", + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.query_id = query_id + self.report_id = report_id + self.api_version = api_version + self.gcp_conn_id = gcp_conn_id + self.impersonation_chain = impersonation_chain + + def poke(self, context: Context) -> bool: + hook = GoogleBidManagerHook( + gcp_conn_id=self.gcp_conn_id, + api_version=self.api_version, + impersonation_chain=self.impersonation_chain, + ) + + response = hook.get_report(query_id=self.query_id, report_id=self.report_id) + status = response.get("metadata", {}).get("status", {}).get("state") + self.log.info("STATUS OF THE REPORT %s FOR QUERY %s: %s", self.report_id, self.query_id, status) + if response and status in ["DONE", "FAILED"]: + return True + return False diff --git a/providers/google/src/airflow/providers/google/marketing_platform/sensors/display_video.py b/providers/google/src/airflow/providers/google/marketing_platform/sensors/display_video.py index 433262c7698c9..83ab3df4f99e8 100644 --- a/providers/google/src/airflow/providers/google/marketing_platform/sensors/display_video.py +++ b/providers/google/src/airflow/providers/google/marketing_platform/sensors/display_video.py @@ -14,7 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -"""Sensor for detecting the completion of DV360 reports.""" +"""Sensor for detecting the completion of DV360 SDF operations.""" from __future__ import annotations diff --git a/providers/google/tests/system/google/marketing_platform/example_bid_manager.py b/providers/google/tests/system/google/marketing_platform/example_bid_manager.py new file mode 100644 index 0000000000000..9315258fa12d0 --- /dev/null +++ b/providers/google/tests/system/google/marketing_platform/example_bid_manager.py @@ -0,0 +1,214 @@ +# +# 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. +""" +Example Airflow DAG that shows how to use Bid Manager API from GoogleDisplayVideo360. +""" + +from __future__ import annotations + +import json +import os +from datetime import datetime +from typing import cast + +from airflow.models.dag import DAG +from airflow.models.xcom_arg import XComArg +from airflow.providers.google.cloud.operators.gcs import GCSCreateBucketOperator, GCSDeleteBucketOperator +from airflow.providers.google.marketing_platform.operators.bid_manager import ( + GoogleBidManagerCreateQueryOperator, + GoogleBidManagerDeleteQueryOperator, + GoogleBidManagerDownloadReportOperator, + GoogleBidManagerRunQueryOperator, +) +from airflow.providers.google.marketing_platform.sensors.bid_manager import ( + GoogleBidManagerRunQuerySensor, +) + +try: + from airflow.sdk import TriggerRule +except ImportError: + # Compatibility for Airflow < 3.1 + from airflow.utils.trigger_rule import TriggerRule # type: ignore[no-redef,attr-defined] +from google.cloud.exceptions import NotFound + +try: + from airflow.sdk import task +except ImportError: + # Airflow 2 path + from airflow.decorators import task # type: ignore[attr-defined,no-redef] +from airflow.providers.google.cloud.hooks.secret_manager import ( + GoogleCloudSecretManagerHook, +) + +from system.google.gcp_api_client_helpers import create_airflow_connection, delete_airflow_connection + +DAG_ID = "bid_manager" +ENV_ID = os.environ.get("SYSTEM_TESTS_ENV_ID", "default") +CONNECTION_TYPE = "google_cloud_platform" +CONN_ID = "google_display_video_default" +DISPLAY_VIDEO_SERVICE_ACCOUNT_KEY = "google_display_video_service_account_key" +IS_COMPOSER = bool(os.environ.get("COMPOSER_ENVIRONMENT", "")) + + +PROJECT_ID = os.environ.get("SYSTEM_TESTS_GCP_PROJECT", "default") +BUCKET_NAME = f"bucket_{DAG_ID}_{ENV_ID}" +ADVERTISER_ID = os.environ.get("GMP_ADVERTISER_ID", "1234567") + +REPORT = { + "metadata": { + "title": "Airflow Test Report", + "dataRange": {"range": "LAST_7_DAYS"}, + "format": "CSV", + "sendNotification": False, + }, + "params": { + "type": "STANDARD", + "groupBys": ["FILTER_DATE", "FILTER_PARTNER"], + "filters": [{"type": "FILTER_PARTNER", "value": ADVERTISER_ID}], + "metrics": ["METRIC_IMPRESSIONS", "METRIC_CLICKS"], + }, + "schedule": {"frequency": "ONE_TIME"}, +} + +PARAMETERS = { + "dataRange": {"range": "LAST_7_DAYS"}, +} + + +def get_secret(secret_id: str) -> str: + hook = GoogleCloudSecretManagerHook() + if hook.secret_exists(secret_id=secret_id): + return hook.access_secret(secret_id=secret_id).payload.data.decode() + raise NotFound(f"The secret {secret_id} not found") + + +with DAG( + DAG_ID, + start_date=datetime(2021, 1, 1), + catchup=False, + tags=["example", "bid_manager"], + schedule="@once", +) as dag: + + @task + def get_display_video_service_account_key(): + return get_secret(secret_id=DISPLAY_VIDEO_SERVICE_ACCOUNT_KEY) + + get_display_video_service_account_key_task = get_display_video_service_account_key() + + @task + def create_connection_display_video(connection_id: str, key) -> None: + conn_extra_json = json.dumps( + { + "keyfile_dict": key, + "project": PROJECT_ID, + "scope": "https://www.googleapis.com/auth/display-video, https://www.googleapis.com/auth/cloud-platform, https://www.googleapis.com/auth/doubleclickbidmanager", + } + ) + create_airflow_connection( + connection_id=connection_id, + connection_conf={"conn_type": CONNECTION_TYPE, "extra": conn_extra_json}, + is_composer=IS_COMPOSER, + ) + + create_connection_display_video_task = create_connection_display_video( + connection_id=CONN_ID, key=get_display_video_service_account_key_task + ) + + @task(task_id="delete_connection_task") + def delete_connection_display_video(connection_id: str) -> None: + delete_airflow_connection(connection_id=connection_id, is_composer=IS_COMPOSER) + + delete_connection_task = delete_connection_display_video(connection_id=CONN_ID) + + create_bucket = GCSCreateBucketOperator( + task_id="create_bucket", bucket_name=BUCKET_NAME, project_id=PROJECT_ID, gcp_conn_id=CONN_ID + ) + # [START howto_google_bid_manager_create_query_operator] + create_query = GoogleBidManagerCreateQueryOperator( + body=REPORT, task_id="create_query", gcp_conn_id=CONN_ID + ) + + query_id = cast("str", XComArg(create_query, key="query_id")) + # [END howto_google_bid_manager_create_query_operator] + + # [START howto_google_bid_manager_run_query_report_operator] + run_query = GoogleBidManagerRunQueryOperator( + query_id=query_id, parameters=PARAMETERS, task_id="run_report", gcp_conn_id=CONN_ID + ) + + query_id = cast("str", XComArg(run_query, key="query_id")) + report_id = cast("str", XComArg(run_query, key="report_id")) + # [END howto_google_bid_manager_run_query_report_operator] + + # [START howto_google_bid_manager_wait_run_query_sensor] + wait_for_query = GoogleBidManagerRunQuerySensor( + task_id="wait_for_query", + query_id=query_id, + report_id=report_id, + gcp_conn_id=CONN_ID, + ) + # [END howto_google_bid_manager_wait_run_query_sensor] + + # [START howto_google_bid_manager_get_report_operator] + get_report = GoogleBidManagerDownloadReportOperator( + query_id=query_id, + report_id=report_id, + task_id="get_report", + bucket_name=BUCKET_NAME, + report_name="test1.csv", + gcp_conn_id=CONN_ID, + ) + + delete_bucket = GCSDeleteBucketOperator( + task_id="delete_bucket", + bucket_name=BUCKET_NAME, + gcp_conn_id=CONN_ID, + trigger_rule=TriggerRule.ALL_DONE, + ) + # [END howto_google_bid_manager_get_report_operator] + + # [START howto_google_bid_manager_delete_query_operator] + delete_query = GoogleBidManagerDeleteQueryOperator( + query_id=query_id, task_id="delete_query", trigger_rule=TriggerRule.ALL_DONE, gcp_conn_id=CONN_ID + ) + # [END howto_google_bid_manager_delete_query_operator] + + ( + get_display_video_service_account_key_task + >> create_connection_display_video_task # type: ignore + >> create_bucket + >> create_query + >> run_query + >> wait_for_query + >> get_report + >> delete_query + >> delete_bucket + >> delete_connection_task + ) + + from tests_common.test_utils.watcher import watcher + + # This test needs watcher in order to properly mark success/failure + # when "tearDown" task with trigger rule is part of the DAG + list(dag.tasks) >> watcher() + +from tests_common.test_utils.system_tests import get_test_run # noqa: E402 + +# Needed to run the example DAG with pytest (see: tests/system/README.md#run_via_pytest) +test_run = get_test_run(dag) diff --git a/providers/google/tests/unit/google/marketing_platform/hooks/test_bid_manager.py b/providers/google/tests/unit/google/marketing_platform/hooks/test_bid_manager.py new file mode 100644 index 0000000000000..d3cd91419aa90 --- /dev/null +++ b/providers/google/tests/unit/google/marketing_platform/hooks/test_bid_manager.py @@ -0,0 +1,145 @@ +# +# 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 unittest import mock + +from airflow.providers.google.marketing_platform.hooks.bid_manager import GoogleBidManagerHook + +from unit.google.cloud.utils.base_gcp_mock import mock_base_gcp_hook_default_project_id + +API_VERSION = "v2" +GCP_CONN_ID = "google_cloud_default" + + +class TestGoogleBidManagerHook: + def setup_method(self): + with mock.patch( + "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.__init__", + new=mock_base_gcp_hook_default_project_id, + ): + self.hook = GoogleBidManagerHook(api_version=API_VERSION, gcp_conn_id=GCP_CONN_ID) + + @mock.patch( + "airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook._authorize" + ) + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.build") + def test_gen_conn(self, mock_build, mock_authorize): + result = self.hook.get_conn() + mock_build.assert_called_once_with( + "doubleclickbidmanager", + API_VERSION, + http=mock_authorize.return_value, + cache_discovery=False, + ) + assert mock_build.return_value == result + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_create_query(self, get_conn_mock): + body = {"body": "test"} + + return_value = "TEST" + get_conn_mock.return_value.queries.return_value.create.return_value.execute.return_value = ( + return_value + ) + result = self.hook.create_query(query=body) + + get_conn_mock.return_value.queries.return_value.create.assert_called_once_with(body=body) + + assert return_value == result + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_delete_query(self, get_conn_mock): + query_id = "QUERY_ID" + + return_value = "TEST" + get_conn_mock.return_value.queries.return_value.delete.return_value.execute.return_value = ( + return_value + ) + self.hook.delete_query(query_id=query_id) + + get_conn_mock.return_value.queries.return_value.delete.assert_called_once_with(queryId=query_id) + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_get_query(self, get_conn_mock): + query_id = "QUERY_ID" + + return_value = "TEST" + get_conn_mock.return_value.queries.return_value.get.return_value.execute.return_value = return_value + result = self.hook.get_query(query_id=query_id) + + get_conn_mock.return_value.queries.return_value.get.assert_called_once_with(queryId=query_id) + + assert return_value == result + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_list_queries(self, get_conn_mock): + queries = ["test"] + return_value = {"queries": queries} + get_conn_mock.return_value.queries.return_value.list.return_value.execute.return_value = return_value + result = self.hook.list_queries() + + get_conn_mock.return_value.queries.return_value.list.assert_called_once_with() + + assert queries == result + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_run_query(self, get_conn_mock): + query_id = "QUERY_ID" + params = {"params": "test"} + return_value = "TEST" + get_conn_mock.return_value.queries.return_value.run.return_value.execute.return_value = return_value + + result = self.hook.run_query(query_id=query_id, params=params) + + get_conn_mock.return_value.queries.return_value.run.assert_called_once_with( + queryId=query_id, body=params + ) + assert return_value == result + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_get_report(self, get_conn_mock): + query_id = "QUERY_ID" + report_id = "REPORT_ID" + return_value = "TEST_REPORT" + ( + get_conn_mock.return_value.queries.return_value.reports.return_value.get.return_value.execute.return_value + ) = return_value + + result = self.hook.get_report(query_id=query_id, report_id=report_id) + + get_conn_mock.return_value.queries.return_value.reports.return_value.get.assert_called_once_with( + queryId=query_id, reportId=report_id + ) + assert return_value == result + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_list_reports(self, get_conn_mock): + query_id = "QUERY_ID" + reports = ["report1", "report2"] + return_value = {"reports": reports} + ( + get_conn_mock.return_value.queries.return_value.reports.return_value.list.return_value.execute.return_value + ) = return_value + + result = self.hook.list_reports(query_id=query_id) + + get_conn_mock.return_value.queries.return_value.reports.return_value.list.assert_called_once_with( + queryId=query_id + ) + assert reports == result["reports"] diff --git a/providers/google/tests/unit/google/marketing_platform/operators/test_bid_manager.py b/providers/google/tests/unit/google/marketing_platform/operators/test_bid_manager.py new file mode 100644 index 0000000000000..20c323e57c00d --- /dev/null +++ b/providers/google/tests/unit/google/marketing_platform/operators/test_bid_manager.py @@ -0,0 +1,257 @@ +# +# 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 +from tempfile import NamedTemporaryFile +from unittest import mock + +import pytest +from sqlalchemy import delete + +from airflow.exceptions import AirflowException +from airflow.models import TaskInstance as TI +from airflow.providers.google.marketing_platform.operators.bid_manager import ( + GoogleBidManagerCreateQueryOperator, + GoogleBidManagerDeleteQueryOperator, + GoogleBidManagerDownloadReportOperator, + GoogleBidManagerRunQueryOperator, +) +from airflow.utils import timezone +from airflow.utils.session import create_session + +from tests_common.test_utils.version_compat import AIRFLOW_V_3_0_PLUS + +API_VERSION = "v2" +GCP_CONN_ID = "google_cloud_default" +IMPERSONATION_CHAIN = ["ACCOUNT_1", "ACCOUNT_2", "ACCOUNT_3"] + +DEFAULT_DATE = timezone.datetime(2021, 1, 1) +REPORT_ID = "report_id" +BUCKET_NAME = "test_bucket" +REPORT_NAME = "test_report.csv" +QUERY_ID = FILENAME = "test.csv" + + +class TestGoogleBidManagerDeleteQueryOperator: + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook") + def test_execute(self, hook_mock): + op = GoogleBidManagerDeleteQueryOperator( + query_id=QUERY_ID, api_version=API_VERSION, task_id="test_task" + ) + op.execute(context=None) + hook_mock.assert_called_once_with( + gcp_conn_id=GCP_CONN_ID, + api_version=API_VERSION, + impersonation_chain=None, + ) + hook_mock.return_value.delete_query.assert_called_once_with(query_id=QUERY_ID) + + +@pytest.mark.db_test +class TestGoogleBidManagerDownloadReportOperator: + def setup_method(self): + with create_session() as session: + session.execute(delete(TI)) + + def teardown_method(self): + with create_session() as session: + session.execute(delete(TI)) + + @pytest.mark.parametrize( + ("file_path", "should_except"), [("https://host/path", False), ("file:/path/to/file", True)] + ) + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.shutil") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.urllib.request") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.tempfile") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GCSHook") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook") + def test_execute( + self, + mock_hook, + mock_gcs_hook, + mock_temp, + mock_request, + mock_shutil, + file_path, + should_except, + ): + mock_temp.NamedTemporaryFile.return_value.__enter__.return_value.name = FILENAME + mock_hook.return_value.get_report.return_value = { + "metadata": { + "status": {"state": "DONE"}, + "googleCloudStoragePath": file_path, + } + } + # Create mock context with task_instance + mock_context = {"task_instance": mock.Mock()} + + op = GoogleBidManagerDownloadReportOperator( + query_id=QUERY_ID, + report_id=REPORT_ID, + bucket_name=BUCKET_NAME, + report_name=REPORT_NAME, + task_id="test_task", + ) + if should_except: + with pytest.raises(AirflowException): + op.execute(context=mock_context) + return + op.execute(context=mock_context) + mock_hook.assert_called_once_with( + gcp_conn_id=GCP_CONN_ID, + api_version="v2", + impersonation_chain=None, + ) + mock_hook.return_value.get_report.assert_called_once_with(report_id=REPORT_ID, query_id=QUERY_ID) + + mock_gcs_hook.assert_called_once_with( + gcp_conn_id=GCP_CONN_ID, + impersonation_chain=None, + ) + mock_gcs_hook.return_value.upload.assert_called_once_with( + bucket_name=BUCKET_NAME, + filename=FILENAME, + gzip=True, + mime_type="text/csv", + object_name=REPORT_NAME + ".gz", + ) + mock_context["task_instance"].xcom_push.assert_called_once_with( + key="report_name", value=REPORT_NAME + ".gz" + ) + + @pytest.mark.parametrize( + "test_bucket_name", + [BUCKET_NAME, f"gs://{BUCKET_NAME}", "XComArg", "{{ ti.xcom_pull(task_ids='taskflow_op') }}"], + ) + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.shutil") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.urllib.request") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.tempfile") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GCSHook") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook") + def test_set_bucket_name( + self, + mock_hook, + mock_gcs_hook, + mock_temp, + mock_request, + mock_shutil, + test_bucket_name, + dag_maker, + ): + mock_temp.NamedTemporaryFile.return_value.__enter__.return_value.name = FILENAME + mock_hook.return_value.get_report.return_value = { + "metadata": {"status": {"state": "DONE"}, "googleCloudStoragePath": "TEST"} + } + with dag_maker(dag_id="test_set_bucket_name", start_date=DEFAULT_DATE) as dag: + if BUCKET_NAME not in test_bucket_name: + + @dag.task(task_id="taskflow_op") + def f(): + return BUCKET_NAME + + taskflow_op = f() + + op = GoogleBidManagerDownloadReportOperator( + query_id=QUERY_ID, + report_id=REPORT_ID, + bucket_name=test_bucket_name if test_bucket_name != "XComArg" else taskflow_op, + report_name=REPORT_NAME, + task_id="test_task", + ) + + if test_bucket_name == "{{ ti.xcom_pull(task_ids='taskflow_op') }}": + taskflow_op >> op + + if AIRFLOW_V_3_0_PLUS: + dag.test() + else: + dr = dag_maker.create_dagrun() + for ti in dr.get_task_instances(): + ti.run() + + mock_gcs_hook.return_value.upload.assert_called_once_with( + bucket_name=BUCKET_NAME, + filename=FILENAME, + gzip=True, + mime_type="text/csv", + object_name=REPORT_NAME + ".gz", + ) + + +class TestGoogleBidManagerRunQueryOperator: + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook") + def test_execute(self, hook_mock): + parameters = {"param": "test"} + + # Create mock context with task_instance + mock_context = {"task_instance": mock.Mock()} + + hook_mock.return_value.run_query.return_value = { + "key": { + "queryId": QUERY_ID, + "reportId": REPORT_ID, + } + } + op = GoogleBidManagerRunQueryOperator( + query_id=QUERY_ID, + parameters=parameters, + api_version=API_VERSION, + task_id="test_task", + ) + op.execute(context=mock_context) + hook_mock.assert_called_once_with( + gcp_conn_id=GCP_CONN_ID, + api_version=API_VERSION, + impersonation_chain=None, + ) + + mock_context["task_instance"].xcom_push.assert_any_call(key="query_id", value=QUERY_ID) + mock_context["task_instance"].xcom_push.assert_any_call(key="report_id", value=REPORT_ID) + hook_mock.return_value.run_query.assert_called_once_with(query_id=QUERY_ID, params=parameters) + + +class TestGoogleBidManagerCreateQueryOperator: + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook") + def test_execute(self, hook_mock): + body = {"body": "test"} + + # Create mock context with task_instance + mock_context = {"task_instance": mock.Mock()} + + hook_mock.return_value.create_query.return_value = {"queryId": QUERY_ID} + op = GoogleBidManagerCreateQueryOperator(body=body, task_id="test_task") + op.execute(context=mock_context) + hook_mock.assert_called_once_with( + gcp_conn_id=GCP_CONN_ID, + api_version="v2", + impersonation_chain=None, + ) + hook_mock.return_value.create_query.assert_called_once_with(query=body) + mock_context["task_instance"].xcom_push.assert_called_once_with(key="query_id", value=QUERY_ID) + + def test_prepare_template(self): + body = {"key": "value"} + with NamedTemporaryFile("w+", suffix=".json") as f: + f.write(json.dumps(body)) + f.flush() + op = GoogleBidManagerCreateQueryOperator(body=body, task_id="test_task") + op.prepare_template() + + assert isinstance(op.body, dict) + assert op.body == body diff --git a/providers/google/tests/unit/google/marketing_platform/sensors/test_bid_manager.py b/providers/google/tests/unit/google/marketing_platform/sensors/test_bid_manager.py new file mode 100644 index 0000000000000..f48810e60dea7 --- /dev/null +++ b/providers/google/tests/unit/google/marketing_platform/sensors/test_bid_manager.py @@ -0,0 +1,44 @@ +# +# 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 unittest import mock + +from airflow.providers.google.marketing_platform.sensors.bid_manager import ( + GoogleBidManagerRunQuerySensor, +) + +MODULE_NAME = "airflow.providers.google.marketing_platform.sensors.bid_manager" + +GCP_CONN_ID = "google_cloud_default" + + +class TestGoogleBidManagerRunQuerySensor: + @mock.patch(f"{MODULE_NAME}.GoogleBidManagerHook") + @mock.patch(f"{MODULE_NAME}.BaseSensorOperator") + def test_poke(self, mock_base_op, hook_mock): + query_id = "QUERY_ID" + report_id = "REPORT_ID" + op = GoogleBidManagerRunQuerySensor(query_id=query_id, report_id=report_id, task_id="test_task") + op.poke(context=None) + hook_mock.assert_called_once_with( + gcp_conn_id=GCP_CONN_ID, + api_version="v2", + impersonation_chain=None, + ) + hook_mock.return_value.get_report.assert_called_once_with(query_id=query_id, report_id=report_id) From 7cffced52b043bd98969a330cc83678e69fcb374 Mon Sep 17 00:00:00 2001 From: SameerMesiah97 <75502260+SameerMesiah97@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:34:12 +0000 Subject: [PATCH 112/280] Handle concurrent create race in DatabricksReposCreateOperator when ignore_existing_repo=True. If create_repo() fails, the operator now re-checks repository existence and proceeds if the repository was created concurrently; otherwise, the original exception is re-raised. Add unit tests covering recovery and failure propagation under concurrent create scenarios. (#62422) Co-authored-by: Sameer Mesiah --- .../databricks/operators/databricks_repos.py | 29 ++++++++- .../operators/test_databricks_repos.py | 62 +++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py b/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py index d77a9dc640dfd..cb513ea8873ba 100644 --- a/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py +++ b/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py @@ -142,15 +142,40 @@ def execute(self, context: Context): ) payload["path"] = self.repo_path existing_repo_id = None + if self.repo_path is not None: existing_repo_id = self._hook.get_repo_by_path(self.repo_path) if existing_repo_id is not None and not self.ignore_existing_repo: raise AirflowException(f"Repo with path '{self.repo_path}' already exists") + if existing_repo_id is None: - result = self._hook.create_repo(payload) - repo_id = result["id"] + try: + result = self._hook.create_repo(payload) + repo_id = result["id"] + except Exception: + # If ignore_existing_repo is False, preserve existing behavior + # and propagate the original create failure. + if not self.ignore_existing_repo: + raise + + # When ignore_existing_repo=True, attempt to recover from a possible + # create-time conflict (e.g., repo created concurrently). + if self.repo_path is not None: + repo_id = self._hook.get_repo_by_path(self.repo_path) + + # Only treat this as success if the repo now exists. + # If it still does not exist, re-raise the original exception + # to avoid masking genuine create failures. + if repo_id is None: + raise + + self.log.info( + "Repository at path '%s' already exists; continuing because ignore_existing_repo=True.", + self.repo_path, + ) else: repo_id = existing_repo_id + # update repo if necessary if self.branch is not None: self._hook.update_repo(str(repo_id), {"branch": str(self.branch)}) diff --git a/providers/databricks/tests/unit/databricks/operators/test_databricks_repos.py b/providers/databricks/tests/unit/databricks/operators/test_databricks_repos.py index dea5155cd0d1d..398050cf1ff8f 100644 --- a/providers/databricks/tests/unit/databricks/operators/test_databricks_repos.py +++ b/providers/databricks/tests/unit/databricks/operators/test_databricks_repos.py @@ -210,6 +210,68 @@ def test_create_ignore_existing_plus_checkout(self, db_mock_class): db_mock.get_repo_by_path.assert_called_once_with(repo_path) db_mock.update_repo.assert_called_once_with("123", {"branch": "releases"}) + @mock.patch("airflow.providers.databricks.operators.databricks_repos.DatabricksHook") + def test_create_conflict_recovered_when_ignore_flag_set(self, db_mock_class): + """ + Test that create-time conflict is recovered when ignore_existing_repo=True. + """ + git_url = "https://github.com/test/test" + repo_path = "/Repos/Project1/test-repo" + + op = DatabricksReposCreateOperator( + task_id=TASK_ID, + git_url=git_url, + repo_path=repo_path, + ignore_existing_repo=True, + ) + + db_mock = db_mock_class.return_value + + # After first existence check, repo is not found. + # After second existence check, repo exists (created concurrently). + db_mock.get_repo_by_path.side_effect = [None, "123"] + + db_mock.create_repo.side_effect = Exception("Conflict") + + result = op.execute(None) + + db_mock_class.assert_called_once_with( + DEFAULT_CONN_ID, + retry_limit=op.databricks_retry_limit, + retry_delay=op.databricks_retry_delay, + caller="DatabricksReposCreateOperator", + ) + + db_mock.create_repo.assert_called_once_with({"url": git_url, "provider": "gitHub", "path": repo_path}) + + assert result == "123" + + @mock.patch("airflow.providers.databricks.operators.databricks_repos.DatabricksHook") + def test_create_conflict_not_recovered_when_repo_still_missing(self, db_mock_class): + """ + Test that create failure is re-raised if repo does not exist after failure. + """ + git_url = "https://github.com/test/test" + repo_path = "/Repos/Project1/test-repo" + + op = DatabricksReposCreateOperator( + task_id=TASK_ID, + git_url=git_url, + repo_path=repo_path, + ignore_existing_repo=True, + ) + + db_mock = db_mock_class.return_value + + # Repo not found before or after create failure. + db_mock.get_repo_by_path.side_effect = [None, None] + db_mock.create_repo.side_effect = Exception("Create failure") + + with pytest.raises(Exception, match="Create failure"): + op.execute(None) + + db_mock.create_repo.assert_called_once() + def test_init_exception(self): """ Tests handling of incorrect parameters passed to ``__init__`` From 425b750ea24ede5cfbfafe6854fb7ec51010d947 Mon Sep 17 00:00:00 2001 From: SameerMesiah97 <75502260+SameerMesiah97@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:35:13 +0000 Subject: [PATCH 113/280] Refactor timeout handling in DatabricksSqlHook to use explicit signaling (#62623) Replace implicit timeout detection based on Timer.is_alive() with explicit timeout signaling via threading.Event. Timeout classification now checks an explicit signal set by the timeout callback instead of inferring state from thread lifecycle behavior. Preserves existing cancellation semantics and exception types. Unit tests have been adjusted accordingly. Co-authored-by: Sameer Mesiah --- .../databricks/hooks/databricks_sql.py | 48 ++++++++++++------- .../databricks/hooks/test_databricks_sql.py | 39 +++++++++------ 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/providers/databricks/src/airflow/providers/databricks/hooks/databricks_sql.py b/providers/databricks/src/airflow/providers/databricks/hooks/databricks_sql.py index 127f6b71c708a..2c2164bd9c78b 100644 --- a/providers/databricks/src/airflow/providers/databricks/hooks/databricks_sql.py +++ b/providers/databricks/src/airflow/providers/databricks/hooks/databricks_sql.py @@ -52,14 +52,23 @@ T = TypeVar("T") -def create_timeout_thread(cur, execution_timeout: timedelta | None) -> threading.Timer | None: - if execution_timeout is not None: - seconds_to_timeout = execution_timeout.total_seconds() - t = threading.Timer(seconds_to_timeout, cur.connection.cancel) - else: - t = None +def create_timeout_thread( + cur, execution_timeout: timedelta | None +) -> tuple[threading.Timer | None, threading.Event | None]: + """Create a timeout timer that cancels the connection and sets a timeout flag.""" + if not execution_timeout: + return None, None - return t + timeout_event = threading.Event() + + def _cancel(): + timeout_event.set() + cur.connection.cancel() + + timer = threading.Timer(execution_timeout.total_seconds(), _cancel) + timer.start() + + return timer, timeout_event class DatabricksSqlHook(BaseDatabricksHook, DbApiHook): @@ -290,22 +299,25 @@ def run( self.set_autocommit(conn, autocommit) with closing(conn.cursor()) as cur: - t = create_timeout_thread(cur, execution_timeout) + timer, timeout_event = create_timeout_thread(cur, execution_timeout) - # TODO: adjust this to make testing easier try: self._run_command(cur, sql_statement, parameters) + except Exception as e: - if t is None or t.is_alive(): - raise DatabricksSqlExecutionError( - f"Error running SQL statement: {sql_statement}. {str(e)}" - ) - raise DatabricksSqlExecutionTimeout( - f"Timeout threshold exceeded for SQL statement: {sql_statement} was cancelled." - ) + if timeout_event and timeout_event.is_set(): + raise DatabricksSqlExecutionTimeout( + f"Timeout threshold exceeded for SQL statement: " + f"{sql_statement} was cancelled." + ) from e + + raise DatabricksSqlExecutionError( + f"Error running SQL statement: {sql_statement}. {str(e)}" + ) from e + finally: - if t is not None: - t.cancel() + if timer: + timer.cancel() if query_id := cur.query_id: self.log.info("Databricks query id: %s", query_id) diff --git a/providers/databricks/tests/unit/databricks/hooks/test_databricks_sql.py b/providers/databricks/tests/unit/databricks/hooks/test_databricks_sql.py index 98ea7e1d34779..d661f5b0714ec 100644 --- a/providers/databricks/tests/unit/databricks/hooks/test_databricks_sql.py +++ b/providers/databricks/tests/unit/databricks/hooks/test_databricks_sql.py @@ -18,7 +18,6 @@ # from __future__ import annotations -import threading from collections import namedtuple from datetime import timedelta from unittest import mock @@ -509,8 +508,12 @@ def test_execution_timeout_exceeded( description=get_cursor_descriptions(cursor_descriptions), ) - # Simulate a timeout - mock_create_timeout_thread.return_value = threading.Timer(cur, execution_timeout) + mock_event = mock.MagicMock() + mock_event.is_set.return_value = True # simulate timeout + + mock_timer = mock.MagicMock() + + mock_create_timeout_thread.return_value = (mock_timer, mock_event) mock_run_command.side_effect = Exception("Mocked exception") @@ -532,20 +535,22 @@ def test_execution_timeout_exceeded( "cursor_descriptions", [(("id", "value"),)], ) -def test_create_timeout_thread( - mock_get_conn, - mock_get_requests, - mock_timer, - cursor_descriptions, -): +def test_create_timeout_thread(mock_get_conn, mock_get_requests, cursor_descriptions): + cur = mock.MagicMock( rowcount=1, description=get_cursor_descriptions(cursor_descriptions), ) + timeout = timedelta(seconds=1) - thread = create_timeout_thread(cur=cur, execution_timeout=timeout) - mock_timer.assert_called_once_with(timeout.total_seconds(), cur.connection.cancel) - assert thread is not None + + timer, event = create_timeout_thread(cur=cur, execution_timeout=timeout) + + assert timer is not None + assert event is not None + assert not event.is_set() + + timer.cancel() @pytest.mark.parametrize( @@ -562,9 +567,15 @@ def test_create_timeout_thread_no_timeout( rowcount=1, description=get_cursor_descriptions(cursor_descriptions), ) - thread = create_timeout_thread(cur=cur, execution_timeout=None) + + timer, timeout_event = create_timeout_thread( + cur=cur, + execution_timeout=None, + ) + mock_timer.assert_not_called() - assert thread is None + assert timer is None + assert timeout_event is None def test_get_openlineage_default_schema_with_no_schema_set(): From 70391adcb5dcc83ed9184c9004bf90954919919d Mon Sep 17 00:00:00 2001 From: ANKIT KUMAR Date: Wed, 11 Mar 2026 00:09:06 +0530 Subject: [PATCH 114/280] Fix Breeze unit tests for milestone tagging (#63031) The GITHUB_REPOSITORY environment variable is set in the CI pipeline to the repository fork (e.g. Ironankit525/airflow). This overrides the default value of 'apache/airflow' in the option_github_repository of the Breeze CLI. This causes the test assertions to fail when they expect the comment URL to use 'apache/airflow' but it actually uses the fork repository. This commit explicitly passes '--github-repository apache/airflow' in the test arguments to ensure they are unaffected by this environment variable. --- dev/breeze/tests/test_set_milestone.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev/breeze/tests/test_set_milestone.py b/dev/breeze/tests/test_set_milestone.py index e7c1c29153404..78aa8f19325f6 100644 --- a/dev/breeze/tests/test_set_milestone.py +++ b/dev/breeze/tests/test_set_milestone.py @@ -446,6 +446,8 @@ def test_find_milestone_should_set_and_comment( "testuser", "--github-token", "fake-token", + "--github-repository", + "apache/airflow", ], ) @@ -550,6 +552,8 @@ def test_not_find_milestone_should_comment_warning( "testuser", "--github-token", "fake-token", + "--github-repository", + "apache/airflow", ], ) From c9eb8e5d3e79b8446cf8c76e8e96d50a008b5764 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:55:18 -0400 Subject: [PATCH 115/280] chore(deps-dev): bump babel-loader (#63283) Bumps the fab-ui-package-updates group with 1 update in the /providers/fab/src/airflow/providers/fab/www directory: [babel-loader](https://github.com/babel/babel-loader). Updates `babel-loader` from 10.0.0 to 10.1.0 - [Release notes](https://github.com/babel/babel-loader/releases) - [Changelog](https://github.com/babel/babel-loader/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel-loader/compare/v10.0.0...v10.1.0) --- updated-dependencies: - dependency-name: babel-loader dependency-version: 10.1.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: fab-ui-package-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../airflow/providers/fab/www/package.json | 2 +- .../airflow/providers/fab/www/pnpm-lock.yaml | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/providers/fab/src/airflow/providers/fab/www/package.json b/providers/fab/src/airflow/providers/fab/www/package.json index b3106458723b7..3ea9e9319137e 100644 --- a/providers/fab/src/airflow/providers/fab/www/package.json +++ b/providers/fab/src/airflow/providers/fab/www/package.json @@ -43,7 +43,7 @@ "@babel/eslint-parser": "^7.28.6", "@babel/plugin-transform-runtime": "^7.29.0", "@babel/preset-env": "^7.29.0", - "babel-loader": "^10.0.0", + "babel-loader": "^10.1.0", "copy-webpack-plugin": "^14.0.0", "css-loader": "7.1.4", "css-minimizer-webpack-plugin": "^8.0.0", diff --git a/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml b/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml index 8bfe6c3596fa5..d40f3100cef54 100644 --- a/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml +++ b/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml @@ -34,8 +34,8 @@ importers: specifier: ^7.29.0 version: 7.29.0(@babel/core@7.29.0) babel-loader: - specifier: ^10.0.0 - version: 10.0.0(@babel/core@7.29.0)(webpack@5.105.4) + specifier: ^10.1.0 + version: 10.1.0(@babel/core@7.29.0)(webpack@5.105.4) copy-webpack-plugin: specifier: ^14.0.0 version: 14.0.0(webpack@5.105.4) @@ -919,12 +919,18 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} - babel-loader@10.0.0: - resolution: {integrity: sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==} + babel-loader@10.1.0: + resolution: {integrity: sha512-5HTUZa013O4SWEYlJDHexrqSIYkWatfA9w/ZZQa7V2nMc0dRWkfu/0pmioC7XMYm8M7Z/3+q42NWj6e+fAT0MQ==} engines: {node: ^18.20.0 || ^20.10.0 || >=22.0.0} peerDependencies: - '@babel/core': ^7.12.0 + '@babel/core': ^7.12.0 || ^8.0.0-beta.1 + '@rspack/core': ^1.0.0 || ^2.0.0-0 webpack: '>=5.61.0' + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true babel-plugin-polyfill-corejs2@0.4.15: resolution: {integrity: sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==} @@ -3329,10 +3335,11 @@ snapshots: astral-regex@2.0.0: {} - babel-loader@10.0.0(@babel/core@7.29.0)(webpack@5.105.4): + babel-loader@10.1.0(@babel/core@7.29.0)(webpack@5.105.4): dependencies: '@babel/core': 7.29.0 find-up: 5.0.0 + optionalDependencies: webpack: 5.105.4(webpack-cli@6.0.1) babel-plugin-polyfill-corejs2@0.4.15(@babel/core@7.29.0): From 5d9eed3c664eef1b6ee2f2576266f4c0e862e45e Mon Sep 17 00:00:00 2001 From: Aritra Basu <24430013+aritra24@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:28:28 +0530 Subject: [PATCH 116/280] Updates exception to hide sql statements on constraint failure (#63028) * Updates exception to hide sql statements on constraint failure The exception handler now hides the sql statement when the expose stacktrace flag is false. * Fixes failing UT * Adds ignore type on pop from dict Since pytest returns a starlette HTTPException it annotates details as a string, we make use of FastApi's HTTPException which returns details as a dict. Due to this mypy get's confused about some of the operations we're performing on a dict. * Cleaned up UTs to remove generate_test_cases_parametrize --- .../airflow/api_fastapi/common/exceptions.py | 10 +- .../api_fastapi/common/test_exceptions.py | 119 ++++++++++++++++-- 2 files changed, 113 insertions(+), 16 deletions(-) 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/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 From c2992048a0e7ef8645a728c71b6446404639eac2 Mon Sep 17 00:00:00 2001 From: Daniel Standish <15932138+dstandish@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:12:42 -0700 Subject: [PATCH 117/280] Simplify approach for creating dag run and task spans (#62554) Previously we had a very high touch way of creating long-running spans for dag runs and tasks and keeping them alive across scheduler handoffs etc. We figured out a simpler way to do that. Additionally, we determined that there was really no need to use the custom airflow tracing interfaces and resolved instead to do OTEL in a way where we can just use the OTEL interfaces directly. We pushed the task span creation down to the tasks, so that task spans run in the task process. Replace the custom base tracer class and scattered OTEL logic with a focused helper module in `airflow/observability/traces/`. Remove the large blocks of tracing code from `scheduler_job_runner.py` and `dagrun.py` and consolidate span creation for dag runs and task execution into clean, reusable functions. Add configurable flush timeout and improve span naming. --- .../src/airflow/config_templates/config.yml | 12 + .../src/airflow/executors/base_executor.py | 44 --- .../src/airflow/executors/workloads/task.py | 2 +- .../src/airflow/jobs/scheduler_job_runner.py | 262 ---------------- .../src/airflow/jobs/triggerer_job_runner.py | 10 - airflow-core/src/airflow/models/dagrun.py | 174 ++-------- .../airflow/observability/traces/__init__.py | 134 ++++++++ airflow-core/src/airflow/settings.py | 4 +- .../integration/otel/dags/otel_test_dag.py | 57 +--- .../otel_test_dag_with_pause_between_tasks.py | 158 ---------- .../dags/otel_test_dag_with_pause_in_task.py | 151 --------- .../tests/integration/otel/test_otel.py | 20 +- .../tests/unit/jobs/test_scheduler_job.py | 187 ----------- airflow-core/tests/unit/models/test_dagrun.py | 296 ++++++++++-------- docs/spelling_wordlist.txt | 1 + .../ci/docker-compose/integration-otel.yml | 2 +- task-sdk/src/airflow/sdk/definitions/dag.py | 4 - .../airflow/sdk/execution_time/task_runner.py | 119 ++++--- .../execution_time/test_task_runner.py | 117 ++++++- 19 files changed, 549 insertions(+), 1205 deletions(-) delete mode 100644 airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_between_tasks.py delete mode 100644 airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_in_task.py diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml index 8558a71236eb7..2d70e625c1544 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -1414,9 +1414,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: 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/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 2d58f295c6ecc..c667631756edc 100644 --- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py +++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py @@ -32,7 +32,6 @@ from functools import lru_cache, partial from itertools import groupby from typing import TYPE_CHECKING, Any -from uuid import UUID from sqlalchemy import ( and_, @@ -98,17 +97,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, @@ -116,7 +112,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: @@ -273,14 +268,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, @@ -434,9 +421,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 @@ -1311,18 +1295,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 @@ -1459,39 +1431,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 @@ -1515,12 +1454,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) @@ -1571,162 +1504,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. @@ -1819,9 +1596,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! @@ -2357,16 +2131,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 ( @@ -2383,18 +2147,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 @@ -2434,15 +2192,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") @@ -2554,17 +2303,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? 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/models/dagrun.py b/airflow-core/src/airflow/models/dagrun.py index 4bc47dddea7ac..61242e45390d6 100644 --- a/airflow-core/src/airflow/models/dagrun.py +++ b/airflow-core/src/airflow/models/dagrun.py @@ -28,6 +28,9 @@ from uuid import UUID import structlog +from opentelemetry import context, trace +from opentelemetry.trace import StatusCode +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator from sqlalchemy import ( JSON, Enum, @@ -72,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 @@ -92,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 @@ -120,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.""" @@ -153,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) @@ -368,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__}`" @@ -461,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 @@ -1019,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( @@ -1302,9 +1195,6 @@ def recalculate(self) -> _UnfinishedStates: # 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: @@ -1331,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() 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/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/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/jobs/test_scheduler_job.py b/airflow-core/tests/unit/jobs/test_scheduler_job.py index 3aeebcedbdc2c..c23f180f3a479 100644 --- a/airflow-core/tests/unit/jobs/test_scheduler_job.py +++ b/airflow-core/tests/unit/jobs/test_scheduler_job.py @@ -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 @@ -3283,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 diff --git a/airflow-core/tests/unit/models/test_dagrun.py b/airflow-core/tests/unit/models/test_dagrun.py index f3de13422fac3..14722f83b0cce 100644 --- a/airflow-core/tests/unit/models/test_dagrun.py +++ b/airflow-core/tests/unit/models/test_dagrun.py @@ -27,6 +27,7 @@ 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 @@ -560,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" @@ -744,7 +607,6 @@ def on_success_callable(context): ) 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" @@ -3292,3 +3154,159 @@ def on_failure(context): 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/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index f6b79fba29aba..aa0bd36de11ad 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -1002,6 +1002,7 @@ middleware middlewares midnights milli +millis milton minikube misconfigured diff --git a/scripts/ci/docker-compose/integration-otel.yml b/scripts/ci/docker-compose/integration-otel.yml index 9d5c6c8117ded..f0d32104a14ab 100644 --- a/scripts/ci/docker-compose/integration-otel.yml +++ b/scripts/ci/docker-compose/integration-otel.yml @@ -70,7 +70,7 @@ services: - INTEGRATION_OTEL=true - OTEL_SERVICE_NAME=test - OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf - - OTEL_TRACES_EXPORTER=otlp + - OTEL_TRACES_EXPORTER=otlp_proto_http - OTEL_METRICS_EXPORTER=otlp - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://breeze-otel-collector:4318/v1/traces - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://breeze-otel-collector:4318/v1/metrics diff --git a/task-sdk/src/airflow/sdk/definitions/dag.py b/task-sdk/src/airflow/sdk/definitions/dag.py index 94e447e37a56d..c3f786584746b 100644 --- a/task-sdk/src/airflow/sdk/definitions/dag.py +++ b/task-sdk/src/airflow/sdk/definitions/dag.py @@ -1324,10 +1324,6 @@ def test( triggered_by=DagRunTriggeredByType.TEST, triggering_user_name="dag_test", ) - # Start a mock span so that one is present and not started downstream. We - # don't care about otel in dag.test and starting the span during dagrun update - # is not functioning properly in this context anyway. - dr.start_dr_spans_if_needed(tis=[]) log.debug("starting dagrun") # Instead of starting a scheduler, we run the minimal loop possible to check diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 0ef7a74a24b48..674935f5eaecb 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -19,14 +19,13 @@ from __future__ import annotations -import contextlib import contextvars import functools import os import sys import time from collections.abc import Callable, Iterable, Iterator, Mapping -from contextlib import suppress +from contextlib import ExitStack, contextmanager, suppress from datetime import datetime, timedelta, timezone from itertools import product from pathlib import Path @@ -36,6 +35,8 @@ import attrs import lazy_object_proxy import structlog +from opentelemetry import trace +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator from pydantic import AwareDatetime, ConfigDict, Field, JsonValue, TypeAdapter from airflow.dag_processing.bundles.base import BaseDagBundle, BundleVersionLock @@ -133,6 +134,32 @@ from airflow.sdk.exceptions import DagRunTriggerException from airflow.sdk.types import OutletEventAccessorsProtocol +log = structlog.get_logger("task") + +tracer = trace.get_tracer(__name__) + + +@contextmanager +def _make_task_span(msg: StartupDetails): + parent_context = ( + TraceContextTextMapPropagator().extract(msg.ti.context_carrier) if msg.ti.context_carrier else None + ) + ti = msg.ti + span_name = f"task_run.{ti.task_id}" + if ti.map_index is not None and ti.map_index >= 0: + span_name += f"_{ti.map_index}" + with tracer.start_as_current_span(span_name, context=parent_context) as span: + span.set_attributes( + { + "airflow.dag_id": ti.dag_id, + "airflow.task_id": ti.task_id, + "airflow.dag_run.run_id": ti.run_id, + "airflow.task_instance.try_number": ti.try_number, + "airflow.task_instance.map_index": ti.map_index if ti.map_index is not None else -1, + } + ) + yield span + class TaskRunnerMarker: """Marker for listener hooks, to properly detect from which component they are called.""" @@ -476,8 +503,6 @@ def get_first_reschedule_date(self, context: Context) -> AwareDatetime | None: retries: int = self.task.retries or 0 first_try_number = max_tries - retries + 1 - log = structlog.get_logger(logger_name="task") - log.debug("Requesting first reschedule date from supervisor") response = SUPERVISOR_COMMS.send( @@ -494,8 +519,6 @@ def get_previous_dagrun(self, state: str | None = None) -> DagRun | None: context = self.get_template_context() dag_run = context.get("dag_run") - log = structlog.get_logger(logger_name="task") - log.debug("Getting previous Dag run", dag_run=dag_run) if dag_run is None: @@ -530,7 +553,6 @@ def get_previous_ti( context = self.get_template_context() dag_run = context.get("dag_run") - log = structlog.get_logger(logger_name="task") log.debug("Getting previous task instance", task_id=self.task_id, state=state) # Use current dag run's logical_date if not provided @@ -846,7 +868,6 @@ def _verify_bundle_access(bundle_instance: BaseDagBundle, log: Logger) -> None: def get_startup_details() -> StartupDetails: # The parent sends us a StartupDetails message un-prompted. After this, every single message is only sent # in response to us sending a request. - log = structlog.get_logger(logger_name="task") if os.environ.get("_AIRFLOW__REEXECUTED_PROCESS") == "1" and ( msgjson := os.environ.get("_AIRFLOW__STARTUP_MSG") @@ -871,7 +892,6 @@ def get_startup_details() -> StartupDetails: def startup(msg: StartupDetails) -> tuple[RuntimeTaskInstance, Context, Logger]: - log = structlog.get_logger("task") # setproctitle causes issue on Mac OS: https://github.com/benoitc/gunicorn/issues/3021 os_type = sys.platform if os_type == "darwin": @@ -1238,7 +1258,7 @@ def _on_term(signum, frame): import jinja2 # If the task failed, swallow rendering error so it doesn't mask the main error. - with contextlib.suppress(jinja2.TemplateSyntaxError, jinja2.UndefinedError): + with suppress(jinja2.TemplateSyntaxError, jinja2.UndefinedError): previous_rendered_map_index = ti.rendered_map_index ti.rendered_map_index = _render_map_index(context, ti=ti, log=log) # Send update only if value changed (e.g., user set context variables during execution) @@ -1796,6 +1816,20 @@ def finalize( log.exception("error calling listener") +@contextmanager +def flush_spans(): + try: + yield + finally: + provider = trace.get_tracer_provider() + if hasattr(provider, "force_flush"): + from airflow.sdk.configuration import conf + + timeout_millis = conf.getint("traces", "task_runner_flush_timeout_milliseconds", fallback=30000) + provider.force_flush(timeout_millis=timeout_millis) + + +@flush_spans() def main(): log = structlog.get_logger(logger_name="task") @@ -1805,38 +1839,42 @@ def main(): stats_factory = stats_utils.get_stats_factory(Stats) Stats.initialize(factory=stats_factory) - try: + stack = ExitStack() + with stack: try: - startup_details = get_startup_details() - ti, context, log = startup(msg=startup_details) - except AirflowRescheduleException as reschedule: - log.warning("Rescheduling task during startup, marking task as UP_FOR_RESCHEDULE") - SUPERVISOR_COMMS.send( - msg=RescheduleTask( - reschedule_date=reschedule.reschedule_date, - end_date=datetime.now(tz=timezone.utc), + try: + startup_details = get_startup_details() + span = _make_task_span(msg=startup_details) + stack.enter_context(span) + ti, context, log = startup(msg=startup_details) + except AirflowRescheduleException as reschedule: + log.warning("Rescheduling task during startup, marking task as UP_FOR_RESCHEDULE") + SUPERVISOR_COMMS.send( + msg=RescheduleTask( + reschedule_date=reschedule.reschedule_date, + end_date=datetime.now(tz=timezone.utc), + ) ) - ) - sys.exit(0) - with BundleVersionLock( - bundle_name=ti.bundle_instance.name, - bundle_version=ti.bundle_instance.version, - ): - state, _, error = run(ti, context, log) - context["exception"] = error - finalize(ti, state, context, log, error) - except KeyboardInterrupt: - log.exception("Ctrl-c hit") - sys.exit(2) - except Exception: - log.exception("Top level error") - sys.exit(1) - finally: - # Ensure the request socket is closed on the child side in all circumstances - # before the process fully terminates. - if SUPERVISOR_COMMS and SUPERVISOR_COMMS.socket: - with suppress(Exception): - SUPERVISOR_COMMS.socket.close() + sys.exit(0) + with BundleVersionLock( + bundle_name=ti.bundle_instance.name, + bundle_version=ti.bundle_instance.version, + ): + state, _, error = run(ti, context, log) + context["exception"] = error + finalize(ti, state, context, log, error) + except KeyboardInterrupt: + log.exception("Ctrl-c hit") + sys.exit(2) + except Exception: + log.exception("Top level error") + sys.exit(1) + finally: + # Ensure the request socket is closed on the child side in all circumstances + # before the process fully terminates. + if SUPERVISOR_COMMS and SUPERVISOR_COMMS.socket: + with suppress(Exception): + SUPERVISOR_COMMS.socket.close() def reinit_supervisor_comms() -> None: @@ -1851,7 +1889,6 @@ def reinit_supervisor_comms() -> None: if "SUPERVISOR_COMMS" not in globals(): global SUPERVISOR_COMMS - log = structlog.get_logger(logger_name="task") fd = int(os.environ.get("__AIRFLOW_SUPERVISOR_FD", "0")) diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py index 2a495e557f70f..05191806be8a5 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py +++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py @@ -127,6 +127,7 @@ TaskRunnerMarker, _defer_task, _execute_task, + _make_task_span, _push_xcom_if_needed, _xcom_push, finalize, @@ -367,7 +368,7 @@ def test_parse_not_found_does_not_reschedule_when_max_attempts_reached(test_dags @mock.patch("airflow.sdk.execution_time.task_runner.get_startup_details") @mock.patch("airflow.sdk.execution_time.task_runner.CommsDecoder") def test_main_sends_reschedule_task_when_startup_reschedules( - mock_comms_decoder_cls, mock_get_startup_details, mock_startup, mock_exit, time_machine + mock_comms_decoder_cls, mock_get_startup_details, mock_startup, mock_exit, time_machine, make_ti_context ): """ If startup raises AirflowRescheduleException, the task runner should report a RescheduleTask @@ -379,7 +380,23 @@ def test_main_sends_reschedule_task_when_startup_reschedules( mock_comms_instance = mock.Mock() mock_comms_instance.socket = None mock_comms_decoder_cls.__getitem__.return_value.return_value = mock_comms_instance - mock_get_startup_details.return_value = mock.Mock() + what = StartupDetails( + ti=TaskInstance( + id=uuid7(), + task_id="my_task", + dag_id="test_dag", + run_id="test_run", + try_number=1, + dag_version_id=uuid7(), + context_carrier={}, + ), + dag_rel_path="", + bundle_info=BundleInfo(name="my-bundle", version=None), + ti_context=make_ti_context(), + start_date=timezone.utcnow(), + sentry_integration="", + ) + mock_get_startup_details.return_value = what mock_startup.side_effect = AirflowRescheduleException(reschedule_date=reschedule_date) # Move time @@ -395,6 +412,102 @@ def test_main_sends_reschedule_task_when_startup_reschedules( ] +def test_task_span_is_child_of_dag_run_span(make_ti_context): + """Task span must be a child of the dag run span propagated via 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 + + # Build a real SDK provider and exporter so we can inspect finished spans. + in_mem_exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(in_mem_exporter)) + + # Create a "dag run" span whose context we will propagate into the task. + dag_run_tracer = provider.get_tracer("dag_run") + with dag_run_tracer.start_as_current_span("dag_run.test_dag") as dag_run_span: + carrier: dict[str, str] = {} + TraceContextTextMapPropagator().inject(carrier) + dag_run_span_ctx = dag_run_span.get_span_context() + + what = StartupDetails( + ti=TaskInstance( + id=uuid7(), + task_id="my_task", + dag_id="test_dag", + run_id="test_run", + try_number=1, + dag_version_id=uuid7(), + context_carrier=carrier, + ), + dag_rel_path="", + bundle_info=BundleInfo(name="my-bundle", version=None), + ti_context=make_ti_context(), + start_date=timezone.utcnow(), + sentry_integration="", + ) + + task_tracer = provider.get_tracer("airflow.sdk.execution_time.task_runner") + with mock.patch("airflow.sdk.execution_time.task_runner.tracer", task_tracer): + with _make_task_span(what) as span: + task_span_ctx = span.get_span_context() + + # The task span must share the dag run's trace ID. + assert task_span_ctx.trace_id == dag_run_span_ctx.trace_id + + # The task span's parent must be the dag run span. + finished = in_mem_exporter.get_finished_spans() + task_spans = [s for s in finished if s.name == "task_run.my_task"] + assert len(task_spans) == 1 + assert task_spans[0].parent is not None + assert task_spans[0].parent.span_id == dag_run_span_ctx.span_id + + # Span attributes are set correctly. + attrs = task_spans[0].attributes + assert attrs["airflow.dag_id"] == "test_dag" + assert attrs["airflow.task_id"] == "my_task" + assert attrs["airflow.dag_run.run_id"] == "test_run" + assert attrs["airflow.task_instance.try_number"] == 1 + + +def test_task_span_no_parent_when_no_context_carrier(make_ti_context): + """When context_carrier is absent, the task span should be a root span (no parent).""" + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + + in_mem_exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(in_mem_exporter)) + + what = StartupDetails( + ti=TaskInstance( + id=uuid7(), + task_id="standalone_task", + dag_id="test_dag", + run_id="test_run", + try_number=1, + dag_version_id=uuid7(), + context_carrier=None, + ), + dag_rel_path="", + bundle_info=BundleInfo(name="my-bundle", version=None), + ti_context=make_ti_context(), + start_date=timezone.utcnow(), + sentry_integration="", + ) + + task_tracer = provider.get_tracer("airflow.sdk.execution_time.task_runner") + with mock.patch("airflow.sdk.execution_time.task_runner.tracer", task_tracer): + with _make_task_span(what): + pass + + finished = in_mem_exporter.get_finished_spans() + assert len(finished) == 1 + assert finished[0].parent is None + + def test_parse_module_in_bundle_root(tmp_path: Path, make_ti_context): """Check that the bundle path is added to sys.path, so Dags can import shared modules.""" tmp_path.joinpath("util.py").write_text("NAME = 'dag_name'") From 09d2bd711fc1f647d4896dca11e8da97601bbbb9 Mon Sep 17 00:00:00 2001 From: ANKIT KUMAR Date: Wed, 11 Mar 2026 00:52:09 +0530 Subject: [PATCH 118/280] Fix: Replace print with logger in Hive Hook kill method (#62990) Signed-off-by: Ankit Kumar --- .../apache/hive/src/airflow/providers/apache/hive/hooks/hive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py b/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py index e5ce0cc604778..e8b299d42ff64 100644 --- a/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py +++ b/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py @@ -527,7 +527,7 @@ def kill(self) -> None: """Kill Hive cli command.""" if hasattr(self, "sub_process"): if self.sub_process.poll() is None: - print("Killing the Hive job") + self.log.info("Killing the Hive job") self.sub_process.terminate() time.sleep(60) self.sub_process.kill() From b876865da1cc250860130ff1c065f66e5f1f5192 Mon Sep 17 00:00:00 2001 From: SameerMesiah97 <75502260+SameerMesiah97@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:23:10 +0000 Subject: [PATCH 119/280] Refactor VaultBackend to centralize secret path resolution and fetching logic (#62643) Introduce a private helper to remove duplicated mount parsing, base path handling, and get_secret invocation across public methods. Co-authored-by: Sameer Mesiah --- .../providers/hashicorp/secrets/vault.py | 50 ++++++++----------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py b/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py index b60e623851025..3459314bf7030 100644 --- a/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py +++ b/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py @@ -173,23 +173,30 @@ def _parse_path(self, secret_path: str) -> tuple[str | None, str | None]: return split_secret_path[0], split_secret_path[1] return "", secret_path - def get_response(self, conn_id: str) -> dict | None: - """ - Get data from Vault. + def _get_secret_with_base(self, base_path: str | None, key: str) -> dict | None: + """Resolve mount and base path, then fetch the secret from Vault.""" + mount_point, key_part = self._parse_path(key) - :return: The data from the Vault path if exists - """ - mount_point, conn_key = self._parse_path(conn_id) - if self.connections_path is None or conn_key is None: + if base_path is None or key_part is None: return None - if self.connections_path == "": - secret_path = conn_key + + if base_path == "": + secret_path = key_part else: - secret_path = self.build_path(self.connections_path, conn_key) + secret_path = self.build_path(base_path, key_part) + return self.vault_client.get_secret( secret_path=(mount_point + "/" if mount_point else "") + secret_path ) + def get_response(self, conn_id: str) -> dict | None: + """ + Get data from Vault. + + :return: The data from the Vault path if exists + """ + return self._get_secret_with_base(self.connections_path, conn_id) + # Make sure connection is imported this way for type checking, otherwise when importing # the backend it will get a circular dependency and fail if TYPE_CHECKING: @@ -225,16 +232,8 @@ def get_variable(self, key: str, team_name: str | None = None) -> str | None: :param team_name: Team name associated to the task trying to access the variable (if any) :return: Variable Value retrieved from the vault """ - mount_point, variable_key = self._parse_path(key) - if self.variables_path is None or variable_key is None: - return None - if self.variables_path == "": - secret_path = variable_key - else: - secret_path = self.build_path(self.variables_path, variable_key) - response = self.vault_client.get_secret( - secret_path=(mount_point + "/" if mount_point else "") + secret_path - ) + response = self._get_secret_with_base(self.variables_path, key) + if not response: return None try: @@ -250,16 +249,7 @@ def get_config(self, key: str) -> str | None: :param key: Configuration Option Key :return: Configuration Option Value retrieved from the vault """ - mount_point, config_key = self._parse_path(key) - if self.config_path is None or config_key is None: - return None - if self.config_path == "": - secret_path = config_key - else: - secret_path = self.build_path(self.config_path, config_key) - response = self.vault_client.get_secret( - secret_path=(mount_point + "/" if mount_point else "") + secret_path - ) + response = self._get_secret_with_base(self.config_path, key) if not response: return None try: From 614fc36ad5358fe3ebade0e41dcfb3d323234a29 Mon Sep 17 00:00:00 2001 From: SameerMesiah97 <75502260+SameerMesiah97@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:24:01 +0000 Subject: [PATCH 120/280] Fix string handling for use_tls in VaultHook (#62641) Ensure use_tls extras provided as strings (e.g. "false") are correctly interpreted instead of being treated as truthy, preventing incorrect HTTPS selection and SSL errors. Existing tests were updated to cover string inputs in addition to boolean values. Co-authored-by: Sameer Mesiah --- .../src/airflow/providers/hashicorp/hooks/vault.py | 12 ++++++++---- .../tests/unit/hashicorp/hooks/test_vault.py | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py b/providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py index 05d14b9b4dba8..6de3138a0c840 100644 --- a/providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py +++ b/providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py @@ -30,6 +30,7 @@ _VaultClient, ) from airflow.utils.helpers import merge_dicts +from airflow.utils.strings import to_boolean if TYPE_CHECKING: import hvac @@ -187,11 +188,14 @@ def __init__( else (None, None) ) - if self.connection.extra_dejson.get("use_tls") is not None: - if bool(self.connection.extra_dejson.get("use_tls")): - conn_protocol = "https" + use_tls = self.connection.extra_dejson.get("use_tls") + + if use_tls is not None: + if isinstance(use_tls, bool): + is_tls = use_tls else: - conn_protocol = "http" + is_tls = to_boolean(use_tls) + conn_protocol = "https" if is_tls else "http" else: if self.connection.conn_type == "vault": conn_protocol = "http" diff --git a/providers/hashicorp/tests/unit/hashicorp/hooks/test_vault.py b/providers/hashicorp/tests/unit/hashicorp/hooks/test_vault.py index 50b9f9e022c7d..141f6b838267d 100644 --- a/providers/hashicorp/tests/unit/hashicorp/hooks/test_vault.py +++ b/providers/hashicorp/tests/unit/hashicorp/hooks/test_vault.py @@ -199,6 +199,8 @@ def test_protocol(self, mock_hvac, mock_get_connection, protocol, expected_url): [ (True, "https://localhost:8180"), (False, "http://localhost:8180"), + ("true", "https://localhost:8180"), + ("false", "http://localhost:8180"), ], ) @mock.patch("airflow.providers.hashicorp.hooks.vault.VaultHook.get_connection") From a6eefcff23de76452733ade0ad40e057b65a0857 Mon Sep 17 00:00:00 2001 From: Pradeep Kalluri <128097794+kalluripradeep@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:27:58 +0000 Subject: [PATCH 121/280] fix: suppress warning for TYPE_CHECKING-only forward references in TaskFlow (#63053) Fixes #62945 --- .../tests/unit/standard/decorators/test_python.py | 12 +----------- task-sdk/src/airflow/sdk/bases/decorator.py | 10 +++------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/providers/standard/tests/unit/standard/decorators/test_python.py b/providers/standard/tests/unit/standard/decorators/test_python.py index d2aa38ec54434..95c96ce7bed89 100644 --- a/providers/standard/tests/unit/standard/decorators/test_python.py +++ b/providers/standard/tests/unit/standard/decorators/test_python.py @@ -195,17 +195,7 @@ def t3( # type: ignore[empty-body] y: int, ) -> "UnresolveableName[int, int]": ... - with pytest.warns(UserWarning, match="Cannot infer multiple_outputs.*t3") as recwarn: - line = sys._getframe().f_lineno - 5 if PY38 else sys._getframe().f_lineno - 2 - - if PY311: - # extra line explaining the error location in Py311 - line = line - 1 - - warn = recwarn[0] - assert warn.filename == __file__ - assert warn.lineno == line - + # No warning should be raised for TYPE_CHECKING-only forward references assert t3(5, 5).operator.multiple_outputs is False def test_infer_multiple_outputs_using_other_typing(self): diff --git a/task-sdk/src/airflow/sdk/bases/decorator.py b/task-sdk/src/airflow/sdk/bases/decorator.py index 53565c531a636..d76c029827814 100644 --- a/task-sdk/src/airflow/sdk/bases/decorator.py +++ b/task-sdk/src/airflow/sdk/bases/decorator.py @@ -20,7 +20,6 @@ import itertools import re import textwrap -import warnings from collections.abc import Callable, Collection, Iterator, Mapping, Sequence from contextlib import suppress from functools import cached_property, partial, update_wrapper @@ -448,12 +447,9 @@ def fake(): ... fake.__annotations__ = {"return": self.function.__annotations__["return"]} return_type = typing_extensions.get_type_hints(fake, self.function.__globals__).get("return", Any) - except NameError as e: - warnings.warn( - f"Cannot infer multiple_outputs for TaskFlow function {self.function.__name__!r} with forward" - f" type references that are not imported. (Error was {e})", - stacklevel=4, - ) + except NameError: + # Forward references using TYPE_CHECKING-only imports are valid Python patterns. + # We cannot infer multiple_outputs when the type is not available at runtime. return False except TypeError: # Can't evaluate return type. return False From 3aa5c0925552c6dd7d9e2e86e34b52c0ae20457e Mon Sep 17 00:00:00 2001 From: Yoann <60654707+YoannAbriel@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:32:19 -0700 Subject: [PATCH 122/280] fix: use configured schema in TableauHook Server construction (#62847) Previously, TableauHook.__init__ ignored the schema field from the connection configuration, always creating HTTP connections. Now it prepends the schema (e.g., https) to the host when configured. Fixes apache/airflow#62459 Co-authored-by: Claude Sonnet 4.6 --- .../providers/tableau/hooks/tableau.py | 3 +- .../tests/unit/tableau/hooks/test_tableau.py | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/providers/tableau/src/airflow/providers/tableau/hooks/tableau.py b/providers/tableau/src/airflow/providers/tableau/hooks/tableau.py index 4fd083820ba8e..3e39927870027 100644 --- a/providers/tableau/src/airflow/providers/tableau/hooks/tableau.py +++ b/providers/tableau/src/airflow/providers/tableau/hooks/tableau.py @@ -86,7 +86,8 @@ def __init__(self, site_id: str | None = None, tableau_conn_id: str = default_co self.tableau_conn_id = tableau_conn_id self.conn = self.get_connection(self.tableau_conn_id) self.site_id = site_id or self.conn.extra_dejson.get("site_id", "") - self.server = Server(self.conn.host) + server_address = f"{self.conn.schema}://{self.conn.host}" if self.conn.schema else self.conn.host + self.server = Server(server_address) verify: Any = self.conn.extra_dejson.get("verify", True) if isinstance(verify, str): verify = parse_boolean(verify) diff --git a/providers/tableau/tests/unit/tableau/hooks/test_tableau.py b/providers/tableau/tests/unit/tableau/hooks/test_tableau.py index e13642d6df963..9d5a9a18f3292 100644 --- a/providers/tableau/tests/unit/tableau/hooks/test_tableau.py +++ b/providers/tableau/tests/unit/tableau/hooks/test_tableau.py @@ -110,6 +110,17 @@ def setup_connections(self, create_connection_without_db): extra='{"auth": "jwt", "jwt_token": "fake_jwt_token", "site_id": ""}', ) ) + create_connection_without_db( + models.Connection( + conn_id="tableau_test_with_schema", + conn_type="tableau", + host="tableau", + schema="https", + login="user", + password="password", + extra='{"site_id": "my_site"}', + ) + ) @patch("airflow.providers.tableau.hooks.tableau.TableauAuth") @patch("airflow.providers.tableau.hooks.tableau.Server") @@ -327,6 +338,26 @@ def test_get_conn_ssl_bool_param(self, mock_server, mock_tableau_auth): mock_server.return_value.auth.sign_in.assert_called_once_with(mock_tableau_auth.return_value) mock_server.return_value.auth.sign_out.assert_called_once_with() + @patch("airflow.providers.tableau.hooks.tableau.TableauAuth") + @patch("airflow.providers.tableau.hooks.tableau.Server") + def test_get_conn_uses_schema_when_configured(self, mock_server, mock_tableau_auth): + """ + Test that Server is constructed with the schema prepended to the host when schema is configured. + """ + with TableauHook(tableau_conn_id="tableau_test_with_schema") as tableau_hook: + mock_server.assert_called_once_with(f"{tableau_hook.conn.schema}://{tableau_hook.conn.host}") + mock_server.return_value.auth.sign_out.assert_called_once_with() + + @patch("airflow.providers.tableau.hooks.tableau.TableauAuth") + @patch("airflow.providers.tableau.hooks.tableau.Server") + def test_get_conn_uses_host_only_without_schema(self, mock_server, mock_tableau_auth): + """ + Test that Server is constructed with the host only when no schema is configured (backward compatibility). + """ + with TableauHook(tableau_conn_id="tableau_test_password") as tableau_hook: + mock_server.assert_called_once_with(tableau_hook.conn.host) + mock_server.return_value.auth.sign_out.assert_called_once_with() + @patch("airflow.providers.tableau.hooks.tableau.TableauAuth") @patch("airflow.providers.tableau.hooks.tableau.Server") @patch("airflow.providers.tableau.hooks.tableau.Pager", return_value=[1, 2, 3]) From 0dc0397b48555a226ed0ea86fc3033e30219eba5 Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Wed, 11 Mar 2026 03:38:31 +0800 Subject: [PATCH 123/280] =?UTF-8?q?fix=20Inconsistent=20XCom=20Return=20Ty?= =?UTF-8?q?pe=20in=20Mapped=20Task=20Groups=20with=20Dynamic=20=E2=80=A6?= =?UTF-8?q?=20(#59104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix Inconsistent XCom Return Type in Mapped Task Groups with Dynamic Task Mapping * add unit test for xcom_pull * remove asc func --- .../src/airflow/models/taskinstance.py | 10 ++- .../tests/unit/models/test_taskinstance.py | 65 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/airflow-core/src/airflow/models/taskinstance.py b/airflow-core/src/airflow/models/taskinstance.py index 8112d955582fd..32daf41aec3e7 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -1683,6 +1683,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 +1715,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/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") From 0c471653a5ddef4285c0892f4c3862fd8c501712 Mon Sep 17 00:00:00 2001 From: Yoann <60654707+YoannAbriel@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:42:04 -0700 Subject: [PATCH 124/280] fix(providers/google): wrap sync get_job with sync_to_async in BigQueryAsyncHook (#63230) BigQueryAsyncHook._get_job() calls sync_hook.get_job() directly, blocking the event loop in triggers. This is a regression from #56363 which removed the async wrapping but left the sync call in place. Wrap with asgiref.sync_to_async to run the blocking call in a thread. Fixes apache/airflow#63182 --- .../providers/google/cloud/hooks/bigquery.py | 3 ++- .../unit/google/cloud/hooks/test_bigquery.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/providers/google/src/airflow/providers/google/cloud/hooks/bigquery.py b/providers/google/src/airflow/providers/google/cloud/hooks/bigquery.py index 1ee60b524ee19..c5b395b2fab34 100644 --- a/providers/google/src/airflow/providers/google/cloud/hooks/bigquery.py +++ b/providers/google/src/airflow/providers/google/cloud/hooks/bigquery.py @@ -33,6 +33,7 @@ import pendulum from aiohttp import ClientSession as ClientSession +from asgiref.sync import sync_to_async from gcloud.aio.bigquery import Job, Table as Table_async from google.cloud.bigquery import ( DEFAULT_RETRY, @@ -2089,7 +2090,7 @@ async def _get_job( ) -> BigQueryJob | UnknownJob: """Get BigQuery job by its ID, project ID and location.""" sync_hook = await self.get_sync_hook() - job = sync_hook.get_job(job_id=job_id, project_id=project_id, location=location) + job = await sync_to_async(sync_hook.get_job)(job_id=job_id, project_id=project_id, location=location) return job async def get_job_status( diff --git a/providers/google/tests/unit/google/cloud/hooks/test_bigquery.py b/providers/google/tests/unit/google/cloud/hooks/test_bigquery.py index bbd4f64bf4649..6a6fa8b3f3b3c 100644 --- a/providers/google/tests/unit/google/cloud/hooks/test_bigquery.py +++ b/providers/google/tests/unit/google/cloud/hooks/test_bigquery.py @@ -1548,6 +1548,23 @@ async def test_get_job_instance(self, mock_session, mock_auth_default): result = await hook.get_job_instance(project_id=PROJECT_ID, job_id=JOB_ID, session=mock_session) assert isinstance(result, Job) + @pytest.mark.asyncio + @mock.patch("airflow.providers.google.cloud.hooks.bigquery.sync_to_async") + @mock.patch("airflow.providers.google.cloud.hooks.bigquery.BigQueryAsyncHook.get_sync_hook") + async def test_get_job_runs_via_sync_to_async(self, mock_get_sync_hook, mock_sync_to_async): + """Verify _get_job wraps the sync get_job call with sync_to_async (#63182).""" + mock_sync_hook = mock.MagicMock() + mock_get_sync_hook.return_value = mock_sync_hook + + mock_async_get_job = mock.AsyncMock(return_value=mock.MagicMock()) + mock_sync_to_async.return_value = mock_async_get_job + + hook = BigQueryAsyncHook() + await hook._get_job(job_id=JOB_ID, project_id=PROJECT_ID, location="US") + + mock_sync_to_async.assert_called_once_with(mock_sync_hook.get_job) + mock_async_get_job.assert_awaited_once_with(job_id=JOB_ID, project_id=PROJECT_ID, location="US") + @pytest.mark.parametrize( ("job_state", "error_result", "expected"), [ From bf4f927feee325843559207b62554c66197510a7 Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Wed, 11 Mar 2026 03:59:44 +0800 Subject: [PATCH 125/280] Improve the log file template for ExecuteCallback by including dag_id and run_id in the path (#62616) --- airflow-core/src/airflow/executors/workloads/callback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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), From 19023dfef1d782f16d1f22b15c18c899af5db3d5 Mon Sep 17 00:00:00 2001 From: Yoann <60654707+YoannAbriel@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:08:14 -0700 Subject: [PATCH 126/280] fix: warn about hardcoded 24h visibility_timeout that kills long-running Celery tasks (#62869) * fix: warn about hardcoded 24h visibility_timeout that kills long-running Celery tasks Add a warning log when the default visibility_timeout of 86400 seconds (24 hours) is applied for Redis/SQS brokers, so users with long-running tasks know to increase it. Fix misleading task_acks_late documentation that incorrectly claimed it overrides visibility_timeout (it does not for Redis/SQS brokers). Fixes apache/airflow#62218 * fix: fix test assertion for visibility_timeout type (int vs string) * fix: apply ruff format to test file * fix: update get_provider_info.py to match provider.yaml description * ci: retrigger CI (unrelated infra failures) --- providers/celery/provider.yaml | 12 +++-- .../celery/executors/default_celery.py | 7 +++ .../providers/celery/get_provider_info.py | 4 +- .../celery/executors/test_celery_executor.py | 51 +++++++++++++++++++ 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/providers/celery/provider.yaml b/providers/celery/provider.yaml index 448d0fc369088..836a31b72a47e 100644 --- a/providers/celery/provider.yaml +++ b/providers/celery/provider.yaml @@ -310,10 +310,14 @@ config: instance then runs concurrently with the original task and the Airflow UI and logs only show an error message: 'Task Instance Not Running' FAILED: Task is in the running state' - Setting task_acks_late to True will force Celery to wait until a task is finished before a - new task instance is assigned. This effectively overrides the visibility timeout. + Setting task_acks_late to True acknowledges the task only after it completes. + Note: for Redis and SQS brokers, task_acks_late does NOT override visibility_timeout. + The broker will still redeliver tasks that exceed visibility_timeout regardless of this setting. + For long-running tasks, you must also increase [celery_broker_transport_options] visibility_timeout. + The default visibility_timeout is 86400 seconds (24 hours). See also: https://docs.celeryq.dev/en/stable/reference/celery.app.task.html#celery.app.task.Task.acks_late + https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html#visibility-timeout version_added: 3.6.0 type: boolean example: "True" @@ -368,8 +372,10 @@ config: description: | The visibility timeout defines the number of seconds to wait for the worker to acknowledge the task before the message is redelivered to another worker. + If not set, Airflow defaults to 86400 seconds (24 hours) for Redis and SQS brokers. + Tasks running longer than this value will be terminated and redelivered. Make sure to increase the visibility timeout to match the time of the longest - ETA you're planning to use. + task you're planning to run. visibility_timeout is only supported for Redis and SQS celery brokers. See: https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html#visibility-timeout diff --git a/providers/celery/src/airflow/providers/celery/executors/default_celery.py b/providers/celery/src/airflow/providers/celery/executors/default_celery.py index 50b6c7851277d..c11b7e9194899 100644 --- a/providers/celery/src/airflow/providers/celery/executors/default_celery.py +++ b/providers/celery/src/airflow/providers/celery/executors/default_celery.py @@ -58,6 +58,13 @@ def get_default_celery_config(team_conf) -> dict[str, Any]: if "visibility_timeout" not in broker_transport_options: if _broker_supports_visibility_timeout(broker_url): broker_transport_options["visibility_timeout"] = 86400 + log.warning( + "No visibility_timeout configured in [celery_broker_transport_options]. " + "Using default of 86400 seconds (24 hours). Celery tasks running longer than this " + "will be redelivered by the broker, which terminates the original task. " + "If you have long-running tasks, increase this value in your Airflow configuration: " + "[celery_broker_transport_options] visibility_timeout = " + ) if "sentinel_kwargs" in broker_transport_options: try: diff --git a/providers/celery/src/airflow/providers/celery/get_provider_info.py b/providers/celery/src/airflow/providers/celery/get_provider_info.py index 48097457f5d81..537344c7a4e66 100644 --- a/providers/celery/src/airflow/providers/celery/get_provider_info.py +++ b/providers/celery/src/airflow/providers/celery/get_provider_info.py @@ -205,7 +205,7 @@ def get_provider_info(): "default": "1.0", }, "task_acks_late": { - "description": "If an Airflow task's execution time exceeds the visibility_timeout, Celery will re-assign the\ntask to a Celery worker, even if the original task is still running successfully. The new task\ninstance then runs concurrently with the original task and the Airflow UI and logs only show an\nerror message:\n'Task Instance Not Running' FAILED: Task is in the running state'\nSetting task_acks_late to True will force Celery to wait until a task is finished before a\nnew task instance is assigned. This effectively overrides the visibility timeout.\nSee also:\nhttps://docs.celeryq.dev/en/stable/reference/celery.app.task.html#celery.app.task.Task.acks_late\n", + "description": "If an Airflow task's execution time exceeds the visibility_timeout, Celery will re-assign the\ntask to a Celery worker, even if the original task is still running successfully. The new task\ninstance then runs concurrently with the original task and the Airflow UI and logs only show an\nerror message:\n'Task Instance Not Running' FAILED: Task is in the running state'\nSetting task_acks_late to True acknowledges the task only after it completes.\nNote: for Redis and SQS brokers, task_acks_late does NOT override visibility_timeout.\nThe broker will still redeliver tasks that exceed visibility_timeout regardless of this setting.\nFor long-running tasks, you must also increase [celery_broker_transport_options] visibility_timeout.\nThe default visibility_timeout is 86400 seconds (24 hours).\nSee also:\nhttps://docs.celeryq.dev/en/stable/reference/celery.app.task.html#celery.app.task.Task.acks_late\nhttps://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html#visibility-timeout\n", "version_added": "3.6.0", "type": "boolean", "example": "True", @@ -245,7 +245,7 @@ def get_provider_info(): "description": "This section is for specifying options which can be passed to the\nunderlying celery broker transport. See:\nhttps://docs.celeryq.dev/en/latest/userguide/configuration.html#std:setting-broker_transport_options\n", "options": { "visibility_timeout": { - "description": "The visibility timeout defines the number of seconds to wait for the worker\nto acknowledge the task before the message is redelivered to another worker.\nMake sure to increase the visibility timeout to match the time of the longest\nETA you're planning to use.\nvisibility_timeout is only supported for Redis and SQS celery brokers.\nSee:\nhttps://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html#visibility-timeout\n", + "description": "The visibility timeout defines the number of seconds to wait for the worker\nto acknowledge the task before the message is redelivered to another worker.\nIf not set, Airflow defaults to 86400 seconds (24 hours) for Redis and SQS brokers.\nTasks running longer than this value will be terminated and redelivered.\nMake sure to increase the visibility timeout to match the time of the longest\ntask you're planning to run.\nvisibility_timeout is only supported for Redis and SQS celery brokers.\nSee:\nhttps://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html#visibility-timeout\n", "version_added": None, "type": "string", "example": "21600", diff --git a/providers/celery/tests/unit/celery/executors/test_celery_executor.py b/providers/celery/tests/unit/celery/executors/test_celery_executor.py index 1a7bef2523e8e..f5d34fb29162d 100644 --- a/providers/celery/tests/unit/celery/executors/test_celery_executor.py +++ b/providers/celery/tests/unit/celery/executors/test_celery_executor.py @@ -467,6 +467,57 @@ def test_celery_task_acks_late_loaded_from_string(): assert default_celery.DEFAULT_CELERY_CONFIG["task_acks_late"] is False +@conf_vars({("celery", "BROKER_URL"): "redis://localhost:6379/0"}) +def test_visibility_timeout_default_warns_when_not_configured(caplog): + """Test that a warning is logged when visibility_timeout defaults to 86400 (24h).""" + import importlib + + from airflow.providers.celery.executors.default_celery import log + + with caplog.at_level(logging.WARNING, logger=log.name): + importlib.reload(default_celery) + assert default_celery.DEFAULT_CELERY_CONFIG["broker_transport_options"]["visibility_timeout"] == 86400 + assert "No visibility_timeout configured" in caplog.text + assert "86400" in caplog.text + assert "long-running tasks" in caplog.text + + +@conf_vars( + { + ("celery", "BROKER_URL"): "redis://localhost:6379/0", + ("celery_broker_transport_options", "visibility_timeout"): "172800", + } +) +def test_visibility_timeout_no_warning_when_configured(caplog): + """Test that no warning is logged when visibility_timeout is explicitly configured.""" + import importlib + + from airflow.providers.celery.executors.default_celery import log + + with caplog.at_level(logging.WARNING, logger=log.name): + importlib.reload(default_celery) + assert ( + int(default_celery.DEFAULT_CELERY_CONFIG["broker_transport_options"]["visibility_timeout"]) + == 172800 + ) + assert "No visibility_timeout configured" not in caplog.text + + +@conf_vars({("celery", "BROKER_URL"): "amqp://guest:guest@localhost:5672//"}) +def test_visibility_timeout_not_set_for_unsupported_broker(caplog): + """Test that visibility_timeout is not set for brokers that don't support it (e.g. RabbitMQ).""" + import importlib + + from airflow.providers.celery.executors.default_celery import log + + with caplog.at_level(logging.WARNING, logger=log.name): + importlib.reload(default_celery) + assert "visibility_timeout" not in default_celery.DEFAULT_CELERY_CONFIG.get( + "broker_transport_options", {} + ) + assert "No visibility_timeout configured" not in caplog.text + + @conf_vars({("celery", "extra_celery_config"): '{"worker_max_tasks_per_child": 10}'}) def test_celery_extra_celery_config_loaded_from_string(): import importlib From bcd375fe79d4c457ff28a9ad4c28e0ed8d3bd093 Mon Sep 17 00:00:00 2001 From: Subham Date: Wed, 11 Mar 2026 01:43:02 +0530 Subject: [PATCH 127/280] Pass timeout to defer() in MSGraphSensor (#62157) (#62241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR ensures that the timeout parameter is correctly passed to the defer() method in MSGraphSensor. Previously, timeout was not propagated, which could lead to the sensor waiting indefinitely when deferring. Related issues/PRs: • Closes #62157 • Supersedes #62241 Notes: • Improves reliability of deferrable sensors by respecting the configured timeout. --- .../microsoft/azure/sensors/msgraph.py | 4 +++- .../microsoft/azure/sensors/test_msgraph.py | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/sensors/msgraph.py b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/sensors/msgraph.py index 040ccac0bb568..103fb1d1b451f 100644 --- a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/sensors/msgraph.py +++ b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/sensors/msgraph.py @@ -18,6 +18,7 @@ from __future__ import annotations from collections.abc import Callable, Sequence +from datetime import timedelta from typing import TYPE_CHECKING, Any from airflow.providers.common.compat.sdk import AirflowException, BaseSensorOperator @@ -27,7 +28,6 @@ from airflow.providers.microsoft.azure.triggers.msgraph import MSGraphTrigger, ResponseSerializer if TYPE_CHECKING: - from datetime import timedelta from io import BytesIO from msgraph_core import APIVersion @@ -183,9 +183,11 @@ def execute_complete( return result + # Re-defer with timeout so Airflow enforces the sensor timeout natively self.defer( trigger=TimeDeltaTrigger(self.retry_delay), method_name=self.retry_execute.__name__, + timeout=timedelta(seconds=self.timeout) if self.timeout is not None else None, ) return None diff --git a/providers/microsoft/azure/tests/unit/microsoft/azure/sensors/test_msgraph.py b/providers/microsoft/azure/tests/unit/microsoft/azure/sensors/test_msgraph.py index f3df0e882ae0b..66f439ba72266 100644 --- a/providers/microsoft/azure/tests/unit/microsoft/azure/sensors/test_msgraph.py +++ b/providers/microsoft/azure/tests/unit/microsoft/azure/sensors/test_msgraph.py @@ -17,8 +17,9 @@ from __future__ import annotations import json -from datetime import datetime +from datetime import datetime, timedelta from os.path import dirname +from unittest.mock import patch import pytest @@ -141,3 +142,18 @@ def test_template_fields(self): for template_field in MSGraphSensor.template_fields: getattr(sensor, template_field) + + def test_execute_complete_passes_timeout_to_defer(self): + sensor = MSGraphSensor( + task_id="check_timeout", + conn_id="powerbi", + url="myorg/admin/workspaces/scanStatus/{scanId}", + timeout=10, + ) + + with patch.object(sensor, "defer") as mock_defer: + sensor.execute_complete( + context={}, event={"status": "success", "response": json.dumps({"status": "running"})} + ) + mock_defer.assert_called_once() + assert mock_defer.call_args.kwargs["timeout"] == timedelta(seconds=10) From de3333dc5e9e9856fbc51940422b826f3368eb19 Mon Sep 17 00:00:00 2001 From: SameerMesiah97 <75502260+SameerMesiah97@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:19:52 +0000 Subject: [PATCH 128/280] Refactor non-deferrable execution flow to ensure cluster state reconciliation runs after creation completes. (#61951) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit – Extract reconciliation logic into `_reconcile_cluster_state()` – Ensure DELETING state waits for deletion and re-creates the cluster – Ensure CREATING state is fully reconciled before returning – Handle STOPPED state via restart path – Raise explicit exception if cluster is not found after LRO completion – Return reconciled cluster to avoid stale state Update and extend unit tests to cover reconciliation scenarios in the non-deferrable path (CREATING, DELETING, STOPPED, ERROR, and timeout cases). Co-authored-by: Sameer Mesiah --- .../google/cloud/operators/dataproc.py | 134 +++++++------ .../google/cloud/operators/test_dataproc.py | 180 +++++++++++++++++- 2 files changed, 258 insertions(+), 56 deletions(-) diff --git a/providers/google/src/airflow/providers/google/cloud/operators/dataproc.py b/providers/google/src/airflow/providers/google/cloud/operators/dataproc.py index 7b08af16a98d5..6dc6e0ab77529 100644 --- a/providers/google/src/airflow/providers/google/cloud/operators/dataproc.py +++ b/providers/google/src/airflow/providers/google/cloud/operators/dataproc.py @@ -811,11 +811,47 @@ def _retry_cluster_creation(self, hook: DataprocHook): self.log.info("Cluster created.") return Cluster.to_dict(cluster) + def _reconcile_cluster_state(self, hook: DataprocHook, cluster: Cluster) -> Cluster: + + if cluster.status.state == cluster.status.State.CREATING: + self.log.info("Cluster %s is in CREATING state.", self.cluster_name) + + cluster = self._wait_for_cluster_in_creating_state(hook) + self._handle_error_state(hook, cluster) + elif cluster.status.state == cluster.status.State.DELETING: + self.log.info("Cluster %s is in DELETING state.", self.cluster_name) + + self._wait_for_cluster_in_deleting_state(hook) + + self.log.info("Attempting to re-create cluster: %s", self.cluster_name) + + operation = self._create_cluster(hook) + hook.wait_for_operation( + timeout=self.timeout, + result_retry=self.retry, + operation=operation, + ) + cluster = self._get_cluster(hook) + + self._handle_error_state(hook, cluster) + elif cluster.status.state == cluster.status.State.STOPPED: + self.log.info("Cluster %s is in STOPPED state.", self.cluster_name) + + self.log.info("Attempting to re-start cluster: %s", self.cluster_name) + + # _start_cluster waits for the operation to complete. + self._start_cluster(hook) + + cluster = self._get_cluster(hook) + + return cluster + def execute(self, context: Context) -> dict: - self.log.info("Creating cluster: %s", self.cluster_name) + + self.log.info("Attempting to create cluster: %s", self.cluster_name) hook = DataprocHook(gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain) - # Save data required to display extra link no matter what the cluster status will be + # Save data required to display extra link regardless of the cluster status. project_id = self.project_id or hook.project_id if project_id: DataprocClusterLink.persist( @@ -826,37 +862,40 @@ def execute(self, context: Context) -> dict: ) try: - # First try to create a new cluster operation = self._create_cluster(hook) - if not self.deferrable and type(operation) is not str: - cluster = hook.wait_for_operation( - timeout=self.timeout, result_retry=self.retry, operation=operation + + if not self.deferrable and not isinstance(operation, str): + hook.wait_for_operation( + timeout=self.timeout, + result_retry=self.retry, + operation=operation, ) - self.log.info("Cluster created.") - return Cluster.to_dict(cluster) - cluster = hook.get_cluster( - project_id=self.project_id, region=self.region, cluster_name=self.cluster_name - ) - if cluster.status.state == cluster.status.State.RUNNING: - self.log.info("Cluster created.") - return Cluster.to_dict(cluster) - self.defer( - trigger=DataprocClusterTrigger( - cluster_name=self.cluster_name, - project_id=self.project_id, - region=self.region, - gcp_conn_id=self.gcp_conn_id, - impersonation_chain=self.impersonation_chain, - polling_interval_seconds=self.polling_interval_seconds, - delete_on_error=self.delete_on_error, - ), - method_name="execute_complete", - ) + + # Fetch current state. + cluster = self._get_cluster(hook) + + if self.deferrable: + if cluster.status.state != cluster.status.State.RUNNING: + self.defer( + trigger=DataprocClusterTrigger( + cluster_name=self.cluster_name, + project_id=self.project_id, + region=self.region, + gcp_conn_id=self.gcp_conn_id, + impersonation_chain=self.impersonation_chain, + polling_interval_seconds=self.polling_interval_seconds, + delete_on_error=self.delete_on_error, + ), + method_name="execute_complete", + ) + return + except AlreadyExists: if not self.use_if_exists: raise - self.log.info("Cluster already exists.") + self.log.info("Cluster %s already exists.", self.cluster_name) cluster = self._get_cluster(hook) + except DataprocResourceIsNotReadyError as resource_not_ready_error: if self.num_retries_if_resource_is_not_ready: attempt = self.num_retries_if_resource_is_not_ready @@ -876,39 +915,28 @@ def execute(self, context: Context) -> dict: self._delete_cluster(hook) self._wait_for_cluster_in_deleting_state(hook) raise resource_not_ready_error - except AirflowException as ae: - # There still could be a cluster created here in an ERROR state which - # should be deleted immediately rather than consuming another retry attempt - # (assuming delete_on_error is true (default)) - # This reduces overall the number of task attempts from 3 to 2 to successful cluster creation - # assuming the underlying GCE issues have resolved within that window. Users can configure - # a higher number of retry attempts in powers of two with 30s-60s wait interval + + except AirflowException as outer_airflow_exception: + # A cluster may have been created but entered ERROR state. + # If delete_on_error is enabled, delete it immediately so that + # the next retry attempt starts from a clean state. try: cluster = self._get_cluster(hook) self._handle_error_state(hook, cluster) - except AirflowException as ae_inner: - # We could get any number of failures here, including cluster not found and we - # can just ignore to ensure we surface the original cluster create failure - self.log.exception(ae_inner) + except AirflowException as inner_airflow_exception: + # Cleanup logic may raise secondary exceptions (e.g., cluster not found). + # Suppress those so that the original cluster creation failure is surfaced. + self.log.exception(inner_airflow_exception) finally: - raise ae + raise outer_airflow_exception - # Check if cluster is not in ERROR state + # Check if cluster is not in ERROR state. self._handle_error_state(hook, cluster) - if cluster.status.state == cluster.status.State.CREATING: - # Wait for cluster to be created - cluster = self._wait_for_cluster_in_creating_state(hook) - self._handle_error_state(hook, cluster) - elif cluster.status.state == cluster.status.State.DELETING: - # Wait for cluster to be deleted - self._wait_for_cluster_in_deleting_state(hook) - # Create new cluster - cluster = self._create_cluster(hook) - self._handle_error_state(hook, cluster) - elif cluster.status.state == cluster.status.State.STOPPED: - # if the cluster exists and already stopped, then start the cluster - self._start_cluster(hook) + # If cluster is not in RUNNING state, reconcile. + cluster = self._reconcile_cluster_state(hook, cluster) + + self.log.info("Cluster %s is RUNNING.", self.cluster_name) return Cluster.to_dict(cluster) def execute_complete(self, context: Context, event: dict[str, Any]) -> Any: diff --git a/providers/google/tests/unit/google/cloud/operators/test_dataproc.py b/providers/google/tests/unit/google/cloud/operators/test_dataproc.py index 3db5f497e4db5..d284c8755488a 100644 --- a/providers/google/tests/unit/google/cloud/operators/test_dataproc.py +++ b/providers/google/tests/unit/google/cloud/operators/test_dataproc.py @@ -826,7 +826,7 @@ def test_execute(self, mock_hook, to_dict_mock): # Test whether xcom push occurs before create cluster is called self.extra_links_manager_mock.assert_has_calls(expected_calls, any_order=False) - to_dict_mock.assert_called_once_with(mock_hook().wait_for_operation()) + to_dict_mock.assert_called_once_with(mock_hook.return_value.get_cluster.return_value) if AIRFLOW_V_3_0_PLUS: self.mock_ti.xcom_push.assert_called_once_with( key="dataproc_cluster", @@ -881,7 +881,7 @@ def test_execute_in_gke(self, mock_hook, to_dict_mock): # Test whether xcom push occurs before create cluster is called self.extra_links_manager_mock.assert_has_calls(expected_calls, any_order=False) - to_dict_mock.assert_called_once_with(mock_hook().wait_for_operation()) + to_dict_mock.assert_called_once_with(mock_hook.return_value.get_cluster.return_value) if AIRFLOW_V_3_0_PLUS: self.mock_ti.xcom_push.assert_called_once_with( key="dataproc_cluster", @@ -1015,7 +1015,7 @@ def test_execute_if_cluster_exists_in_deleting_state( mock_create_cluster.side_effect = [AlreadyExists("test"), cluster_running] mock_generator.return_value = [0] - mock_get_cluster.side_effect = [cluster_deleting, NotFound("test")] + mock_get_cluster.side_effect = [cluster_deleting, NotFound("test"), cluster_running] op = DataprocCreateClusterOperator( task_id=TASK_ID, @@ -1035,6 +1035,180 @@ def test_execute_if_cluster_exists_in_deleting_state( to_dict_mock.assert_called_once_with(cluster_running) + @mock.patch(DATAPROC_PATH.format("Cluster.to_dict")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._wait_for_cluster_in_deleting_state")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._get_cluster")) + @mock.patch(DATAPROC_PATH.format("DataprocHook")) + def test_execute_recreates_when_deleted_during_creation( + self, + mock_hook, + mock_get_cluster, + mock_wait_for_deleting, + to_dict_mock, + ): + mock_hook.return_value.wait_for_operation.return_value = None + + # First invocation of get_cluster should return cluster in DELETING state. + cluster_deleting = mock.MagicMock() + cluster_deleting.status.state = cluster_deleting.status.State.DELETING + + # Re-creation should return cluster in RUNNING state. + cluster_running = mock.MagicMock() + cluster_running.status.state = cluster_running.status.State.RUNNING + + mock_get_cluster.side_effect = [ + cluster_deleting, + cluster_running, + ] + + op = DataprocCreateClusterOperator( + task_id=TASK_ID, + region=GCP_REGION, + project_id=GCP_PROJECT, + cluster_name=CLUSTER_NAME, + cluster_config=CONFIG, + deferrable=False, + ) + + op.execute(context=mock.MagicMock()) + + # Ensure re-creation path is traversed. + assert mock_wait_for_deleting.called + assert mock_hook.return_value.create_cluster.call_count == 2 + + to_dict_mock.assert_called_once_with(cluster_running) + + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._wait_for_cluster_in_deleting_state")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._get_cluster")) + @mock.patch(DATAPROC_PATH.format("DataprocHook")) + def test_execute_deleting_timeout_raises( + self, + mock_hook, + mock_get_cluster, + mock_wait_for_deleting, + ): + mock_hook.return_value.wait_for_operation.return_value = None + + cluster_deleting = mock.MagicMock() + cluster_deleting.status.state = cluster_deleting.status.State.DELETING + + mock_get_cluster.return_value = cluster_deleting + mock_wait_for_deleting.side_effect = AirflowException("Timeout") + + op = DataprocCreateClusterOperator( + task_id=TASK_ID, + region=GCP_REGION, + project_id=GCP_PROJECT, + cluster_name=CLUSTER_NAME, + cluster_config=CONFIG, + deferrable=False, + ) + + with pytest.raises(AirflowException): + op.execute(context=mock.MagicMock()) + + # Ensure no re-creation is attempted. + assert mock_hook.return_value.create_cluster.call_count == 1 + + @mock.patch(DATAPROC_PATH.format("Cluster.to_dict")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._wait_for_cluster_in_creating_state")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._get_cluster")) + @mock.patch(DATAPROC_PATH.format("DataprocHook")) + def test_execute_waits_when_still_creating( + self, + mock_hook, + mock_get_cluster, + mock_wait_for_creating, + to_dict_mock, + ): + mock_hook.return_value.wait_for_operation.return_value = None + + cluster_creating = mock.MagicMock() + cluster_creating.status.state = cluster_creating.status.State.CREATING + + cluster_running = mock.MagicMock() + cluster_running.status.state = cluster_running.status.State.RUNNING + + mock_get_cluster.return_value = cluster_creating + mock_wait_for_creating.return_value = cluster_running + + op = DataprocCreateClusterOperator( + task_id=TASK_ID, + region=GCP_REGION, + project_id=GCP_PROJECT, + cluster_name=CLUSTER_NAME, + cluster_config=CONFIG, + deferrable=False, + ) + + op.execute(context=mock.MagicMock()) + + mock_wait_for_creating.assert_called_once() + to_dict_mock.assert_called_once_with(cluster_running) + + @mock.patch(DATAPROC_PATH.format("Cluster.to_dict")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._start_cluster")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._get_cluster")) + @mock.patch(DATAPROC_PATH.format("DataprocHook")) + def test_execute_stopped_cluster_restarts( + self, + mock_hook, + mock_get_cluster, + mock_start_cluster, + to_dict_mock, + ): + mock_hook.return_value.wait_for_operation.return_value = None + + cluster_stopped = mock.MagicMock() + cluster_stopped.status.state = cluster_stopped.status.State.STOPPED + + mock_get_cluster.return_value = cluster_stopped + + op = DataprocCreateClusterOperator( + task_id=TASK_ID, + region=GCP_REGION, + project_id=GCP_PROJECT, + cluster_name=CLUSTER_NAME, + cluster_config=CONFIG, + deferrable=False, + ) + + op.execute(context=mock.MagicMock()) + + mock_start_cluster.assert_called_once_with(mock_hook.return_value) + to_dict_mock.assert_called_once_with(cluster_stopped) + + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._handle_error_state")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._get_cluster")) + @mock.patch(DATAPROC_PATH.format("DataprocHook")) + def test_execute_error_state_after_wait_for_completion( + self, + mock_hook, + mock_get_cluster, + mock_handle_error, + ): + mock_hook.return_value.wait_for_operation.return_value = None + + cluster_error = mock.MagicMock() + cluster_error.status.state = cluster_error.status.State.ERROR + + mock_get_cluster.return_value = cluster_error + mock_handle_error.side_effect = AirflowException("Cluster error") + + op = DataprocCreateClusterOperator( + task_id=TASK_ID, + region=GCP_REGION, + project_id=GCP_PROJECT, + cluster_name=CLUSTER_NAME, + cluster_config=CONFIG, + deferrable=False, + ) + + with pytest.raises(AirflowException): + op.execute(context=mock.MagicMock()) + + mock_handle_error.assert_called_once() + @mock.patch(DATAPROC_PATH.format("DataprocHook")) @mock.patch(DATAPROC_TRIGGERS_PATH.format("DataprocAsyncHook")) def test_create_execute_call_defer_method(self, mock_trigger_hook, mock_hook): From d217a9bcc78bde787dd49a7fb7e7f077e4be2878 Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Tue, 10 Mar 2026 20:38:02 +0000 Subject: [PATCH 129/280] Enable parallel backfill by eliminating shared state between providers (#63288) Add --provider and --providers-json flags to extract_parameters.py and extract_connections.py so each backfill run uses an isolated temp providers.json and only scans the target provider. In --provider mode, modules.json is not written (it would be incomplete), so concurrent runs don't clobber each other. The backfill command now creates a TemporaryDirectory with per-version providers.json files instead of patching a shared file. --- dev/breeze/doc/11_registry_tasks.rst | 11 ++ dev/breeze/doc/images/output_registry.svg | 18 ++- dev/breeze/doc/images/output_registry.txt | 2 +- .../doc/images/output_registry_backfill.svg | 38 +++-- .../doc/images/output_registry_backfill.txt | 2 +- .../commands/registry_commands.py | 136 ++++++++-------- dev/breeze/tests/test_registry_backfill.py | 145 ++++++++++-------- dev/registry/extract_connections.py | 34 +++- dev/registry/extract_parameters.py | 93 +++++++---- 9 files changed, 288 insertions(+), 191 deletions(-) diff --git a/dev/breeze/doc/11_registry_tasks.rst b/dev/breeze/doc/11_registry_tasks.rst index 6b01d9065dcd6..9be90b576b3ae 100644 --- a/dev/breeze/doc/11_registry_tasks.rst +++ b/dev/breeze/doc/11_registry_tasks.rst @@ -79,6 +79,17 @@ Example usage: # 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 diff --git a/dev/breeze/doc/images/output_registry.svg b/dev/breeze/doc/images/output_registry.svg index e4b4f92c4f861..951851010e777 100644 --- a/dev/breeze/doc/images/output_registry.svg +++ b/dev/breeze/doc/images/output_registry.svg @@ -1,4 +1,4 @@ - + extract-data    Extract provider metadata, parameters, and connection types for the registry.                      backfill        Extract runtime parameters and connections for older provider versions. Uses 'uv run --with' to    install the specific version in a temporary environment and runs extract_parameters.py +           -extract_connections.py. No Docker needed.                                                          -publish-versionsPublish per-provider versions.json to S3 from deployed directories. Same pattern as 'breeze        -release-management publish-docs-to-s3'.                                                            -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +extract_connections.py. No Docker needed. Each version uses an isolated providers.json, so         +multiple providers can be backfilled in parallel from separate terminal sessions.                  +publish-versionsPublish per-provider versions.json to S3 from deployed directories. Same pattern as 'breeze        +release-management publish-docs-to-s3'.                                                            +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/dev/breeze/doc/images/output_registry.txt b/dev/breeze/doc/images/output_registry.txt index dae5504430be8..fadd741e8b654 100644 --- a/dev/breeze/doc/images/output_registry.txt +++ b/dev/breeze/doc/images/output_registry.txt @@ -1 +1 @@ -297843509448a55e7941eed3c0485df8 +8c9be6264d33af7facd1fbdf435697b7 diff --git a/dev/breeze/doc/images/output_registry_backfill.svg b/dev/breeze/doc/images/output_registry_backfill.svg index 12b49bb040261..4478565366e98 100644 --- a/dev/breeze/doc/images/output_registry_backfill.svg +++ b/dev/breeze/doc/images/output_registry_backfill.svg @@ -1,4 +1,4 @@ - +

{{ provider.description }}

- {% set counts = provider.module_counts or {} %} - {% set totalMods = (counts.operator or 0) + (counts.hook or 0) + (counts.sensor or 0) + (counts.trigger or 0) + (counts.transfer or 0) + (counts.notifier or 0) + (counts.secret or 0) + (counts.logging or 0) + (counts.executor or 0) + (counts.bundle or 0) + (counts.decorator or 0) %} + {% set counts = moduleCountsByProvider[provider.id] or {} %} + {% set totalMods = 0 %} + {% for t in types %}{% set totalMods = totalMods + (counts[t.id] or 0) %}{% endfor %} {{ totalMods }} module{{ "s" if totalMods != 1 }} {% if totalMods > 0 %}
- {% for mtype in ["operator", "hook", "sensor", "trigger", "transfer", "executor", "notifier", "secret", "logging", "bundle", "decorator"] %} - {% if counts[mtype] %} -
+ {% for t in types %} + {% if counts[t.id] %} +
{% endif %} {% endfor %}
diff --git a/registry/src/css/main.css b/registry/src/css/main.css index 11dfe1f06a333..e602fd7f1b0e9 100644 --- a/registry/src/css/main.css +++ b/registry/src/css/main.css @@ -2723,150 +2723,6 @@ main { } /* Stats */ -.provider-detail-page header .stats { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: var(--space-3); - margin-bottom: var(--space-6); -} - -@media (min-width: 640px) { - .provider-detail-page header .stats { - grid-template-columns: repeat(3, 1fr); - } -} - -@media (min-width: 1024px) { - .provider-detail-page header .stats { - grid-template-columns: repeat(6, 1fr); - } -} - -.provider-detail-page header .stat { - background: rgb(from var(--bg-secondary) r g b / 0.5); - border-radius: var(--radius-lg); - padding: var(--space-3); -} - -.provider-detail-page header .stat-row { - display: flex; - align-items: center; - gap: var(--space-2); - margin-bottom: var(--space-1); -} - -.provider-detail-page header .stat.operator { - border: 1px solid rgb(from var(--color-operator) r g b / 0.2); -} - -.provider-detail-page header .stat.hook { - border: 1px solid rgb(from var(--color-hook) r g b / 0.2); -} - -.provider-detail-page header .stat.sensor { - border: 1px solid rgb(from var(--color-sensor) r g b / 0.2); -} - -.provider-detail-page header .stat.trigger { - border: 1px solid rgb(from var(--color-trigger) r g b / 0.2); -} - -.provider-detail-page header .stat.transfer { - border: 1px solid rgb(from var(--color-transfer) r g b / 0.2); -} - -.provider-detail-page header .stat.bundle { - border: 1px solid rgb(from var(--color-bundle) r g b / 0.2); -} - -.provider-detail-page header .stat.total { - border: 1px solid rgb(from var(--accent-primary) r g b / 0.2); -} - -.provider-detail-page header .stat .icon { - width: 1.5rem; - height: 1.5rem; - border-radius: var(--radius-sm); - display: flex; - align-items: center; - justify-content: center; - font-size: var(--text-xs); - font-weight: var(--font-bold); -} - -.provider-detail-page header .stat.operator .icon { - background: rgb(from var(--color-operator) r g b / 0.2); - color: var(--color-operator); -} - -.provider-detail-page header .stat.hook .icon { - background: rgb(from var(--color-hook) r g b / 0.2); - color: var(--color-hook); -} - -.provider-detail-page header .stat.sensor .icon { - background: rgb(from var(--color-sensor) r g b / 0.2); - color: var(--color-sensor); -} - -.provider-detail-page header .stat.trigger .icon { - background: rgb(from var(--color-trigger) r g b / 0.2); - color: var(--color-trigger); -} - -.provider-detail-page header .stat.transfer .icon { - background: rgb(from var(--color-transfer) r g b / 0.2); - color: var(--color-transfer); -} - -.provider-detail-page header .stat.bundle .icon { - background: rgb(from var(--color-bundle) r g b / 0.2); - color: var(--color-bundle); -} - -.provider-detail-page header .stat.total .icon { - background: rgb(from var(--accent-primary) r g b / 0.2); - color: var(--accent-primary); -} - -.provider-detail-page header .stat .count { - font-size: var(--text-xl); - font-weight: var(--font-bold); -} - -.provider-detail-page header .stat.operator .count { - color: var(--color-operator); -} - -.provider-detail-page header .stat.hook .count { - color: var(--color-hook); -} - -.provider-detail-page header .stat.sensor .count { - color: var(--color-sensor); -} - -.provider-detail-page header .stat.trigger .count { - color: var(--color-trigger); -} - -.provider-detail-page header .stat.transfer .count { - color: var(--color-transfer); -} - -.provider-detail-page header .stat.bundle .count { - color: var(--color-bundle); -} - -.provider-detail-page header .stat.total .count { - color: var(--accent-primary); -} - -.provider-detail-page header .stat .label { - font-size: var(--text-xs); - color: var(--text-secondary); -} - /* Header Footer */ .provider-detail-page header .footer { display: flex; diff --git a/registry/src/js/search.js b/registry/src/js/search.js index 76416338d14cb..d7b13d523d636 100644 --- a/registry/src/js/search.js +++ b/registry/src/js/search.js @@ -24,19 +24,18 @@ let currentResults = []; let searchId = 0; - const typeLabels = { - operator: 'Operator', - hook: 'Hook', - sensor: 'Sensor', - trigger: 'Trigger', - transfer: 'Transfer', - bundle: 'Bundle', - notifier: 'Notifier', - secret: 'Secrets Backend', - logging: 'Log Handler', - executor: 'Executor', - decorator: 'Decorator', - }; + // Type labels loaded from types.json (injected via base.njk) + const typeLabels = {}; + try { + const typesEl = document.getElementById('types-data'); + if (typesEl) { + for (const t of JSON.parse(typesEl.textContent)) { + typeLabels[t.id] = t.label; + } + } + } catch (_) { + // Fallback: empty object — badges will show raw type name + } function escapeHtml(str) { const div = document.createElement('div'); diff --git a/registry/src/provider-version.njk b/registry/src/provider-version.njk index 790b19172ba5a..51179078bab49 100644 --- a/registry/src/provider-version.njk +++ b/registry/src/provider-version.njk @@ -14,7 +14,7 @@ eleventyComputed: {# Choose data source: latest uses providers.json + modules.json, older uses versionData #} {% if pv.isLatest %} - {% set moduleCounts = pv.provider.module_counts or {} %} + {% set moduleCounts = moduleCountsByProvider[pv.provider.id] or {} %} {% set deps = pv.provider.dependencies or [] %} {% set extras = pv.provider.optional_extras or {} %} {% set conns = pv.provider.connection_types or [] %} @@ -35,7 +35,8 @@ eleventyComputed: {% set sourceUrl = pv.provider.source_url %} {% endif %} -{% set totalModules = (moduleCounts.operator or 0) + (moduleCounts.hook or 0) + (moduleCounts.sensor or 0) + (moduleCounts.trigger or 0) + (moduleCounts.transfer or 0) + (moduleCounts.bundle or 0) + (moduleCounts.notifier or 0) + (moduleCounts.secret or 0) + (moduleCounts.logging or 0) + (moduleCounts.executor or 0) + (moduleCounts.decorator or 0) %} +{% set totalModules = 0 %} +{% for t in types %}{% set totalModules = totalModules + (moduleCounts[t.id] or 0) %}{% endfor %}
{# Breadcrumb #} @@ -94,77 +95,16 @@ eleventyComputed:
- {# Stats Grid #} -
- {% if moduleCounts.operator > 0 %} -
-
- O - {{ moduleCounts.operator }} -
- Operators -
- {% endif %} - {% if moduleCounts.hook > 0 %} -
-
- H - {{ moduleCounts.hook }} -
- Hooks -
- {% endif %} - {% if moduleCounts.sensor > 0 %} -
-
- S - {{ moduleCounts.sensor }} -
- Sensors -
- {% endif %} - {% if moduleCounts.trigger > 0 %} -
-
- T - {{ moduleCounts.trigger }} -
- Triggers -
- {% endif %} - {% if moduleCounts.transfer > 0 %} -
-
- X - {{ moduleCounts.transfer }} -
- Transfers -
- {% endif %} - {% if moduleCounts.bundle > 0 %} -
-
- B - {{ moduleCounts.bundle }} -
- Bundles -
- {% endif %} -
-
- Σ - {{ totalModules }} -
- Total Modules -
-
- - {# Bottom row: Downloads and actions #} + {# Bottom row: Downloads, module count, and actions #}