You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ra...@apache.org on 2026/09/18 06:46:58 UTC
(airflow) branch v3-3-test updated (86e8fdd597b -> f1093760c34)
This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a change to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
discard 86e8fdd597b [v3-3-test] Decode deadline alert interval and callback without generic deserialization (#72651) (#73304)
omit 0e9f66a9574 [v3-3-test] Document that plugin names must be unique (#73197) (#73203)
omit 217654a6584 Split the api_fastapi common parameters module into a package (#73239)
omit 1b64dcb91ee [v3-3-test] Fix DAG.cli() crashing on dags test --show-dagrun (#72810) (#73216)
omit 240306e2492 [v3-3-test] Reject non-numeric --limit values in airflow dags list-jobs (#72891) (#73214)
omit de56215b4fe Bump the 3-3-fab-ui-package-updates group across 1 directory with 3 updates (#73208)
omit e4c58e48381 Authorize POST /assets/events on the asset named in the body (#73007) (#73139)
omit a84a76d9a11 [v3-3-test] Keep the Gradle wrapper jar out of the source release (#69444) (#73108)
omit c313f5da521 [v3-3-test] Clarify that AccessView.JOBS is the Edge worker management permission (#72627) (#73073)
add d15b0651fa1 Update version to 3.3.2
add d5e9f5b3163 Add release notes for 3.3.2rc1
add aa19d2dbb9e Allow GHSA-8mgp-746c-j5xp (nltk) in the dependency review gate
new 9d2a71d4b8d [v3-3-test] Clarify that AccessView.JOBS is the Edge worker management permission (#72627) (#73073)
new 9dd0068ca34 [v3-3-test] Keep the Gradle wrapper jar out of the source release (#69444) (#73108)
new 1c0d24cede6 Authorize POST /assets/events on the asset named in the body (#73007) (#73139)
new ca43e967a6f Bump the 3-3-fab-ui-package-updates group across 1 directory with 3 updates (#73208)
new 4b7d6c43f72 [v3-3-test] Reject non-numeric --limit values in airflow dags list-jobs (#72891) (#73214)
new cec23c41cf1 [v3-3-test] Fix DAG.cli() crashing on dags test --show-dagrun (#72810) (#73216)
new 93e3df37b75 Split the api_fastapi common parameters module into a package (#73239)
new 0b3a31be6ff [v3-3-test] Document that plugin names must be unique (#73197) (#73203)
new f1093760c34 [v3-3-test] Decode deadline alert interval and callback without generic deserialization (#72651) (#73304)
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (86e8fdd597b)
\
N -- N -- N refs/heads/v3-3-test (f1093760c34)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 9 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.github/workflows/java-sdk-dependency-security.yml | 9 +
README.md | 12 +-
RELEASE_NOTES.rst | 209 +++++++++++++++++++++
.../docs/installation/supported-versions.rst | 2 +-
airflow-core/newsfragments/68518.bugfix.rst | 1 -
airflow-core/newsfragments/70923.bugfix.rst | 1 -
airflow-core/newsfragments/70977.bugfix.rst | 1 -
airflow-core/newsfragments/71113.significant.rst | 22 ---
airflow-core/newsfragments/71704.bugfix.rst | 1 -
airflow-core/newsfragments/71767.bugfix.rst | 1 -
airflow-core/newsfragments/71802.bugfix.rst | 1 -
airflow-core/newsfragments/71814.bugfix.rst | 1 -
airflow-core/newsfragments/72042.bugfix.rst | 1 -
airflow-core/newsfragments/72225.significant.rst | 27 ---
airflow-core/newsfragments/72327.bugfix.rst | 1 -
airflow-core/newsfragments/72374.bugfix.rst | 1 -
airflow-core/pyproject.toml | 4 +-
airflow-core/src/airflow/__init__.py | 2 +-
dev/breeze/src/airflow_breeze/global_constants.py | 2 +-
docker-stack-docs/README.md | 10 +-
.../extending/add-airflow-configuration/Dockerfile | 2 +-
.../extending/add-apt-packages/Dockerfile | 2 +-
.../add-build-essential-extend/Dockerfile | 2 +-
.../extending/add-providers/Dockerfile | 2 +-
.../add-pypi-packages-constraints/Dockerfile | 2 +-
.../extending/add-pypi-packages-uv/Dockerfile | 2 +-
.../extending/add-pypi-packages/Dockerfile | 2 +-
.../extending/add-python-venv/Dockerfile | 2 +-
.../extending/add-requirement-packages/Dockerfile | 2 +-
.../extending/custom-providers/Dockerfile | 2 +-
.../extending/embedding-dags/Dockerfile | 2 +-
.../extending/writable-directory/Dockerfile | 2 +-
docker-stack-docs/entrypoint.rst | 14 +-
docs/spelling_wordlist.txt | 9 +
generated/PYPI_README.md | 10 +-
pyproject.toml | 6 +-
reproducible_build.yaml | 4 +-
scripts/ci/prek/supported_versions.py | 2 +-
task-sdk/src/airflow/sdk/__init__.py | 2 +-
39 files changed, 274 insertions(+), 106 deletions(-)
delete mode 100644 airflow-core/newsfragments/68518.bugfix.rst
delete mode 100644 airflow-core/newsfragments/70923.bugfix.rst
delete mode 100644 airflow-core/newsfragments/70977.bugfix.rst
delete mode 100644 airflow-core/newsfragments/71113.significant.rst
delete mode 100644 airflow-core/newsfragments/71704.bugfix.rst
delete mode 100644 airflow-core/newsfragments/71767.bugfix.rst
delete mode 100644 airflow-core/newsfragments/71802.bugfix.rst
delete mode 100644 airflow-core/newsfragments/71814.bugfix.rst
delete mode 100644 airflow-core/newsfragments/72042.bugfix.rst
delete mode 100644 airflow-core/newsfragments/72225.significant.rst
delete mode 100644 airflow-core/newsfragments/72327.bugfix.rst
delete mode 100644 airflow-core/newsfragments/72374.bugfix.rst
(airflow) 08/09: [v3-3-test] Document that plugin names must be unique (#73197) (#73203)
Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 0b3a31be6ff9f762925e76fd9d1db8c94db85db4
Author: github-actions[bot] <41...@users.noreply.github.com>
AuthorDate: Wed Sep 16 17:22:45 2026 +0200
[v3-3-test] Document that plugin names must be unique (#73197) (#73203)
(cherry picked from commit 49674b776df40562e47b60693958293b734199ce)
Co-authored-by: Aaron Chen <na...@gmail.com>
---
airflow-core/docs/administration-and-deployment/plugins.rst | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/airflow-core/docs/administration-and-deployment/plugins.rst b/airflow-core/docs/administration-and-deployment/plugins.rst
index 265237fc22d..eaa3a6f5979 100644
--- a/airflow-core/docs/administration-and-deployment/plugins.rst
+++ b/airflow-core/docs/administration-and-deployment/plugins.rst
@@ -172,6 +172,10 @@ You can derive it by inheritance (please refer to the example below). In the exa
defined as class attributes, but you can also define them as properties if you need to perform
additional initialization. Please note ``name`` inside this class must be specified.
+``name`` must also be unique across all plugins. Airflow registers the first plugin it discovers under a
+given name and skips any later plugin that reuses it, so two plugins sharing a name means one of them is
+not loaded.
+
Make sure you restart the webserver and scheduler after making changes to plugins so that they take effect.
Plugin Management Interface
(airflow) 04/09: Bump the 3-3-fab-ui-package-updates group across 1 directory with 3 updates (#73208)
Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit ca43e967a6f9526244652980916cc88ceffd07c6
Author: Dependabot [bot] <49...@users.noreply.github.com>
AuthorDate: Tue Sep 15 22:02:29 2026 +0300
Bump the 3-3-fab-ui-package-updates group across 1 directory with 3 updates (#73208)
Bumps the 3-3-fab-ui-package-updates group with 3 updates in the /providers/fab/src/airflow/providers/fab/www directory: [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core), [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) and [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env).
Updates `@babel/core` from 8.0.1 to 8.0.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v8.0.5/packages/babel-core)
Updates `@babel/eslint-parser` from 8.0.1 to 8.0.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v8.0.5/eslint/babel-eslint-parser)
Updates `@babel/preset-env` from 8.0.2 to 8.0.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v8.0.5/packages/babel-preset-env)
---
updated-dependencies:
- dependency-name: "@babel/core"
dependency-version: 8.0.5
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: 3-3-fab-ui-package-updates
- dependency-name: "@babel/eslint-parser"
dependency-version: 8.0.5
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: 3-3-fab-ui-package-updates
- dependency-name: "@babel/preset-env"
dependency-version: 8.0.5
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: 3-3-fab-ui-package-updates
...
Signed-off-by: dependabot[bot] <su...@github.com>
Co-authored-by: dependabot[bot] <49...@users.noreply.github.com>
---
.../fab/src/airflow/providers/fab/www/package.json | 6 +-
.../src/airflow/providers/fab/www/pnpm-lock.yaml | 945 ++++++++++-----------
2 files changed, 442 insertions(+), 509 deletions(-)
diff --git a/providers/fab/src/airflow/providers/fab/www/package.json b/providers/fab/src/airflow/providers/fab/www/package.json
index fd434cd2e48..efa4ac7eced 100644
--- a/providers/fab/src/airflow/providers/fab/www/package.json
+++ b/providers/fab/src/airflow/providers/fab/www/package.json
@@ -39,10 +39,10 @@
]
},
"devDependencies": {
- "@babel/core": "^8.0.1",
- "@babel/eslint-parser": "^8.0.1",
+ "@babel/core": "^8.0.5",
+ "@babel/eslint-parser": "^8.0.5",
"@babel/plugin-transform-runtime": "^8.0.1",
- "@babel/preset-env": "^8.0.2",
+ "@babel/preset-env": "^8.0.5",
"babel-loader": "^10.1.1",
"copy-webpack-plugin": "^14.0.0",
"css-loader": "7.1.5",
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 e549c4169f5..230a2217126 100644
--- a/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml
+++ b/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml
@@ -30,20 +30,20 @@ importers:
version: 0.6.3
devDependencies:
'@babel/core':
- specifier: ^8.0.1
- version: 8.0.1
+ specifier: ^8.0.5
+ version: 8.0.5
'@babel/eslint-parser':
- specifier: ^8.0.1
- version: 8.0.1(@babel/core@8.0.1)(eslint@10.10.0)
+ specifier: ^8.0.5
+ version: 8.0.5(@babel/core@8.0.5)(eslint@10.10.0)
'@babel/plugin-transform-runtime':
specifier: ^8.0.1
- version: 8.0.1(@babel/core@8.0.1)
+ version: 8.0.1(@babel/core@8.0.5)
'@babel/preset-env':
- specifier: ^8.0.2
- version: 8.0.2(@babel/core@8.0.1)
+ specifier: ^8.0.5
+ version: 8.0.5(@babel/core@8.0.5)
babel-loader:
specifier: ^10.1.1
- version: 10.1.1(@babel/core@8.0.1)(webpack@5.110.3)
+ version: 10.1.1(@babel/core@8.0.5)(webpack@5.110.3)
copy-webpack-plugin:
specifier: ^14.0.0
version: 14.0.0(webpack@5.110.3)
@@ -103,41 +103,41 @@ packages:
resolution: {integrity: sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==}
engines: {node: ^22.18.0 || >=24.11.0}
- '@babel/compat-data@8.0.0':
- resolution: {integrity: sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==}
+ '@babel/compat-data@8.0.5':
+ resolution: {integrity: sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==}
engines: {node: ^22.18.0 || >=24.11.0}
- '@babel/core@8.0.1':
- resolution: {integrity: sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==}
+ '@babel/core@8.0.5':
+ resolution: {integrity: sha512-2/oWkgTbBYoqioCWAE4XJobOrzwxTDa5/XjDP3tJ1BhDr/owcd9qnXBp4xc3/2G5X4bvXM04nrxbRJxFcXAxFQ==}
engines: {node: ^22.18.0 || >=24.11.0}
- '@babel/eslint-parser@8.0.1':
- resolution: {integrity: sha512-2javO8pAQv/ld6sS6OcxoLAlzZEZy+xm99bnoAfMhzKSumKhdF5wylpbZB7XTorWr3KLPtx5K95eduJPOy1mzA==}
+ '@babel/eslint-parser@8.0.5':
+ resolution: {integrity: sha512-q4c+FpoJ/FKeiLnZ9Os3vWnSlZw3dBmlTUx6vuITRqbd8yn4hgUB0/OOJ+gi5lt+mOx/0IW2rXINR98JJllfag==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
eslint: ^9.0.0 || ^10.0.0
- '@babel/generator@8.0.0':
- resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==}
+ '@babel/generator@8.0.5':
+ resolution: {integrity: sha512-f/TuhuMAxJqhwxEGNsJrswuG9VHmh0oNFoQoo6TbpgtFAz9wYZXcTAcWZMHfp7ljesr0RG04bp3Aos9GI59L7w==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/helper-annotate-as-pure@8.0.0':
resolution: {integrity: sha512-NSpMkMsvvZqzThJ0p1B02cbtA2ObEyfBvq950bmNkyxsxvcxwhvvCB036rKhlEnuBBo30bOrk13u3FzlKSoRrw==}
engines: {node: ^22.18.0 || >=24.11.0}
- '@babel/helper-compilation-targets@8.0.0':
- resolution: {integrity: sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==}
+ '@babel/helper-compilation-targets@8.0.5':
+ resolution: {integrity: sha512-Qk8ahMGooH5mz6uuhoDvfZGkUf/Mf3RTBucVVl4MKx4LKMTv872TeW8O92h15iVtlN8wAROBIpI1aV6x1z0LCQ==}
engines: {node: ^22.18.0 || >=24.11.0}
- '@babel/helper-create-class-features-plugin@8.0.1':
- resolution: {integrity: sha512-++t3ZktzlLmASAxIlxeXQK9Z2YwUafYGYcvGBFevqOqt16HozVHStUoQvWD09fzAZOb/uJGpUTBuGK41AJAuOA==}
+ '@babel/helper-create-class-features-plugin@8.0.5':
+ resolution: {integrity: sha512-ckUE7tmolBbW4QV02lD874sWSPy8da2A09i/h0qS+IWolR+WiNhpq7C/rHZm00uOIkUb0Cp6+0Cy1ku/wj5uQQ==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/helper-create-regexp-features-plugin@8.0.1':
- resolution: {integrity: sha512-PydTbcVTiIfVweHMeY1u3MslaD/ZzvnaTNhJp+7ghofelLWshF66Ckc/ZsjStfvRQIKQ4uVG0yEJucyDtyrWgw==}
+ '@babel/helper-create-regexp-features-plugin@8.0.5':
+ resolution: {integrity: sha512-TWnIgw21pAv/FbBKB8ks26DMxQ+A3aBdi1HUBp+QhqSE8QdOqJgSK/FEvdMYPTZcHBBJPZ5pN6DDKmC5ZYPJbg==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
@@ -152,16 +152,16 @@ packages:
resolution: {integrity: sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==}
engines: {node: ^22.18.0 || >=24.11.0}
- '@babel/helper-member-expression-to-functions@8.0.0':
- resolution: {integrity: sha512-xkXrMbtk87Gk7+oKBVmBc6EORg/Qwx++AHESldmHkpvG8wgccdhJJFwrzqlF382Fk8wfXhJHWE/g/43QvEGNPQ==}
+ '@babel/helper-member-expression-to-functions@8.0.5':
+ resolution: {integrity: sha512-GLe05QD98BkNFTkjaqeqF9QSRoHLKrrB4tpoplyQuuPrQJ72rtmkM4wvqJLX/sjZPqkbK5MUYSsuUTqdJoOVjA==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/helper-module-imports@8.0.0':
resolution: {integrity: sha512-NZ7mSS93o4ndX4KrbD7W8Sf3QT8Qe24PrnFyUcuOPDzK6faqDFKjY9RG7he7+I7FdiQ4llpnosFqzrXa+Vy3Ew==}
engines: {node: ^22.18.0 || >=24.11.0}
- '@babel/helper-module-transforms@8.0.1':
- resolution: {integrity: sha512-UgAhl1kqiW5ciE0yCXqqvnb4H2n3IELJ7lIIQRezwDPilPEZX5i+Rvbja9MFTkwUn2biEiSMeV31aUzR4Lwakw==}
+ '@babel/helper-module-transforms@8.0.5':
+ resolution: {integrity: sha512-lUsSqMD0l5cJKl+3vlTV/hPDA/IvNkcHWVI3zFud2Vur+C9bCKdpqbpkN9eABembedbdWwF0qow6nbdIpNlpcw==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
@@ -216,17 +216,12 @@ packages:
resolution: {integrity: sha512-Qpm8+wi5xfDkBfollanwriCcKniFfBmMmaKB01GVM6VGzKXo1fdxosZp04qEr5HM+LKhwr3hG1yRy8+ORsficA==}
engines: {node: ^22.18.0 || >=24.11.0}
- '@babel/helpers@8.0.0':
- resolution: {integrity: sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==}
+ '@babel/helpers@8.0.5':
+ resolution: {integrity: sha512-fQtPOXjYOYv85PIdwotp2TJGVYOycX0PQq+l844fFAxOULtBy8BVF35GyeueX0r4KvDthqPH5xAI1clQPk/2uA==}
engines: {node: ^22.18.0 || >=24.11.0}
- '@babel/parser@8.0.0':
- resolution: {integrity: sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==}
- engines: {node: ^22.18.0 || >=24.11.0}
- hasBin: true
-
- '@babel/parser@8.0.4':
- resolution: {integrity: sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==}
+ '@babel/parser@8.0.5':
+ resolution: {integrity: sha512-51RXvQNFakaS0bTpYiGkxNbUVwkPO4kONv6EVLorZABxsx+KZ6Z7uSYvi/wmKS/+X+rfj9RvOw0/ZNh+cmI0Rw==}
engines: {node: ^22.18.0 || >=24.11.0}
hasBin: true
@@ -248,8 +243,8 @@ packages:
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@8.0.1':
- resolution: {integrity: sha512-DmR/N+B9+4PbURFj4+zdnWj49/PFAnK2bn8+E4ZAmwn3J5QCxnbG7Ep6aRfz9M8Aw+rBro0kIJQycvzFpl4buQ==}
+ '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@8.0.5':
+ resolution: {integrity: sha512-e5eeoBASO+893sCNdFE/vQ9STIxkSRtIcQwBmLdfDOWn1nnXDY+/ngqFC5zTSUTRl0yWR3inkPRlOQD4MMHeFg==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
@@ -290,8 +285,8 @@ packages:
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/plugin-transform-block-scoping@8.0.1':
- resolution: {integrity: sha512-HxiQvKsSCs2jOmMhjDrooHaZYOy6W8bqwXp/zjdgPjsNrda6tK9/CH3a/cVIeg6ge3hSS02ALqvqgIo4rTsuSg==}
+ '@babel/plugin-transform-block-scoping@8.0.5':
+ resolution: {integrity: sha512-czkN3g76Ns2wMe1C0IuceKUCqCe/yJ1by0qz4h4znq74K/Ks7K2Fx6Dplq9ecS65Z3rJHxT9a/kDSF2LbEov8w==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
@@ -302,14 +297,14 @@ packages:
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/plugin-transform-class-static-block@8.0.1':
- resolution: {integrity: sha512-NEVK+L0Le8h8tJ+IK0CGS5y9Yi1ZHxLj6M5PeanhMFuq9aSo0XI+Wtmbuyop6fTNukOm7ORNntf/kwid891vqQ==}
+ '@babel/plugin-transform-class-static-block@8.0.5':
+ resolution: {integrity: sha512-hyqluKOSy6PVOoHDEb4xsjFbtOSy+FFNzHNCrAHS5T+LxS8BtZHBCIVYFAcWzhBf/wRYsYu2uEpUvRmHYeOf8A==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/plugin-transform-classes@8.0.1':
- resolution: {integrity: sha512-phwyCES8kIMAdVOFw25ztmgAvkl2G+TvUv7azUYyrlR1Qoo3eLJC/MU3MGUKFZ4BWtsJ1NTJM1lKRLzKbswg7w==}
+ '@babel/plugin-transform-classes@8.0.5':
+ resolution: {integrity: sha512-GKFgovQQzZfEDFxclYFDOiw/Pszxa1epcs7RasL7yyeqi6rxL61Vbqt0uSDPc2M7XXCmNZnQZaK19333v6rabA==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
@@ -320,8 +315,8 @@ packages:
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/plugin-transform-destructuring@8.0.1':
- resolution: {integrity: sha512-RtR8uLDl0QcCmqMNIkM8gmDeYZ3rS0ZH+sa+I6sfc09yFoqfp9AEPgBstq9KyfVb0lFCVSRFfJXCI70FIl5ccw==}
+ '@babel/plugin-transform-destructuring@8.0.5':
+ resolution: {integrity: sha512-evjxc5fGvpXG2WMGSalpX8IJZVumLIDgk5r5eRhvXRDqFX/GPWq1BeRfJalPCcwJfrR5A/wvegUhx9W5Nr7/mg==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
@@ -350,8 +345,8 @@ packages:
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/plugin-transform-explicit-resource-management@8.0.1':
- resolution: {integrity: sha512-VzDIYwBlLCpV6mJfloRdJm8HmYnMqs7O+bGha8yfg2kP7jAdxeCw6yZBVBeaKKQUThtSU52iy+3lB7DhYsbOBA==}
+ '@babel/plugin-transform-explicit-resource-management@8.0.5':
+ resolution: {integrity: sha512-Vr20jk/ZxRGu30o0jYvcKl2Pf0tiyr8H9q2ZGxYD6yjOpv9DdP6//I85q0cSOdADZhsu/ZZoApjGVCfl4xnVJw==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
@@ -470,14 +465,14 @@ packages:
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/plugin-transform-optional-chaining@8.0.1':
- resolution: {integrity: sha512-WtRS1c94lZGpGHxYLXMEWeoMVcuv8nkiyr8BTs6OYZv7N3Y9xVE8nbdFIl4lDJH6aH8/pLhqAQOL69d/WI9WdA==}
+ '@babel/plugin-transform-optional-chaining@8.0.5':
+ resolution: {integrity: sha512-w8ouIDKdmUc2oyvBx4S/WtiQM7bMww5LGjB2A4L1+sDWZGw37XxzqOqfgHnPm+2ldINmAqrf2ekzpeJ/sS9poQ==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/plugin-transform-parameters@8.0.1':
- resolution: {integrity: sha512-IIwRqroW0CYQwR6+3pnmu27z+H98poScWdnov8z6osumMeEsFxAFBBsDS2CFk2jFpPlGqVr89jK/HXO6i5DzxQ==}
+ '@babel/plugin-transform-parameters@8.0.5':
+ resolution: {integrity: sha512-oXsstDkzuE+gOqBz2+e1VFqDew0KlGgd9GT4MGptmcIq3uDbxcowHRSaqMCvp3nbiZws4wTm8Phuk9ZJO3NtTg==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
@@ -488,8 +483,8 @@ packages:
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/plugin-transform-private-property-in-object@8.0.1':
- resolution: {integrity: sha512-e+yfOqSYBZaf3PARpiQkjZrpWYgmcFLhK+1tevh2CpHR1O9/36IdyPnAZusESX5nzVV/XZTDAtQBRLa8HPT5Dw==}
+ '@babel/plugin-transform-private-property-in-object@8.0.5':
+ resolution: {integrity: sha512-OilYoElZMdD/My3fJTh0jLOPy8Y9Jfpcb9nPXG0Bpy4oRbZeeVX39P1cXKsPwnXTBZg7qEKk8TQ0lzpJvdK3yw==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
@@ -500,8 +495,8 @@ packages:
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/plugin-transform-regenerator@8.0.2':
- resolution: {integrity: sha512-aFfsjCRYducRV4dPnpsBbdRkLjboca9FVDg6HZCgy0Ahvk2ZQ/2exmCRC5qS9P6rsWwrmIheNaIM6A1j2F8KMA==}
+ '@babel/plugin-transform-regenerator@8.0.5':
+ resolution: {integrity: sha512-eSjLoICd0l8SkUAyV7hnVrsDycTVK7DRKXi3Pr1oqAVe37vdd4sesp8t6aZ/Wa23rV2ZQbnYJWDn7pOIf9Z/Jw==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
@@ -530,8 +525,8 @@ packages:
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/plugin-transform-spread@8.0.1':
- resolution: {integrity: sha512-O9Bw9FyxlSw1SlMg3S82/GKNZ0x77RPbHezotEy1JTlIM/vk6WO8jW1iF+iTiKLOXNvi+b+LZ9t77Gi+Q0FhGg==}
+ '@babel/plugin-transform-spread@8.0.5':
+ resolution: {integrity: sha512-thqaGFTViCmJxUPJ3IivfaGHz8eodOL7UbsZ2gMOybSi8uQFXFEoabnnILMl4ZCok0vVrKD4HKq9MJWSj5qqxw==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
@@ -578,8 +573,8 @@ packages:
peerDependencies:
'@babel/core': ^8.0.0
- '@babel/preset-env@8.0.2':
- resolution: {integrity: sha512-CUGLn9hNBCF/eXnwdFAWERbniCcXCRvnKwLV9fegeUEIqv7YlU2MepsWMMM54GcILx5XYMnRh+JAL+K5G+mK6g==}
+ '@babel/preset-env@8.0.5':
+ resolution: {integrity: sha512-0ZiW2jOnEQ3+FV3ro3uh6B5afBT0C2yJHCKvw+s1BOKKXg7VfCvTpGBylN5UFeO2SFtkVJB2KY0tEpdnUjjguw==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
'@babel/core': ^8.0.0
@@ -597,16 +592,16 @@ packages:
resolution: {integrity: sha512-bxTj/W2VclGE6CctlfQOpxg8MPDzXArRqkOBePw8EHfebcjF7fETWSS3BriEECo+UiU/Yblq+xUtSImFu7cTbw==}
engines: {node: ^22.18.0 || >=24.11.0}
- '@babel/traverse@8.0.4':
- resolution: {integrity: sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==}
+ '@babel/traverse@8.0.5':
+ resolution: {integrity: sha512-XFfnuvapSc/vJOcUO7kwORSvpBIvraofKEZ2dhT0PjiF21BRCD7YbAFC8UEeDJNeLoQz82/gVqzgX5hCzkCbdg==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/types@8.0.0':
resolution: {integrity: sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==}
engines: {node: ^22.18.0 || >=24.11.0}
- '@babel/types@8.0.4':
- resolution: {integrity: sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==}
+ '@babel/types@8.0.5':
+ resolution: {integrity: sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==}
engines: {node: ^22.18.0 || >=24.11.0}
'@cacheable/memory@2.2.0':
@@ -728,6 +723,9 @@ packages:
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+ '@jridgewell/gen-mapping@0.4.0-beta.0':
+ resolution: {integrity: sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==}
+
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
@@ -738,6 +736,9 @@ packages:
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+ '@jridgewell/sourcemap-codec@1.6.0':
+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
+
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
@@ -936,11 +937,6 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
- baseline-browser-mapping@2.11.12:
- resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==}
- engines: {node: '>=6.0.0'}
- hasBin: true
-
baseline-browser-mapping@2.11.21:
resolution: {integrity: sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==}
engines: {node: '>=6.0.0'}
@@ -960,16 +956,6 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
- browserslist@4.28.7:
- resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==}
- engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
- hasBin: true
-
- browserslist@4.28.8:
- resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==}
- engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
- hasBin: true
-
browserslist@4.28.9:
resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -988,9 +974,6 @@ packages:
caniuse-api@3.0.0:
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
- caniuse-lite@1.0.30001806:
- resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==}
-
caniuse-lite@1.0.30001810:
resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==}
@@ -1044,8 +1027,9 @@ packages:
peerDependencies:
webpack: ^5.1.0
- core-js-compat@3.49.0:
- resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==}
+ core-js-compat@3.50.0:
+ resolution: {integrity: sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==}
+ engines: {node: '>=6.4.0'}
cosmiconfig@9.0.2:
resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==}
@@ -1174,12 +1158,6 @@ packages:
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
- electron-to-chromium@1.5.401:
- resolution: {integrity: sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ==}
-
- electron-to-chromium@1.5.420:
- resolution: {integrity: sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==}
-
electron-to-chromium@1.5.422:
resolution: {integrity: sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==}
@@ -1723,10 +1701,6 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
- node-releases@2.0.52:
- resolution: {integrity: sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==}
- engines: {node: '>=18'}
-
node-releases@2.0.54:
resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==}
engines: {node: '>=18'}
@@ -1738,12 +1712,8 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
- obug@2.1.3:
- resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
- engines: {node: '>=12.20.0'}
-
- obug@2.1.4:
- resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
+ obug@2.2.1:
+ resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
engines: {node: '>=12.20.0'}
optionator@0.9.4:
@@ -2326,12 +2296,6 @@ packages:
resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==}
engines: {node: '>=20'}
- update-browserslist-db@1.2.3:
- resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
- hasBin: true
- peerDependencies:
- browserslist: '>= 4.21.0'
-
update-browserslist-db@1.3.2:
resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==}
hasBin: true
@@ -2354,6 +2318,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+ verkit@0.3.2:
+ resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==}
+ engines: {node: '>=18.12.0'}
+
watchpack@2.5.2:
resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==}
engines: {node: '>=10.13.0'}
@@ -2449,129 +2417,129 @@ snapshots:
'@babel/code-frame@8.0.0':
dependencies:
- '@babel/helper-validator-identifier': 8.0.2
+ '@babel/helper-validator-identifier': 8.0.4
js-tokens: 10.0.0
- '@babel/compat-data@8.0.0': {}
+ '@babel/compat-data@8.0.5': {}
- '@babel/core@8.0.1':
+ '@babel/core@8.0.5':
dependencies:
'@babel/code-frame': 8.0.0
- '@babel/generator': 8.0.0
- '@babel/helper-compilation-targets': 8.0.0
- '@babel/helpers': 8.0.0
- '@babel/parser': 8.0.0
+ '@babel/generator': 8.0.5
+ '@babel/helper-compilation-targets': 8.0.5
+ '@babel/helpers': 8.0.5
+ '@babel/parser': 8.0.5
'@babel/template': 8.0.0
- '@babel/traverse': 8.0.0
- '@babel/types': 8.0.0
+ '@babel/traverse': 8.0.5
+ '@babel/types': 8.0.5
'@types/gensync': 1.0.5
convert-source-map: 2.0.0
empathic: 2.0.1
gensync: 1.0.0-beta.2
import-meta-resolve: 4.2.0
json5: 2.2.3
- obug: 2.1.3
- semver: 7.8.5
+ obug: 2.2.1
+ verkit: 0.3.2
- '@babel/eslint-parser@8.0.1(@babel/core@8.0.1)(eslint@10.10.0)':
+ '@babel/eslint-parser@8.0.5(@babel/core@8.0.5)(eslint@10.10.0)':
dependencies:
- '@babel/core': 8.0.1
+ '@babel/core': 8.0.5
eslint: 10.10.0
eslint-scope: 9.1.2
eslint-visitor-keys: 5.0.1
- semver: 7.8.5
+ verkit: 0.3.2
- '@babel/generator@8.0.0':
+ '@babel/generator@8.0.5':
dependencies:
- '@babel/parser': 8.0.0
- '@babel/types': 8.0.0
- '@jridgewell/gen-mapping': 0.3.13
+ '@babel/parser': 8.0.5
+ '@babel/types': 8.0.5
+ '@jridgewell/gen-mapping': 0.4.0-beta.0
'@jridgewell/trace-mapping': 0.3.31
'@types/jsesc': 2.5.1
jsesc: 3.1.0
'@babel/helper-annotate-as-pure@8.0.0':
dependencies:
- '@babel/types': 8.0.4
+ '@babel/types': 8.0.5
- '@babel/helper-compilation-targets@8.0.0':
+ '@babel/helper-compilation-targets@8.0.5':
dependencies:
- '@babel/compat-data': 8.0.0
+ '@babel/compat-data': 8.0.5
'@babel/helper-validator-option': 8.0.0
- browserslist: 4.28.7
+ browserslist: 4.28.9
lru-cache: 11.5.2
- semver: 7.8.5
+ verkit: 0.3.2
- '@babel/helper-create-class-features-plugin@8.0.1(@babel/core@8.0.1)':
+ '@babel/helper-create-class-features-plugin@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
+ '@babel/core': 8.0.5
'@babel/helper-annotate-as-pure': 8.0.0
- '@babel/helper-member-expression-to-functions': 8.0.0
+ '@babel/helper-member-expression-to-functions': 8.0.5
'@babel/helper-optimise-call-expression': 8.0.0
- '@babel/helper-replace-supers': 8.0.1(@babel/core@8.0.1)
+ '@babel/helper-replace-supers': 8.0.1(@babel/core@8.0.5)
'@babel/helper-skip-transparent-expression-wrappers': 8.0.0
- '@babel/traverse': 8.0.4
- semver: 7.8.5
+ '@babel/traverse': 8.0.5
+ verkit: 0.3.2
- '@babel/helper-create-regexp-features-plugin@8.0.1(@babel/core@8.0.1)':
+ '@babel/helper-create-regexp-features-plugin@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
+ '@babel/core': 8.0.5
'@babel/helper-annotate-as-pure': 8.0.0
regexpu-core: 6.4.0
- semver: 7.8.5
+ verkit: 0.3.2
- '@babel/helper-define-polyfill-provider@1.0.0(@babel/core@8.0.1)':
+ '@babel/helper-define-polyfill-provider@1.0.0(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-compilation-targets': 8.0.0
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-compilation-targets': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
lodash.debounce: 4.0.8
'@babel/helper-globals@8.0.0': {}
- '@babel/helper-member-expression-to-functions@8.0.0':
+ '@babel/helper-member-expression-to-functions@8.0.5':
dependencies:
- '@babel/traverse': 8.0.4
- '@babel/types': 8.0.4
+ '@babel/traverse': 8.0.5
+ '@babel/types': 8.0.5
'@babel/helper-module-imports@8.0.0':
dependencies:
'@babel/traverse': 8.0.0
'@babel/types': 8.0.0
- '@babel/helper-module-transforms@8.0.1(@babel/core@8.0.1)':
+ '@babel/helper-module-transforms@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
+ '@babel/core': 8.0.5
'@babel/helper-module-imports': 8.0.0
'@babel/helper-validator-identifier': 8.0.4
- '@babel/traverse': 8.0.4
+ '@babel/traverse': 8.0.5
'@babel/helper-optimise-call-expression@8.0.0':
dependencies:
- '@babel/types': 8.0.4
+ '@babel/types': 8.0.5
- '@babel/helper-plugin-utils@8.0.1(@babel/core@8.0.1)':
+ '@babel/helper-plugin-utils@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
+ '@babel/core': 8.0.5
- '@babel/helper-remap-async-to-generator@8.0.1(@babel/core@8.0.1)':
+ '@babel/helper-remap-async-to-generator@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
+ '@babel/core': 8.0.5
'@babel/helper-annotate-as-pure': 8.0.0
'@babel/helper-wrap-function': 8.0.0
- '@babel/traverse': 8.0.4
+ '@babel/traverse': 8.0.5
- '@babel/helper-replace-supers@8.0.1(@babel/core@8.0.1)':
+ '@babel/helper-replace-supers@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-member-expression-to-functions': 8.0.0
+ '@babel/core': 8.0.5
+ '@babel/helper-member-expression-to-functions': 8.0.5
'@babel/helper-optimise-call-expression': 8.0.0
- '@babel/traverse': 8.0.4
+ '@babel/traverse': 8.0.5
'@babel/helper-skip-transparent-expression-wrappers@8.0.0':
dependencies:
- '@babel/traverse': 8.0.4
- '@babel/types': 8.0.4
+ '@babel/traverse': 8.0.5
+ '@babel/types': 8.0.5
'@babel/helper-string-parser@8.0.0': {}
@@ -2586,461 +2554,457 @@ snapshots:
'@babel/helper-wrap-function@8.0.0':
dependencies:
'@babel/template': 8.0.0
- '@babel/traverse': 8.0.4
- '@babel/types': 8.0.4
+ '@babel/traverse': 8.0.5
+ '@babel/types': 8.0.5
- '@babel/helpers@8.0.0':
+ '@babel/helpers@8.0.5':
dependencies:
'@babel/template': 8.0.0
- '@babel/types': 8.0.0
-
- '@babel/parser@8.0.0':
- dependencies:
- '@babel/types': 8.0.0
+ '@babel/types': 8.0.5
- '@babel/parser@8.0.4':
+ '@babel/parser@8.0.5':
dependencies:
- '@babel/types': 8.0.4
+ '@babel/types': 8.0.5
- '@babel/plugin-bugfix-firefox-class-in-computed-class-key@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-bugfix-safari-class-field-initializer-scope@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
'@babel/helper-skip-transparent-expression-wrappers': 8.0.0
- '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
'@babel/helper-skip-transparent-expression-wrappers': 8.0.0
- '@babel/plugin-transform-optional-chaining': 8.0.1(@babel/core@8.0.1)
+ '@babel/plugin-transform-optional-chaining': 8.0.5(@babel/core@8.0.5)
- '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-arrow-functions@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-arrow-functions@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-async-generator-functions@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-async-generator-functions@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-remap-async-to-generator': 8.0.1(@babel/core@8.0.1)
- '@babel/traverse': 8.0.4
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
+ '@babel/helper-remap-async-to-generator': 8.0.1(@babel/core@8.0.5)
+ '@babel/traverse': 8.0.5
- '@babel/plugin-transform-async-to-generator@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-async-to-generator@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
+ '@babel/core': 8.0.5
'@babel/helper-module-imports': 8.0.0
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-remap-async-to-generator': 8.0.1(@babel/core@8.0.1)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
+ '@babel/helper-remap-async-to-generator': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-block-scoped-functions@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-block-scoped-functions@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-block-scoping@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-block-scoping@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-class-properties@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-class-properties@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-create-class-features-plugin': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-create-class-features-plugin': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-class-static-block@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-class-static-block@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-create-class-features-plugin': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-create-class-features-plugin': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-classes@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-classes@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
+ '@babel/core': 8.0.5
'@babel/helper-annotate-as-pure': 8.0.0
- '@babel/helper-compilation-targets': 8.0.0
+ '@babel/helper-compilation-targets': 8.0.5
'@babel/helper-globals': 8.0.0
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-replace-supers': 8.0.1(@babel/core@8.0.1)
- '@babel/traverse': 8.0.4
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
+ '@babel/helper-replace-supers': 8.0.1(@babel/core@8.0.5)
+ '@babel/traverse': 8.0.5
- '@babel/plugin-transform-computed-properties@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-computed-properties@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-destructuring@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-destructuring@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-dotall-regex@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-dotall-regex@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-create-regexp-features-plugin': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-create-regexp-features-plugin': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-duplicate-keys@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-duplicate-keys@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-duplicate-named-capturing-groups-regex@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-create-regexp-features-plugin': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-create-regexp-features-plugin': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-dynamic-import@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-dynamic-import@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-explicit-resource-management@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-explicit-resource-management@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-destructuring': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-destructuring': 8.0.5(@babel/core@8.0.5)
- '@babel/plugin-transform-exponentiation-operator@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-exponentiation-operator@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-export-namespace-from@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-export-namespace-from@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-for-of@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-for-of@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
'@babel/helper-skip-transparent-expression-wrappers': 8.0.0
- '@babel/plugin-transform-function-name@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-function-name@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-compilation-targets': 8.0.0
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-compilation-targets': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-json-strings@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-json-strings@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-literals@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-literals@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-logical-assignment-operators@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-logical-assignment-operators@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-member-expression-literals@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-member-expression-literals@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-modules-amd@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-modules-amd@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-module-transforms': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-module-transforms': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-modules-commonjs@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-modules-commonjs@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-module-transforms': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-module-transforms': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-modules-systemjs@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-modules-systemjs@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-module-transforms': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-module-transforms': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
'@babel/helper-validator-identifier': 8.0.4
- '@babel/plugin-transform-modules-umd@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-modules-umd@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-module-transforms': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-module-transforms': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-named-capturing-groups-regex@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-named-capturing-groups-regex@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-create-regexp-features-plugin': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-create-regexp-features-plugin': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-new-target@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-new-target@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-nullish-coalescing-operator@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-nullish-coalescing-operator@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-numeric-separator@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-numeric-separator@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-object-rest-spread@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-object-rest-spread@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-compilation-targets': 8.0.0
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-destructuring': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-parameters': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-compilation-targets': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-destructuring': 8.0.5(@babel/core@8.0.5)
+ '@babel/plugin-transform-parameters': 8.0.5(@babel/core@8.0.5)
- '@babel/plugin-transform-object-super@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-object-super@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-replace-supers': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
+ '@babel/helper-replace-supers': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-optional-catch-binding@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-optional-catch-binding@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-optional-chaining@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-optional-chaining@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
'@babel/helper-skip-transparent-expression-wrappers': 8.0.0
- '@babel/plugin-transform-parameters@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-parameters@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-private-methods@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-private-methods@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-create-class-features-plugin': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-create-class-features-plugin': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-private-property-in-object@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-private-property-in-object@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
+ '@babel/core': 8.0.5
'@babel/helper-annotate-as-pure': 8.0.0
- '@babel/helper-create-class-features-plugin': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/helper-create-class-features-plugin': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-property-literals@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-property-literals@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-regenerator@8.0.2(@babel/core@8.0.1)':
+ '@babel/plugin-transform-regenerator@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-regexp-modifiers@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-regexp-modifiers@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-create-regexp-features-plugin': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-create-regexp-features-plugin': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-reserved-words@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-reserved-words@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-runtime@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-runtime@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
+ '@babel/core': 8.0.5
'@babel/helper-module-imports': 8.0.0
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-shorthand-properties@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-shorthand-properties@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-spread@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-spread@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
'@babel/helper-skip-transparent-expression-wrappers': 8.0.0
- '@babel/plugin-transform-sticky-regex@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-sticky-regex@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-template-literals@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-template-literals@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-typeof-symbol@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-typeof-symbol@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-unicode-escapes@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-unicode-escapes@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-unicode-property-regex@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-unicode-property-regex@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-create-regexp-features-plugin': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-create-regexp-features-plugin': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-unicode-regex@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-unicode-regex@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-create-regexp-features-plugin': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-create-regexp-features-plugin': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/plugin-transform-unicode-sets-regex@8.0.1(@babel/core@8.0.1)':
+ '@babel/plugin-transform-unicode-sets-regex@8.0.1(@babel/core@8.0.5)':
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-create-regexp-features-plugin': 8.0.1(@babel/core@8.0.1)
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/core': 8.0.5
+ '@babel/helper-create-regexp-features-plugin': 8.0.5(@babel/core@8.0.5)
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
- '@babel/preset-env@8.0.2(@babel/core@8.0.1)':
+ '@babel/preset-env@8.0.5(@babel/core@8.0.5)':
dependencies:
- '@babel/compat-data': 8.0.0
- '@babel/core': 8.0.1
- '@babel/helper-compilation-targets': 8.0.0
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
+ '@babel/compat-data': 8.0.5
+ '@babel/core': 8.0.5
+ '@babel/helper-compilation-targets': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
'@babel/helper-validator-option': 8.0.0
- '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-bugfix-safari-class-field-initializer-scope': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-arrow-functions': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-async-generator-functions': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-async-to-generator': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-block-scoped-functions': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-block-scoping': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-class-properties': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-class-static-block': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-classes': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-computed-properties': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-destructuring': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-dotall-regex': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-duplicate-keys': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-dynamic-import': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-explicit-resource-management': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-exponentiation-operator': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-export-namespace-from': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-for-of': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-function-name': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-json-strings': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-literals': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-logical-assignment-operators': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-member-expression-literals': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-modules-amd': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-modules-commonjs': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-modules-systemjs': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-modules-umd': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-named-capturing-groups-regex': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-new-target': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-nullish-coalescing-operator': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-numeric-separator': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-object-rest-spread': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-object-super': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-optional-catch-binding': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-optional-chaining': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-parameters': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-private-methods': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-private-property-in-object': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-property-literals': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-regenerator': 8.0.2(@babel/core@8.0.1)
- '@babel/plugin-transform-regexp-modifiers': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-reserved-words': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-shorthand-properties': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-spread': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-sticky-regex': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-template-literals': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-typeof-symbol': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-unicode-escapes': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-unicode-property-regex': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-unicode-regex': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-unicode-sets-regex': 8.0.1(@babel/core@8.0.1)
- '@babel/preset-modules': 0.2.0(@babel/core@8.0.1)
- babel-plugin-polyfill-corejs3: 1.0.0(@babel/core@8.0.1)
- core-js-compat: 3.49.0
- semver: 7.8.5
-
- '@babel/preset-modules@0.2.0(@babel/core@8.0.1)':
- dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-dotall-regex': 8.0.1(@babel/core@8.0.1)
- '@babel/plugin-transform-unicode-property-regex': 8.0.1(@babel/core@8.0.1)
- '@babel/types': 8.0.4
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 8.0.5(@babel/core@8.0.5)
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-arrow-functions': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-async-generator-functions': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-async-to-generator': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-block-scoped-functions': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-block-scoping': 8.0.5(@babel/core@8.0.5)
+ '@babel/plugin-transform-class-properties': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-class-static-block': 8.0.5(@babel/core@8.0.5)
+ '@babel/plugin-transform-classes': 8.0.5(@babel/core@8.0.5)
+ '@babel/plugin-transform-computed-properties': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-destructuring': 8.0.5(@babel/core@8.0.5)
+ '@babel/plugin-transform-dotall-regex': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-duplicate-keys': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-dynamic-import': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-explicit-resource-management': 8.0.5(@babel/core@8.0.5)
+ '@babel/plugin-transform-exponentiation-operator': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-export-namespace-from': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-for-of': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-function-name': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-json-strings': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-literals': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-logical-assignment-operators': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-member-expression-literals': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-modules-amd': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-modules-commonjs': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-modules-systemjs': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-modules-umd': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-named-capturing-groups-regex': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-new-target': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-nullish-coalescing-operator': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-numeric-separator': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-object-rest-spread': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-object-super': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-optional-catch-binding': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-optional-chaining': 8.0.5(@babel/core@8.0.5)
+ '@babel/plugin-transform-parameters': 8.0.5(@babel/core@8.0.5)
+ '@babel/plugin-transform-private-methods': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-private-property-in-object': 8.0.5(@babel/core@8.0.5)
+ '@babel/plugin-transform-property-literals': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-regenerator': 8.0.5(@babel/core@8.0.5)
+ '@babel/plugin-transform-regexp-modifiers': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-reserved-words': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-shorthand-properties': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-spread': 8.0.5(@babel/core@8.0.5)
+ '@babel/plugin-transform-sticky-regex': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-template-literals': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-typeof-symbol': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-unicode-escapes': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-unicode-property-regex': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-unicode-regex': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-unicode-sets-regex': 8.0.1(@babel/core@8.0.5)
+ '@babel/preset-modules': 0.2.0(@babel/core@8.0.5)
+ babel-plugin-polyfill-corejs3: 1.0.0(@babel/core@8.0.5)
+ core-js-compat: 3.50.0
+ verkit: 0.3.2
+
+ '@babel/preset-modules@0.2.0(@babel/core@8.0.5)':
+ dependencies:
+ '@babel/core': 8.0.5
+ '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-dotall-regex': 8.0.1(@babel/core@8.0.5)
+ '@babel/plugin-transform-unicode-property-regex': 8.0.1(@babel/core@8.0.5)
+ '@babel/types': 8.0.5
esutils: 2.0.3
'@babel/template@8.0.0':
dependencies:
'@babel/code-frame': 8.0.0
- '@babel/parser': 8.0.0
- '@babel/types': 8.0.0
+ '@babel/parser': 8.0.5
+ '@babel/types': 8.0.5
'@babel/traverse@8.0.0':
dependencies:
'@babel/code-frame': 8.0.0
- '@babel/generator': 8.0.0
+ '@babel/generator': 8.0.5
'@babel/helper-globals': 8.0.0
- '@babel/parser': 8.0.0
+ '@babel/parser': 8.0.5
'@babel/template': 8.0.0
'@babel/types': 8.0.0
- obug: 2.1.3
+ obug: 2.2.1
- '@babel/traverse@8.0.4':
+ '@babel/traverse@8.0.5':
dependencies:
'@babel/code-frame': 8.0.0
- '@babel/generator': 8.0.0
+ '@babel/generator': 8.0.5
'@babel/helper-globals': 8.0.0
- '@babel/parser': 8.0.4
+ '@babel/parser': 8.0.5
'@babel/template': 8.0.0
- '@babel/types': 8.0.4
- obug: 2.1.4
+ '@babel/types': 8.0.5
+ obug: 2.2.1
'@babel/types@8.0.0':
dependencies:
'@babel/helper-string-parser': 8.0.0
'@babel/helper-validator-identifier': 8.0.2
- '@babel/types@8.0.4':
+ '@babel/types@8.0.5':
dependencies:
'@babel/helper-string-parser': 8.0.0
'@babel/helper-validator-identifier': 8.0.4
@@ -3154,7 +3118,12 @@ snapshots:
'@jridgewell/gen-mapping@0.3.13':
dependencies:
- '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/sourcemap-codec': 1.6.0
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/gen-mapping@0.4.0-beta.0':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.6.0
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
@@ -3166,6 +3135,8 @@ snapshots:
'@jridgewell/sourcemap-codec@1.5.5': {}
+ '@jridgewell/sourcemap-codec@1.6.0': {}
+
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
@@ -3356,23 +3327,21 @@ snapshots:
astral-regex@2.0.0: {}
- babel-loader@10.1.1(@babel/core@8.0.1)(webpack@5.110.3):
+ babel-loader@10.1.1(@babel/core@8.0.5)(webpack@5.110.3):
dependencies:
- '@babel/core': 8.0.1
+ '@babel/core': 8.0.5
find-up: 5.0.0
optionalDependencies:
webpack: 5.110.3(postcss@8.5.28)(webpack-cli@7.2.3)
- babel-plugin-polyfill-corejs3@1.0.0(@babel/core@8.0.1):
+ babel-plugin-polyfill-corejs3@1.0.0(@babel/core@8.0.5):
dependencies:
- '@babel/core': 8.0.1
- '@babel/helper-define-polyfill-provider': 1.0.0(@babel/core@8.0.1)
- core-js-compat: 3.49.0
+ '@babel/core': 8.0.5
+ '@babel/helper-define-polyfill-provider': 1.0.0(@babel/core@8.0.5)
+ core-js-compat: 3.50.0
balanced-match@4.0.4: {}
- baseline-browser-mapping@2.11.12: {}
-
baseline-browser-mapping@2.11.21: {}
big.js@5.2.2: {}
@@ -3387,22 +3356,6 @@ snapshots:
dependencies:
fill-range: 7.1.1
- browserslist@4.28.7:
- dependencies:
- baseline-browser-mapping: 2.11.12
- caniuse-lite: 1.0.30001806
- electron-to-chromium: 1.5.401
- node-releases: 2.0.52
- update-browserslist-db: 1.2.3(browserslist@4.28.7)
-
- browserslist@4.28.8:
- dependencies:
- baseline-browser-mapping: 2.11.12
- caniuse-lite: 1.0.30001810
- electron-to-chromium: 1.5.420
- node-releases: 2.0.54
- update-browserslist-db: 1.3.2(browserslist@4.28.8)
-
browserslist@4.28.9:
dependencies:
baseline-browser-mapping: 2.11.21
@@ -3430,8 +3383,6 @@ snapshots:
lodash.memoize: 4.1.2
lodash.uniq: 4.5.0
- caniuse-lite@1.0.30001806: {}
-
caniuse-lite@1.0.30001810: {}
chalk@4.1.2:
@@ -3476,9 +3427,9 @@ snapshots:
tinyglobby: 0.2.15
webpack: 5.110.3(postcss@8.5.28)(webpack-cli@7.2.3)
- core-js-compat@3.49.0:
+ core-js-compat@3.50.0:
dependencies:
- browserslist: 4.28.8
+ browserslist: 4.28.9
cosmiconfig@9.0.2:
dependencies:
@@ -3616,10 +3567,6 @@ snapshots:
domelementtype: 2.3.0
domhandler: 5.0.3
- electron-to-chromium@1.5.401: {}
-
- electron-to-chromium@1.5.420: {}
-
electron-to-chromium@1.5.422: {}
emoji-regex@8.0.0: {}
@@ -4055,8 +4002,6 @@ snapshots:
neo-async@2.6.2: {}
- node-releases@2.0.52: {}
-
node-releases@2.0.54: {}
normalize-path@3.0.0: {}
@@ -4065,9 +4010,7 @@ snapshots:
dependencies:
boolbase: 1.0.0
- obug@2.1.3: {}
-
- obug@2.1.4: {}
+ obug@2.2.1: {}
optionator@0.9.4:
dependencies:
@@ -4615,18 +4558,6 @@ snapshots:
unicorn-magic@0.4.0: {}
- update-browserslist-db@1.2.3(browserslist@4.28.7):
- dependencies:
- browserslist: 4.28.7
- escalade: 3.2.0
- picocolors: 1.1.1
-
- update-browserslist-db@1.3.2(browserslist@4.28.8):
- dependencies:
- browserslist: 4.28.8
- escalade: 3.2.0
- picocolors: 1.1.1
-
update-browserslist-db@1.3.2(browserslist@4.28.9):
dependencies:
browserslist: 4.28.9
@@ -4648,6 +4579,8 @@ snapshots:
util-deprecate@1.0.2: {}
+ verkit@0.3.2: {}
+
watchpack@2.5.2:
dependencies:
graceful-fs: 4.2.11
(airflow) 02/09: [v3-3-test] Keep the Gradle wrapper jar out of the source release (#69444) (#73108)
Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 9dd0068ca340bdc12f39d87e60f51402ff7f67ae
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Mon Sep 14 14:11:38 2026 +0630
[v3-3-test] Keep the Gradle wrapper jar out of the source release (#69444) (#73108)
ASF policy does not permit compiled binaries in a source release and the
Gradle wrapper is not among the exempted build tools (LEGAL-570), so main
stopped shipping java-sdk/gradle/wrapper/gradle-wrapper.jar in #69444.
Only half of that change reached this branch. The breeze side already
restores gradlew and gradlew.bat after `git archive` drops them, but the
java-sdk/.gitattributes side never followed, so nothing is actually dropped
and the 43 KB jar is still in the 3.3.2rc1 source tarball.
Carrying the rest of the file over also starts shipping java-sdk/.editorconfig,
which the root .gitattributes strips today even though ktlint reads it at
build time and the build task requires that lint to pass.
gradle-wrapper.properties stays in the tarball on purpose: it carries the
pinned Gradle version and distribution checksum a verifier needs to
regenerate the wrapper.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01DCUVuZ8CCeER1QLhVEKAaZ
---
java-sdk/.gitattributes | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/java-sdk/.gitattributes b/java-sdk/.gitattributes
index a87d264c425..04a9c526a5e 100644
--- a/java-sdk/.gitattributes
+++ b/java-sdk/.gitattributes
@@ -9,3 +9,16 @@
# Binary files should be left untouched
*.jar binary
+
+# Keep the Gradle wrapper jar and scripts out of source releases.
+# See: https://issues.apache.org/jira/browse/LEGAL-570
+# This intentionally does not exclude the entire gradle/wrapper directory so
+# gradle-wrapper.properties is kept. This file carries the pinned Gradle version
+# and distribution checksum for a verifier to use when regenerating the wrapper.
+/gradlew export-ignore
+/gradlew.bat export-ignore
+/gradle/wrapper/gradle-wrapper.jar export-ignore
+
+# ktlint reads .editorconfig for its formatting rules at build time, and the
+# build task requires the lint to pass, so the source release must ship it.
+/.editorconfig -export-ignore
(airflow) 03/09: Authorize POST /assets/events on the asset named in the body (#73007) (#73139)
Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 1c0d24cede6e7862a5c5f6e8ba8013d99e6a7ef9
Author: Vincent <97...@users.noreply.github.com>
AuthorDate: Mon Sep 14 14:52:51 2026 -0400
Authorize POST /assets/events on the asset named in the body (#73007) (#73139)
The route dependency reads the asset id from the path, but this endpoint
carries it in the request body, so the auth manager was only asked whether
the caller may post to any asset at all. An auth manager that scopes assets
by id, name, or uri could not deny an event for an asset the caller may not
touch, and the response still returned that asset's name and uri.
Co-authored-by: Henry Chen <he...@apache.org>
---
.../api_fastapi/core_api/routes/public/assets.py | 5 ++-
.../src/airflow/api_fastapi/core_api/security.py | 32 ++++++++++++---
.../core_api/routes/public/test_assets.py | 47 ++++++++++++++++++++--
3 files changed, 74 insertions(+), 10 deletions(-)
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py
index fd359fd86bc..de780f24dba 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py
@@ -366,7 +366,10 @@ def get_asset_events(
@assets_router.post(
"/assets/events",
responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]),
- dependencies=[Depends(requires_access_asset(method="POST")), Depends(action_logging())],
+ dependencies=[
+ Depends(requires_access_asset(method="POST", asset_id_from_body=True)),
+ Depends(action_logging()),
+ ],
)
def create_asset_event(
body: CreateAssetEventsBody,
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 a78187be582..7d180d59067 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/security.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/security.py
@@ -56,6 +56,7 @@ from airflow.api_fastapi.auth.managers.models.resource_details import (
)
from airflow.api_fastapi.common.db.common import SessionDep
from airflow.api_fastapi.core_api.base import OrmClause
+from airflow.api_fastapi.core_api.datamodels.assets import CreateAssetEventsBody
from airflow.api_fastapi.core_api.datamodels.common import (
BulkAction,
BulkActionOnExistence,
@@ -967,19 +968,38 @@ def requires_access_dag_run_clear_bulk() -> Callable[[BulkDAGRunClearBody, BaseU
return inner
-def requires_access_asset(method: ResourceMethod) -> Callable[[Request, BaseUser], None]:
- def inner(
- request: Request,
- user: GetUserDep,
- ) -> None:
- asset_id = request.path_params.get("asset_id")
+def requires_access_asset(method: ResourceMethod, *, asset_id_from_body: bool = False) -> Callable[..., None]:
+ """
+ Authorize the caller on the asset targeted by the request.
+
+ :param method: the method to perform
+ :param asset_id_from_body: read ``asset_id`` from a ``CreateAssetEventsBody`` request body instead of
+ the path. The dependency parameter must be named ``body`` to share the route's body.
+ """
+ def _authorize(asset_id: str | None, user: BaseUser) -> None:
_requires_access(
is_authorized_callback=lambda: get_auth_manager().is_authorized_asset(
method=method, details=AssetDetails(id=asset_id), user=user
),
)
+ if asset_id_from_body:
+
+ def inner_from_body(
+ body: CreateAssetEventsBody,
+ user: GetUserDep,
+ ) -> None:
+ _authorize(str(body.asset_id), user)
+
+ return inner_from_body
+
+ def inner(
+ request: Request,
+ user: GetUserDep,
+ ) -> None:
+ _authorize(request.path_params.get("asset_id"), user)
+
return inner
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
index e1b66fb7a65..e5a19448302 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
@@ -26,7 +26,11 @@ from sqlalchemy import delete, func, select, update
from airflow._shared.timezones import timezone
from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager
-from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity, DagDetails
+from airflow.api_fastapi.auth.managers.models.resource_details import (
+ AssetDetails,
+ DagAccessEntity,
+ DagDetails,
+)
from airflow.api_fastapi.core_api.security import PermittedAssetEventFilter
from airflow.models import DagModel
from airflow.models.asset import (
@@ -1532,12 +1536,49 @@ class TestPostAssetEvents(TestAssets):
}
check_last_log(session, dag_id=None, event="create_asset_event", logical_date=None)
+ @mock.patch(
+ "airflow.api_fastapi.auth.managers.simple.simple_auth_manager.SimpleAuthManager.is_authorized_asset",
+ autospec=True,
+ )
+ def test_should_authorize_on_the_asset_named_in_the_body(
+ self, mock_is_authorized_asset, test_client, session
+ ):
+ """The asset id lives in the body, so the route must read it from there to authorize."""
+ (asset,) = self.create_assets(num=1, session=session)
+ mock_is_authorized_asset.return_value = True
+
+ response = test_client.post("/assets/events", json={"asset_id": asset.id})
+
+ assert response.status_code == 200
+ mock_is_authorized_asset.assert_called_once_with(
+ mock.ANY,
+ method="POST",
+ details=AssetDetails(id=str(asset.id)),
+ user=mock.ANY,
+ )
+
+ @mock.patch(
+ "airflow.api_fastapi.auth.managers.simple.simple_auth_manager.SimpleAuthManager.is_authorized_asset",
+ autospec=True,
+ )
+ def test_should_respond_403_when_not_authorized_on_the_asset(
+ self, mock_is_authorized_asset, test_client, session
+ ):
+ (asset,) = self.create_assets(num=1, session=session)
+ mock_is_authorized_asset.return_value = False
+
+ response = test_client.post("/assets/events", json={"asset_id": asset.id})
+
+ assert response.status_code == 403
+ assert session.scalar(select(func.count()).select_from(AssetEvent)) == 0
+
def test_should_respond_401(self, unauthenticated_test_client):
response = unauthenticated_test_client.post("/assets/events", json={"asset_uri": "s3://bucket/key/1"})
assert response.status_code == 401
- def test_should_respond_403(self, unauthorized_test_client):
- response = unauthorized_test_client.post("/assets/events", json={"asset_uri": "s3://bucket/key/1"})
+ def test_should_respond_403(self, unauthorized_test_client, session):
+ (asset,) = self.create_assets(num=1, session=session)
+ response = unauthorized_test_client.post("/assets/events", json={"asset_id": asset.id})
assert response.status_code == 403
def test_invalid_attr_not_allowed(self, test_client, session):
(airflow) 06/09: [v3-3-test] Fix DAG.cli() crashing on dags test --show-dagrun (#72810) (#73216)
Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit cec23c41cf19bef34981487ed71a37d445d8e0d1
Author: github-actions[bot] <41...@users.noreply.github.com>
AuthorDate: Wed Sep 16 03:45:21 2026 +0800
[v3-3-test] Fix DAG.cli() crashing on dags test --show-dagrun (#72810) (#73216)
* Fix DAG.cli() crashing on dags test --show-dagrun
A Dag file run as a script goes through DAG.cli(), whose parser drops the
dag_id positional and passes the Dag object to the handler instead. dag_test
never needed dag_id from the parsed arguments until it rendered the run: the
task instance query read args.dag_id, so --show-dagrun, --save-dagrun and
--imgcat-dagrun raised AttributeError after the Dag had already run, while
the plain command and the regular airflow dags test path worked.
Filtering on the resolved Dag's id is the same value on the regular path and
the only one available from DAG.cli().
* Update airflow-core/tests/unit/cli/commands/test_dag_command.py
* Update airflow-core/tests/unit/cli/commands/test_dag_command.py
---------
(cherry picked from commit 63fc32fd14bbbb20abfa60301abcd2f33d49726f)
Co-authored-by: Y-C <ea...@gmail.com>
Co-authored-by: Eason09053360 <18...@users.noreply.github.com>
Co-authored-by: rjgoyln <15...@users.noreply.github.com>
Co-authored-by: Henry Chen <he...@gmail.com>
---
.../src/airflow/cli/commands/dag_command.py | 2 +-
.../tests/unit/cli/commands/test_dag_command.py | 21 ++++++++++++++++++++-
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/airflow-core/src/airflow/cli/commands/dag_command.py b/airflow-core/src/airflow/cli/commands/dag_command.py
index c31b989d1e6..1d5f7bba099 100644
--- a/airflow-core/src/airflow/cli/commands/dag_command.py
+++ b/airflow-core/src/airflow/cli/commands/dag_command.py
@@ -849,7 +849,7 @@ def dag_test(args, dag: DAG | None = None, *, session: Session = NEW_SESSION) ->
if show_dagrun or imgcat or filename:
tis = session.scalars(
select(TaskInstance).where(
- TaskInstance.dag_id == args.dag_id,
+ TaskInstance.dag_id == dag.dag_id,
TaskInstance.run_id == dr.run_id,
)
).all()
diff --git a/airflow-core/tests/unit/cli/commands/test_dag_command.py b/airflow-core/tests/unit/cli/commands/test_dag_command.py
index db7bf2d14ad..d9020a4601d 100644
--- a/airflow-core/tests/unit/cli/commands/test_dag_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_dag_command.py
@@ -942,9 +942,10 @@ class TestCliDags:
)
@mock.patch("airflow.cli.commands.dag_command.render_dag", return_value=MagicMock(source="SOURCE"))
- @mock.patch("airflow.cli.commands.dag_command.get_bagged_dag")
+ @mock.patch("airflow.cli.commands.dag_command.get_bagged_dag", autospec=True)
def test_dag_test_show_dag(self, mock_get_dag, mock_render_dag, stdout_capture):
mock_get_dag.return_value.test.return_value.run_id = "__test_dag_test_show_dag_fake_dag_run_run_id__"
+ mock_get_dag.return_value.dag_id = "example_bash_operator"
cli_args = self.parser.parse_args(
["dags", "test", "example_bash_operator", DEFAULT_DATE.isoformat(), "--show-dagrun"]
@@ -969,6 +970,24 @@ class TestCliDags:
mock_render_dag.assert_has_calls([mock.call(mock_get_dag.return_value, tis=[])])
assert "SOURCE" in output
+ @mock.patch("airflow.cli.commands.dag_command.render_dag", autospec=True)
+ @mock.patch.object(DAG, "test", autospec=True)
+ def test_dag_test_show_dag_from_dag_cli(self, mock_test, mock_render_dag, dag_maker, stdout_capture):
+ """``DAG.cli()`` passes the Dag positionally and its parser drops ``dag_id``."""
+ with dag_maker("dag_cli_show_dagrun", schedule=None) as dag:
+ EmptyOperator(task_id="only_task")
+ mock_test.return_value = dag_maker.create_dagrun(run_id="dag_cli_run")
+
+ parser = cli_parser.get_parser(dag_parser=True)
+ with stdout_capture:
+ dag_command.dag_test(parser.parse_args(["dags", "test", "--show-dagrun"]), dag)
+
+ mock_render_dag.assert_called_once()
+ assert mock_render_dag.call_args.args[0] is dag
+ assert [(ti.dag_id, ti.task_id, ti.run_id) for ti in mock_render_dag.call_args.kwargs["tis"]] == [
+ ("dag_cli_show_dagrun", "only_task", "dag_cli_run")
+ ]
+
@mock.patch("airflow.dag_processing.dagbag.BundleDagBag")
def test_dag_test_with_bundle_name(self, mock_dagbag, configure_dag_bundles):
"""Test that DAG can be tested using bundle name."""
(airflow) 05/09: [v3-3-test] Reject non-numeric --limit values in airflow dags list-jobs (#72891) (#73214)
Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 4b7d6c43f721fc0a8000dbede3d122d55b3b3974
Author: Henry Chen <he...@apache.org>
AuthorDate: Wed Sep 16 03:45:12 2026 +0800
[v3-3-test] Reject non-numeric --limit values in airflow dags list-jobs (#72891) (#73214)
The --limit option had no argparse type, so a value such as "abc" or "-1"
was passed straight into the SQLAlchemy query and surfaced as a raw
ValueError traceback instead of a usage error. Validating it at parse
time gives the standard argparse message and exit code 2, while keeping
0 and positive integers working exactly as before.
(cherry picked from commit 022ff83f14563cde9b91e87cd5a18253997384e3)
Co-authored-by: Y-C <ea...@gmail.com>
Co-authored-by: Eason09053360 <18...@users.noreply.github.com>
---
airflow-core/src/airflow/cli/cli_config.py | 2 +-
airflow-core/tests/unit/cli/test_cli_parser.py | 9 +++++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/airflow-core/src/airflow/cli/cli_config.py b/airflow-core/src/airflow/cli/cli_config.py
index c8a5e224e90..8f1386fccef 100644
--- a/airflow-core/src/airflow/cli/cli_config.py
+++ b/airflow-core/src/airflow/cli/cli_config.py
@@ -294,7 +294,7 @@ ARG_DR_STATE = Arg(
# list_jobs
ARG_DAG_ID_OPT = Arg(("-d", "--dag-id"), help="The id of the dag")
-ARG_LIMIT = Arg(("--limit",), help="Return a limited number of records")
+ARG_LIMIT = Arg(("--limit",), type=positive_int(allow_zero=True), help="Return a limited number of records")
job_states = tuple(state.value for state in JobState)
ARG_JOB_STATE = Arg(
("--state",),
diff --git a/airflow-core/tests/unit/cli/test_cli_parser.py b/airflow-core/tests/unit/cli/test_cli_parser.py
index 2a0680f08ef..d9a8080ed18 100644
--- a/airflow-core/tests/unit/cli/test_cli_parser.py
+++ b/airflow-core/tests/unit/cli/test_cli_parser.py
@@ -608,6 +608,15 @@ class TestCli:
"airflow db export-archived command error: argument --export-format: invalid choice" in error_msg
)
+ @pytest.mark.parametrize("bad_value", ["abc", "-1"])
+ def test_dags_list_jobs_rejects_invalid_limit(self, bad_value):
+ with contextlib.redirect_stderr(StringIO()) as stderr:
+ parser = cli_parser.get_parser()
+ with pytest.raises(SystemExit) as e:
+ parser.parse_args(["dags", "list-jobs", "--limit", bad_value])
+ assert e.value.code == 2
+ assert f"argument --limit: invalid positive int value: '{bad_value}'" in stderr.getvalue()
+
@pytest.mark.parametrize(
"action_cmd",
[
(airflow) 07/09: Split the api_fastapi common parameters module into a package (#73239)
Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 93e3df37b75f275c8269e189465cd348f95eb78f
Author: Pierre Jeambrun <pi...@gmail.com>
AuthorDate: Wed Sep 16 13:06:36 2026 +0200
Split the api_fastapi common parameters module into a package (#73239)
The parameters module had grown to ~1800 lines mixing generic query
helpers with hundreds of domain-specific query-parameter aliases, making
it hard to navigate and maintain. Grouping the machinery (base, search,
filter, sort, range) and the per-domain aliases (dag, dag_run,
task_instance, xcom, asset, misc) into focused submodules keeps each file
readable. The package __init__ re-exports the names consumed elsewhere in
the codebase so existing import paths keep working.
Backport of #72795 to v3-3-test. Because v3-3-test's parameters.py has a
smaller set of query-parameter definitions than main (some PRs were not
backported), this was re-created by running the same mechanical split on
v3-3-test's file rather than cherry-picked: every definition is
byte-identical to the original, and __init__ re-exports only the names
consumed outside the package.
---
.../src/airflow/api_fastapi/common/parameters.py | 1824 --------------------
.../api_fastapi/common/parameters/__init__.py | 152 ++
.../airflow/api_fastapi/common/parameters/asset.py | 237 +++
.../airflow/api_fastapi/common/parameters/base.py | 139 ++
.../airflow/api_fastapi/common/parameters/dag.py | 213 +++
.../api_fastapi/common/parameters/dag_run.py | 171 ++
.../api_fastapi/common/parameters/filter.py | 156 ++
.../airflow/api_fastapi/common/parameters/misc.py | 302 ++++
.../airflow/api_fastapi/common/parameters/range.py | 216 +++
.../api_fastapi/common/parameters/search.py | 306 ++++
.../airflow/api_fastapi/common/parameters/sort.py | 205 +++
.../api_fastapi/common/parameters/task_instance.py | 276 +++
.../airflow/api_fastapi/common/parameters/xcom.py | 68 +
.../unit/api_fastapi/common/test_parameters.py | 15 +-
14 files changed, 2449 insertions(+), 1831 deletions(-)
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py b/airflow-core/src/airflow/api_fastapi/common/parameters.py
deleted file mode 100644
index db873ad9c11..00000000000
--- a/airflow-core/src/airflow/api_fastapi/common/parameters.py
+++ /dev/null
@@ -1,1824 +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 abc import ABC, abstractmethod
-from collections.abc import Callable, Iterable, Sequence
-from datetime import datetime
-from enum import Enum
-from typing import (
- TYPE_CHECKING,
- Annotated,
- Any,
- Generic,
- Literal,
- TypeVar,
- cast,
- overload,
-)
-
-from fastapi import Depends, HTTPException, Query, status
-from pendulum.parsing.exceptions import ParserError
-from pydantic import AfterValidator, BaseModel, NonNegativeInt
-from sqlalchemy import Column, String, and_, func, not_, or_, select as sql_select, true as sql_true
-from sqlalchemy.ext.compiler import compiles
-from sqlalchemy.inspection import inspect
-from sqlalchemy.orm import aliased
-from sqlalchemy.sql.functions import FunctionElement
-
-from airflow._shared.timezones import timezone
-from airflow.api_fastapi.compat import HTTP_422_UNPROCESSABLE_CONTENT
-from airflow.api_fastapi.core_api.base import OrmClause
-from airflow.api_fastapi.core_api.security import GetUserDep
-from airflow.configuration import conf
-from airflow.models import Base
-from airflow.models.asset import (
- AssetAliasModel,
- AssetEvent,
- AssetModel,
- AssetPartitionDagRun,
- DagScheduleAssetReference,
- TaskInletAssetReference,
- TaskOutletAssetReference,
- association_table,
-)
-from airflow.models.connection import Connection
-from airflow.models.dag import DagModel, DagTag
-from airflow.models.dag_favorite import DagFavorite
-from airflow.models.dag_version import DagVersion
-from airflow.models.dagrun import DagRun
-from airflow.models.errors import ParseImportError
-from airflow.models.hitl import HITLDetail
-from airflow.models.pool import Pool
-from airflow.models.taskinstance import TaskInstance
-from airflow.models.variable import Variable
-from airflow.models.xcom import XComModel
-from airflow.typing_compat import Self
-from airflow.utils.state import DagRunState, TaskInstanceState
-from airflow.utils.types import DagRunType
-
-if TYPE_CHECKING:
- from sqlalchemy.orm.attributes import InstrumentedAttribute
- from sqlalchemy.sql import ColumnElement, Select
- from sqlalchemy.sql.compiler import SQLCompiler
-
- from airflow.serialization.definitions.dag import SerializedDAG
-
-T = TypeVar("T")
-
-_FALLBACK_PAGE_LIMIT: int = conf.getint("api", "fallback_page_limit")
-
-
-class _MySQLCollate(FunctionElement):
- """
- Wraps a SQL expression so that on MySQL it is emitted with an explicit ``COLLATE`` clause.
-
- On every other dialect the expression is passed through unchanged.
-
- This is needed when a computed expression (e.g. a ``CASE … END`` that mixes
- a stored ``VARCHAR`` column with a ``CAST(integer AS CHAR)``) ends up with
- MySQL coercibility ``NONE`` because the two branches carry different implicit
- collations. Comparing such an expression with a bound parameter fails with
- "Illegal mix of collations". Wrapping the expression in an explicit
- ``COLLATE`` gives it ``EXPLICIT`` coercibility, which MySQL accepts in all
- comparison operators.
- """
-
- type = String()
- inherit_cache = True
-
- def __init__(self, expr: ColumnElement[Any], collation: str) -> None:
- super().__init__(expr)
- self.collation = collation
-
-
-@compiles(_MySQLCollate)
-def _compile_mysql_collate_default(element: _MySQLCollate, compiler: SQLCompiler, **kw: Any) -> str:
- """Non-MySQL: render the inner expression without any COLLATE clause."""
- (expr,) = element.clauses
- return compiler.process(expr, **kw)
-
-
-@compiles(_MySQLCollate, "mysql")
-def _compile_mysql_collate_mysql(element: _MySQLCollate, compiler: SQLCompiler, **kw: Any) -> str:
- """MySQL: wrap the inner expression with the requested COLLATE clause."""
- (expr,) = element.clauses
- inner = compiler.process(expr, **kw)
- return f"({inner}) COLLATE {element.collation}"
-
-
-class BaseParam(OrmClause[T], ABC):
- """Base class for path or query parameters with ORM transformation."""
-
- def __init__(self, value: T | None = None, skip_none: bool = True) -> None:
- super().__init__(value)
- self.attribute: ColumnElement | InstrumentedAttribute | None = None
- self.skip_none = skip_none
-
- def set_value(self, value: T | None) -> Self:
- self.value = value
- return self
-
- @classmethod
- @abstractmethod
- def depends(cls, *args: Any, **kwargs: Any) -> Self:
- pass
-
-
-class LimitFilter(BaseParam[NonNegativeInt]):
- """Filter on the limit."""
-
- def to_orm(self, select: Select) -> Select:
- if self.value is None and self.skip_none:
- return select
-
- return select.limit(self.value)
-
- @classmethod
- def depends(cls, limit: NonNegativeInt = _FALLBACK_PAGE_LIMIT) -> LimitFilter:
- return cls().set_value(min(limit, conf.getint("api", "maximum_page_limit")))
-
-
-class OffsetFilter(BaseParam[NonNegativeInt]):
- """Filter on offset."""
-
- def to_orm(self, select: Select) -> Select:
- if self.value is None and self.skip_none:
- return select
- return select.offset(self.value)
-
- @classmethod
- def depends(
- cls,
- offset: NonNegativeInt = 0,
- ) -> OffsetFilter:
- return cls().set_value(offset)
-
-
-class _FavoriteFilter(BaseParam[bool]):
- """Filter Dags by favorite status."""
-
- def __init__(self, user_id: str, value: T | None = None, skip_none: bool = True) -> None:
- super().__init__(skip_none=skip_none)
- self.user_id = user_id
-
- def to_orm(self, select_stmt: Select) -> Select:
- if self.value is None and self.skip_none:
- return select_stmt
-
- if self.value:
- select_stmt = select_stmt.join(DagFavorite, DagFavorite.dag_id == DagModel.dag_id).where(
- DagFavorite.user_id == self.user_id
- )
- else:
- select_stmt = select_stmt.where(
- not_(
- sql_select(DagFavorite)
- .where(and_(DagFavorite.dag_id == DagModel.dag_id, DagFavorite.user_id == self.user_id))
- .exists()
- )
- )
-
- return select_stmt
-
- @classmethod
- def depends(cls, user: GetUserDep, is_favorite: bool | None = Query(None)) -> _FavoriteFilter:
- instance = cls(user_id=str(user.get_id())).set_value(is_favorite)
- return instance
-
-
-class _ExcludeStaleFilter(BaseParam[bool]):
- """Filter on is_stale."""
-
- def to_orm(self, select: Select) -> Select:
- if self.value and self.skip_none:
- return select.where(DagModel.is_stale != self.value)
- return select
-
- @classmethod
- def depends(cls, exclude_stale: bool = True) -> _ExcludeStaleFilter:
- return cls().set_value(exclude_stale)
-
-
-class _PrefixPatternParam(BaseParam[str], ABC):
- """
- Shared prefix pattern: pipe ``|`` for OR, ``~`` → empty (match all), Unicode prefix range.
-
- .. note::
- Trailing non-alphanumeric characters in a search term are stripped before the range
- is computed. A range scan with a punctuation-terminated upper bound is unsafe under
- PostgreSQL's default locale-aware collation (``en_US.utf8`` sorts punctuation in a
- way that breaks the range), and additionally stopping the range at an alphanumeric
- character keeps the upper bound alphanumeric too, so the predicate stays usable by
- default btree indexes. A user who asks for prefix ``"test_"`` gets matches starting
- with ``"test"`` — a small over-match trade-off made explicit in the public
- ``*_prefix_pattern`` query-param description.
- """
-
- pipe_as_or: bool = True
-
- @staticmethod
- def _prefix_range_upper(term: str) -> str | None:
- """
- Compute the exclusive upper bound for a prefix range scan.
-
- Returns ``None`` if the term has no alphanumeric characters. Trailing non-alphanumeric
- characters are dropped before bumping the last character so the resulting upper bound
- is itself alphanumeric and behaves predictably under locale-aware collations. If
- incrementing would land outside the alphanumeric range (e.g. ``'z' → '{'``), we drop
- that character and retry.
- """
- while term and not term[-1].isalnum():
- term = term[:-1]
- if not term:
- return None
- last = ord(term[-1])
- if last >= 0x10FFFF:
- return _PrefixPatternParam._prefix_range_upper(term[:-1])
- bumped = chr(last + 1)
- if not bumped.isalnum():
- return _PrefixPatternParam._prefix_range_upper(term[:-1])
- return term[:-1] + bumped
-
- @staticmethod
- def _prefix_lower_bound(term: str) -> str:
- """Return the matching lower bound: strip trailing non-alphanumeric chars to pair with the upper."""
- while term and not term[-1].isalnum():
- term = term[:-1]
- return term
-
- @abstractmethod
- def _prefix_clause(self, term: str):
- """Return the SQL boolean for one prefix term (including empty string after ``~`` alias)."""
-
- def to_orm(self, select: Select) -> Select:
- # ``skip_none`` only gates the "no value" behavior for the callers that must keep
- # the filter slot present (e.g. ``QueryDagIdPrefixPatternSearchWithNone``); applying
- # a ``None`` value as a filter produces nonsense predicates, so always skip it here.
- if self.value is None:
- return select
-
- val_str = str(self.value)
- if self.pipe_as_or and "|" in val_str:
- search_terms = [term.strip() for term in val_str.split("|") if term.strip()]
- if search_terms:
- return select.where(or_(*(self._prefix_clause(term) for term in search_terms)))
-
- return select.where(self._prefix_clause(val_str))
-
- def transform_aliases(self, value: str | None) -> str | None:
- if value == "~":
- value = ""
- return value
-
-
-def _build_pipe_clause(pipe_as_or: bool) -> str:
- """Build the per-parameter pipe note. OR is the documented default (see the API description), so only the literal exception is spelled out."""
- return "" if pipe_as_or else "Here `|` is matched literally, not as OR. "
-
-
-_LIKE_ESCAPE_CHAR = "\\"
-
-
-def _escape_like_pattern(value: str) -> str:
- r"""
- Escape SQL ``LIKE`` / ``ILIKE`` metacharacters in a user-supplied value.
-
- Use together with ``column.ilike(f"%{_escape_like_pattern(value)}%", escape="\\")`` on filter
- parameters that intend literal substring matching (so a user-supplied ``%`` or ``_`` does not
- widen the match beyond what the filter semantics promise). Search parameters that explicitly
- expose wildcard semantics (see :class:`_SearchParam`) must not call this — they want the
- metacharacters to pass through.
- """
- return (
- value.replace(_LIKE_ESCAPE_CHAR, _LIKE_ESCAPE_CHAR * 2)
- .replace("%", _LIKE_ESCAPE_CHAR + "%")
- .replace("_", _LIKE_ESCAPE_CHAR + "_")
- )
-
-
-class _SearchParam(BaseParam[str]):
- """
- Substring search on a column using ``ILIKE '%term%'`` (case-insensitive).
-
- .. note::
- This full-match substring search most of the time prevents the database
- from using B-tree indexes on ``attribute``, which can be very slow on
- large tables. Prefer :class:`_PrefixSearchParam` (the ``*_prefix_pattern``
- query-param counterpart) when matching from the beginning of the value
- is acceptable.
- """
-
- def __init__(self, attribute: ColumnElement, skip_none: bool = True, pipe_as_or: bool = True) -> None:
- super().__init__(skip_none=skip_none)
- self.attribute: ColumnElement = attribute
- self.pipe_as_or = pipe_as_or
-
- def to_orm(self, select: Select) -> Select:
- if self.value is None and self.skip_none:
- return select
-
- val_str = str(self.value)
- if self.pipe_as_or and "|" in val_str:
- search_terms = [term.strip() for term in val_str.split("|") if term.strip()]
- if search_terms:
- return select.where(or_(*(self.attribute.ilike(f"%{term}%") for term in search_terms)))
-
- return select.where(self.attribute.ilike(f"%{val_str}%"))
-
- def transform_aliases(self, value: str | None) -> str | None:
- if value == "~":
- value = "%"
- return value
-
- @classmethod
- def depends(cls, *args: Any, **kwargs: Any) -> Self:
- raise NotImplementedError("Use search_param_factory instead , depends is not implemented.")
-
-
-class _PrefixSearchParam(_PrefixPatternParam):
- """
- Prefix search on a column using range comparison (case-sensitive, index-friendly).
-
- Unlike :class:`_SearchParam`, wildcard characters are treated as literals and the query
- plan can use the column's default B-tree index for the range scan. Trailing
- non-alphanumeric characters in ``term`` are stripped first (see
- :class:`_PrefixPatternParam` for why).
- """
-
- def __init__(self, attribute: ColumnElement, skip_none: bool = True, pipe_as_or: bool = True) -> None:
- super().__init__(skip_none=skip_none)
- self.attribute: ColumnElement = attribute
- self.pipe_as_or = pipe_as_or
-
- def _prefix_clause(self, term: str):
- lower = self._prefix_lower_bound(term)
- if not lower:
- return self.attribute.is_not(None)
- upper = self._prefix_range_upper(term)
- if upper is None:
- return self.attribute >= lower
- return and_(self.attribute >= lower, self.attribute < upper)
-
- @classmethod
- def depends(cls, *args: Any, **kwargs: Any) -> Self:
- raise NotImplementedError("Use prefix_search_param_factory instead, depends is not implemented.")
-
-
-class _TaskDisplayNamePrefixPatternParam(_PrefixPatternParam):
- """
- Prefix filter equivalent to :attr:`TaskInstance.task_display_name`, rewritten for composite-index use.
-
- The hybrid expression ``coalesce(_task_display_property_value, task_id)`` cannot use those indexes;
- this implementation applies an equivalent ``OR`` of simpler range predicates instead. Trailing
- non-alphanumeric characters in ``term`` are stripped first (see :class:`_PrefixPatternParam`).
- """
-
- def _prefix_clause(self, term: str):
- lower = self._prefix_lower_bound(term)
- if not lower:
- return sql_true()
- upper = self._prefix_range_upper(term)
- if upper is None:
- return or_(
- and_(
- TaskInstance._task_display_property_value.is_(None),
- TaskInstance.task_id >= lower,
- ),
- and_(
- TaskInstance._task_display_property_value.is_not(None),
- TaskInstance._task_display_property_value >= lower,
- ),
- )
- return or_(
- and_(
- TaskInstance._task_display_property_value.is_(None),
- TaskInstance.task_id >= lower,
- TaskInstance.task_id < upper,
- ),
- and_(
- TaskInstance._task_display_property_value.is_not(None),
- TaskInstance._task_display_property_value >= lower,
- TaskInstance._task_display_property_value < upper,
- ),
- )
-
- @classmethod
- def depends(
- cls,
- task_display_name_prefix_pattern: str | None = Query(
- default=None,
- description=(
- "Case-sensitive prefix match on task display name (`_task_display_property_value` else "
- "`task_id`). Index-friendly alternative to `task_display_name_pattern`; on large databases "
- "combine with `dag_id_prefix_pattern` (or a specific Dag in the path) so composite indexes "
- 'apply. See "Filtering with pattern parameters".'
- ),
- ),
- ) -> Self:
- param = cls()
- return param.set_value(param.transform_aliases(task_display_name_prefix_pattern))
-
-
-class QueryTaskInstanceTaskGroupFilter(BaseParam[str]):
- """Task group filter - returns all tasks in the specified group."""
-
- def __init__(self, dag=None, skip_none: bool = True):
- super().__init__(skip_none=skip_none)
- self._dag: None | SerializedDAG = dag
-
- @property
- def dag(self) -> None | SerializedDAG:
- return self._dag
-
- @dag.setter
- def dag(self, value: None | SerializedDAG) -> None:
- self._dag = value
-
- def to_orm(self, select: Select) -> Select:
- if self.value is None and self.skip_none:
- return select
-
- if not self.dag:
- raise ValueError("Dag must be set before calling to_orm")
-
- if not hasattr(self.dag, "task_group"):
- return select
-
- # Exact matching on group_id
- task_groups = self.dag.task_group.get_task_group_dict()
- task_group = task_groups.get(self.value)
- if not task_group:
- raise HTTPException(
- status.HTTP_404_NOT_FOUND,
- detail={
- "reason": "not_found",
- "message": f"Task group {self.value} not found",
- },
- )
-
- return select.where(TaskInstance.task_id.in_(task.task_id for task in task_group.iter_tasks()))
-
- @classmethod
- def depends(
- cls,
- value: str | None = Query(
- alias="task_group_id",
- default=None,
- description="Filter by exact task group ID. Returns all tasks within the specified task group.",
- ),
- ) -> QueryTaskInstanceTaskGroupFilter:
- return cls(dag=None).set_value(value)
-
-
-def search_param_factory(
- attribute: ColumnElement,
- pattern_name: str,
- skip_none: bool = True,
- pipe_as_or: bool = True,
-) -> Callable[[str | None], _SearchParam]:
- prefix_pattern_name = pattern_name.replace("_pattern", "_prefix_pattern")
- DESCRIPTION = (
- "Case-insensitive substring match (SQL `ILIKE`). "
- f"{_build_pipe_clause(pipe_as_or)}"
- f'Slower than `{prefix_pattern_name}` on large tables — see "Filtering with pattern parameters".'
- )
-
- def depends_search(
- value: str | None = Query(alias=pattern_name, default=None, description=DESCRIPTION),
- ) -> _SearchParam:
- search_parm = _SearchParam(attribute, skip_none, pipe_as_or=pipe_as_or)
- value = search_parm.transform_aliases(value)
- return search_parm.set_value(value)
-
- return depends_search
-
-
-def prefix_search_param_factory(
- attribute: ColumnElement,
- prefix_pattern_name: str,
- skip_none: bool = True,
- pipe_as_or: bool = True,
-) -> Callable[[str | None], _PrefixSearchParam]:
- """
- Build a FastAPI ``Depends`` returning a :class:`_PrefixSearchParam` for prefix matching.
-
- Prefer this over :func:`search_param_factory` for performance: prefix matching uses a
- B-tree index range scan, while substring matching requires a full table scan.
- """
- DESCRIPTION = (
- "Case-sensitive, index-friendly prefix match. "
- f"{_build_pipe_clause(pipe_as_or)}"
- 'See "Filtering with pattern parameters".'
- )
-
- def depends_prefix_search(
- value: str | None = Query(alias=prefix_pattern_name, default=None, description=DESCRIPTION),
- ) -> _PrefixSearchParam:
- search_parm = _PrefixSearchParam(attribute, skip_none, pipe_as_or=pipe_as_or)
- value = search_parm.transform_aliases(value)
- return search_parm.set_value(value)
-
- return depends_prefix_search
-
-
-class SortParam(BaseParam[list[str]]):
- """Order result by the attribute."""
-
- MAX_SORT_PARAMS = 10
-
- def __init__(
- self,
- allowed_attrs: list[str],
- model: Base,
- to_replace: dict[str, str | Column | list[Column]] | None = None,
- ) -> None:
- super().__init__()
- self.allowed_attrs = allowed_attrs
- self.model = model
- self.to_replace = to_replace
- self._cached_resolution: list[tuple[str, ColumnElement, bool]] | None = None
-
- def set_value(self, value: list[str] | None) -> Self:
- self._cached_resolution = None
- return super().set_value(value)
-
- def _resolve(self) -> list[tuple[str, ColumnElement, bool]]:
- """Resolve sort columns as (attr_name, column, is_descending) tuples. Cached after first call."""
- if self._cached_resolution is not None:
- return self._cached_resolution
-
- if self.value is None:
- self.value = [self.get_primary_key_string()]
-
- order_by_values = self.value
- if len(order_by_values) > self.MAX_SORT_PARAMS:
- raise HTTPException(
- 400,
- f"Ordering with more than {self.MAX_SORT_PARAMS} parameters is not allowed. Provided: {order_by_values}",
- )
-
- resolved: list[tuple[str, ColumnElement, bool]] = []
- for order_by_value in order_by_values:
- lstriped_orderby = order_by_value.lstrip("-")
- # Store the user-facing name in the resolved tuple. ``row_value`` resolves
- # it back to the actual row accessor via ``to_replace`` when reading values
- # for cursor encoding.
- attr_name = lstriped_orderby
- column: Column | None = None
- if self.to_replace:
- replacement = self.to_replace.get(lstriped_orderby, lstriped_orderby)
- if isinstance(replacement, str):
- lstriped_orderby = replacement
- elif isinstance(replacement, list):
- # Compound sort: expand the list into multiple sort entries.
- # Each column's ORM key becomes its attr_name so that
- # row_value() can read the corresponding attribute via
- # getattr(row, attr_name) without further to_replace lookups.
- is_desc = order_by_value.startswith("-")
- for col in replacement:
- col_attr_name = col.key
- resolved.append((col_attr_name, col, is_desc))
- continue
- else:
- column = replacement
-
- if (self.allowed_attrs and lstriped_orderby not in self.allowed_attrs) and column is None:
- raise HTTPException(
- 400,
- f"Ordering with '{lstriped_orderby}' is disallowed or "
- f"the attribute does not exist on the model",
- )
- if column is None:
- column = getattr(self.model, lstriped_orderby)
-
- resolved.append((attr_name, column, order_by_value.startswith("-")))
-
- primary_key_column = self.get_primary_key_column()
- pk_name = self.get_primary_key_string()
- resolved_column_keys = {getattr(col, "key", None) for _, col, _ in resolved}
- if pk_name not in resolved_column_keys:
- pk_desc = bool(order_by_values and order_by_values[0].startswith("-"))
- resolved.append((pk_name, primary_key_column, pk_desc))
-
- self._cached_resolution = resolved
- return self._cached_resolution
-
- def to_orm(self, select: Select, *, reversed: bool = False) -> Select:
- if self.skip_none is False:
- raise ValueError(f"Cannot set 'skip_none' to False on a {type(self)}")
-
- resolved = self._resolve()
- if reversed:
- columns = [col.asc() if is_desc else col.desc() for _, col, is_desc in resolved]
- else:
- columns = [col.desc() if is_desc else col.asc() for _, col, is_desc in resolved]
- return select.order_by(None).order_by(*columns)
-
- def get_resolved_columns(self) -> list[tuple[str, ColumnElement, bool]]:
- """Return resolved sort columns as (attr_name, column_element, is_descending) tuples."""
- return self._resolve()
-
- def row_value(self, row: Any, name: str) -> Any:
- """
- Extract the sort-key value for ``name`` from a result row.
-
- Resolves the accessor through ``to_replace`` for string aliases
- (e.g. ``{"dag_run_id": "run_id"}``). For column-form mappings
- (e.g. ``{"run_after": DagRun.run_after}``), resolves through the
- primary model's attribute so association proxies can still be used
- for cursor values. Raises ``NotImplementedError`` when the model
- exposes no such attribute rather than emitting a ``None`` cursor token.
- """
- if self.to_replace:
- replacement = self.to_replace.get(name)
- if isinstance(replacement, str):
- return getattr(row, replacement, None)
- if replacement is not None and not isinstance(replacement, list):
- # Column-form mapping resolves through the primary model's attribute,
- # often an association proxy onto the joined entity
- # (``TaskInstance.run_after`` -> ``dag_run.run_after``). Fail loudly if the
- # model exposes no such attribute, rather than emitting a ``None`` cursor token.
- try:
- return getattr(row, name)
- except AttributeError:
- raise NotImplementedError(
- f"Cursor pagination cannot resolve column-form ``to_replace`` for "
- f"``{name}``: the primary model exposes no such attribute. Add an "
- f"association proxy, use a string alias, or sort by a primary-model column."
- )
- # List-form replacements are expanded in _resolve() into individual entries
- # each using the column's own ORM key as attr_name, so ``name`` at this point
- # is already a concrete model attribute (e.g. ``_rendered_map_index`` or
- # ``map_index``) — fall through to the getattr below.
- return getattr(row, name, None)
-
- def get_primary_key_column(self) -> Column:
- """Get the primary key column of the model of SortParam object."""
- return inspect(self.model).primary_key[0]
-
- def get_primary_key_string(self) -> str:
- """Get the primary key string of the model of SortParam object."""
- return self.get_primary_key_column().name
-
- @classmethod
- def depends(cls, *args: Any, **kwargs: Any) -> Self:
- raise NotImplementedError("Use dynamic_depends, depends not implemented.")
-
- def dynamic_depends(self, default: str | Sequence[str] | None = None) -> Callable:
- # Include to_replace keys that are not already in allowed_attrs to avoid
- # duplicate entries in the spec description.
- allowed_set = set(self.allowed_attrs)
- to_replace_attrs = [k for k in self.to_replace if k not in allowed_set] if self.to_replace else []
-
- all_attrs = self.allowed_attrs + to_replace_attrs
-
- if default is None:
- default_list = [self.get_primary_key_string()]
- elif isinstance(default, str):
- default_list = [default]
- else:
- default_list = list(default)
-
- _order_by_query = Query(
- default=default_list,
- description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
- f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
- )
-
- def inner(order_by: list[str] = _order_by_query) -> SortParam:
- return SortParam(self.allowed_attrs, self.model, self.to_replace).set_value(order_by)
-
- return inner
-
-
-class FilterOptionEnum(Enum):
- """Filter options for FilterParam."""
-
- EQUAL = "eq"
- NOT_EQUAL = "ne"
- LESS_THAN = "lt"
- LESS_THAN_EQUAL = "le"
- GREATER_THAN = "gt"
- GREATER_THAN_EQUAL = "ge"
- IN = "in"
- NOT_IN = "not_in"
- ANY_EQUAL = "any_eq"
- ALL_EQUAL = "all_eq"
- IS_NONE = "is_none"
- CONTAINS = "contains"
-
-
-class FilterParam(BaseParam[T]):
- """Filter on attribute."""
-
- def __init__(
- self,
- attribute: InstrumentedAttribute,
- value: T | None = None,
- filter_option: FilterOptionEnum = FilterOptionEnum.EQUAL,
- skip_none: bool = True,
- ) -> None:
- super().__init__(value, skip_none)
- self.attribute: InstrumentedAttribute = attribute
- self.value: T | None = value
- self.filter_option: FilterOptionEnum = filter_option
-
- def to_orm(self, select: Select) -> Select:
- if isinstance(self.value, (list, str)) and not self.value and self.skip_none:
- return select
- if self.value is None and self.skip_none:
- return select
-
- if isinstance(self.value, list):
- if self.filter_option == FilterOptionEnum.IN:
- return select.where(self.attribute.in_(self.value))
- if self.filter_option == FilterOptionEnum.NOT_IN:
- return select.where(self.attribute.notin_(self.value))
- if self.filter_option == FilterOptionEnum.ANY_EQUAL:
- conditions = [self.attribute == val for val in self.value]
- return select.where(or_(*conditions))
- if self.filter_option == FilterOptionEnum.ALL_EQUAL:
- conditions = [self.attribute == val for val in self.value]
- return select.where(and_(*conditions))
- raise HTTPException(
- 400, f"Invalid filter option {self.filter_option} for list value {self.value}"
- )
-
- if self.filter_option == FilterOptionEnum.EQUAL:
- return select.where(self.attribute == self.value)
- if self.filter_option == FilterOptionEnum.NOT_EQUAL:
- return select.where(self.attribute != self.value)
- if self.filter_option == FilterOptionEnum.LESS_THAN:
- return select.where(self.attribute < self.value)
- if self.filter_option == FilterOptionEnum.LESS_THAN_EQUAL:
- return select.where(self.attribute <= self.value)
- if self.filter_option == FilterOptionEnum.GREATER_THAN:
- return select.where(self.attribute > self.value)
- if self.filter_option == FilterOptionEnum.GREATER_THAN_EQUAL:
- return select.where(self.attribute >= self.value)
- if self.filter_option == FilterOptionEnum.IS_NONE:
- if self.value is None:
- return select
- if self.value is False:
- return select.where(self.attribute.is_not(None))
- if self.value is True:
- return select.where(self.attribute.is_(None))
- if self.filter_option == FilterOptionEnum.CONTAINS:
- # For JSON/JSONB columns, convert to text before applying LIKE
- from sqlalchemy import Text, cast
-
- if str(self.attribute.type).upper() in ("JSON", "JSONB"):
- return select.where(cast(self.attribute, Text).contains(self.value))
- return select.where(self.attribute.contains(self.value))
- raise ValueError(f"Invalid filter option {self.filter_option} for value {self.value}")
-
- @classmethod
- def depends(cls, *args: Any, **kwargs: Any) -> Self:
- raise NotImplementedError("Use filter_param_factory instead , depends is not implemented.")
-
-
-def filter_param_factory(
- attribute: ColumnElement | InstrumentedAttribute,
- _type: type,
- filter_option: FilterOptionEnum = FilterOptionEnum.EQUAL,
- filter_name: str | None = None,
- default_value: T | None = None,
- default_factory: Callable[[], T | None] | None = None,
- skip_none: bool = True,
- transform_callable: Callable[[T | None], Any] | None = None,
- *,
- description: str | None = None,
-) -> Callable[[T | None], FilterParam[T | None]]:
- # if filter_name is not provided, use the attribute name as the default
- filter_name = filter_name or getattr(attribute, "name", str(attribute))
- # can only set either default_value or default_factory
- query = (
- Query(alias=filter_name, default_factory=default_factory, description=description)
- if default_factory is not None
- else Query(alias=filter_name, default=default_value, description=description)
- )
-
- def depends_filter(value: T | None = query) -> FilterParam[T | None]:
- if transform_callable:
- value = transform_callable(value)
- # Cast to InstrumentedAttribute for type compatibility
- attr = cast("InstrumentedAttribute", attribute)
- return FilterParam(attr, value, filter_option, skip_none)
-
- # add type hint to value at runtime
- depends_filter.__annotations__["value"] = _type
-
- return depends_filter
-
-
-class _TagFilterModel(BaseModel):
- """Tag Filter Model with a match mode parameter."""
-
- tags: list[str]
- tags_match_mode: Literal["any", "all"] | None
-
-
-class _TagsFilter(BaseParam[_TagFilterModel]):
- """Filter on tags."""
-
- def to_orm(self, select: Select) -> Select:
- if self.skip_none is False:
- raise ValueError(f"Cannot set 'skip_none' to False on a {type(self)}")
-
- if not self.value or not self.value.tags:
- return select
-
- conditions = [DagModel.tags.any(DagTag.name == tag) for tag in self.value.tags]
- operator = or_ if not self.value.tags_match_mode or self.value.tags_match_mode == "any" else and_
- return select.where(operator(*conditions))
-
- @classmethod
- def depends(
- cls,
- tags: list[str] = Query(default_factory=list),
- tags_match_mode: Literal["any", "all"] | None = None,
- ) -> _TagsFilter:
- return cls().set_value(_TagFilterModel(tags=tags, tags_match_mode=tags_match_mode))
-
-
-class _OwnersFilter(BaseParam[list[str]]):
- """Filter on owners."""
-
- def to_orm(self, select: Select) -> Select:
- if self.skip_none is False:
- raise ValueError(f"Cannot set 'skip_none' to False on a {type(self)}")
-
- if not self.value:
- return select
-
- conditions = [
- DagModel.owners.ilike(f"%{_escape_like_pattern(owner)}%", escape=_LIKE_ESCAPE_CHAR)
- for owner in self.value
- ]
- return select.where(or_(*conditions))
-
- @classmethod
- def depends(cls, owners: list[str] = Query(default_factory=list)) -> _OwnersFilter:
- return cls().set_value(owners)
-
-
-def _safe_parse_datetime(date_to_check: str) -> datetime:
- """
- Parse datetime and raise error for invalid dates.
-
- :param date_to_check: the string value to be parsed
- """
- if not date_to_check:
- raise ValueError(f"{date_to_check} cannot be None.")
- return _safe_parse_datetime_optional(date_to_check)
-
-
-@overload
-def _safe_parse_datetime_optional(date_to_check: str) -> datetime: ...
-
-
-@overload
-def _safe_parse_datetime_optional(date_to_check: None) -> None: ...
-
-
-def _safe_parse_datetime_optional(date_to_check: str | None) -> datetime | None:
- """
- Parse datetime and raise error for invalid dates.
-
- Allow None values.
-
- :param date_to_check: the string value to be parsed
- """
- if date_to_check is None:
- return None
- try:
- return timezone.parse(date_to_check, strict=True)
- except (TypeError, ParserError):
- raise HTTPException(
- 400, f"Invalid datetime: {date_to_check!r}. Please check the date parameter have this value."
- )
-
-
-class _DagIdAssetReferenceFilter(BaseParam[list[str]]):
- """Search on dag_id."""
-
- def __init__(self, skip_none: bool = True) -> None:
- super().__init__(skip_none=skip_none)
-
- @classmethod
- def depends(cls, dag_ids: list[str] = Query(None)) -> _DagIdAssetReferenceFilter:
- # needed to handle cases where dag_ids=a1,b1
- if dag_ids and len(dag_ids) == 1 and "," in dag_ids[0]:
- dag_ids = dag_ids[0].split(",")
- return cls().set_value(dag_ids)
-
- def to_orm(self, select: Select) -> Select:
- if self.value is None and self.skip_none:
- return select
-
- # At this point, self.value is either a list[str] or None -> coerce falsy None to an empty list
- dag_ids = self.value or []
- return select.where(
- (AssetModel.scheduled_dags.any(DagScheduleAssetReference.dag_id.in_(dag_ids)))
- | (AssetModel.producing_tasks.any(TaskOutletAssetReference.dag_id.in_(dag_ids)))
- | (AssetModel.consuming_tasks.any(TaskInletAssetReference.dag_id.in_(dag_ids)))
- )
-
-
-class Range(BaseModel, Generic[T]):
- """Range with a lower and upper bound."""
-
- lower_bound_gte: T | None
- lower_bound_gt: T | None
- upper_bound_lte: T | None
- upper_bound_lt: T | None
-
-
-class RangeFilter(BaseParam[Range]):
- """Filter on range in between the lower and upper bound."""
-
- def __init__(self, value: Range | None, attribute: InstrumentedAttribute) -> None:
- super().__init__(value)
- self.attribute: InstrumentedAttribute = attribute
-
- def to_orm(self, select: Select) -> Select:
- if self.skip_none is False:
- raise ValueError(f"Cannot set 'skip_none' to False on a {type(self)}")
-
- if self.value is None:
- return select
-
- if self.value.lower_bound_gte:
- select = select.where(self.attribute >= self.value.lower_bound_gte)
- if self.value.lower_bound_gt:
- select = select.where(self.attribute > self.value.lower_bound_gt)
- if self.value.upper_bound_lte:
- select = select.where(self.attribute <= self.value.upper_bound_lte)
- if self.value.upper_bound_lt:
- select = select.where(self.attribute < self.value.upper_bound_lt)
-
- return select
-
- @classmethod
- def depends(cls, *args: Any, **kwargs: Any) -> Self:
- raise NotImplementedError("Use the `range_filter_factory` function to create the dependency")
-
- def is_active(self) -> bool:
- """Check if the range filter has any active bounds."""
- return self.value is not None and (
- self.value.lower_bound_gte is not None
- or self.value.lower_bound_gt is not None
- or self.value.upper_bound_lte is not None
- or self.value.upper_bound_lt is not None
- )
-
-
-class NullableDatetimeRangeFilter(RangeFilter):
- """
- RangeFilter for nullable datetime columns (``start_date``, ``end_date``), rewritten for index use.
-
- ``COALESCE(column, now())`` wraps the column in a function call that prevents PostgreSQL from
- using btree indexes, forcing sequential scans on large tables. This class emits equivalent
- ``OR`` predicates so each branch can be satisfied by an independent index scan.
-
- NULL semantics: ``start_date=NULL`` means the task has not started yet; ``end_date=NULL`` means
- the task is still running. For lower bounds the NULL branch passes unconditionally — a not-yet-
- started/ended task will eventually satisfy any past lower bound. For upper bounds the NULL branch
- is ``col IS NULL AND now() <= x``, preserving the COALESCE(col, now()) semantics without the
- function-wrap index penalty.
- """
-
- def to_orm(self, select: Select) -> Select:
- if self.skip_none is False:
- raise ValueError(f"Cannot set 'skip_none' to False on a {type(self)}")
-
- if self.value is None:
- return select
-
- if self.value.lower_bound_gte:
- x = self.value.lower_bound_gte
- select = select.where(or_(self.attribute >= x, self.attribute.is_(None)))
- if self.value.lower_bound_gt:
- x = self.value.lower_bound_gt
- select = select.where(or_(self.attribute > x, self.attribute.is_(None)))
- if self.value.upper_bound_lte:
- x = self.value.upper_bound_lte
- select = select.where(or_(self.attribute <= x, and_(self.attribute.is_(None), func.now() <= x)))
- if self.value.upper_bound_lt:
- x = self.value.upper_bound_lt
- select = select.where(or_(self.attribute < x, and_(self.attribute.is_(None), func.now() < x)))
-
- return select
-
-
-def datetime_range_filter_factory(
- filter_name: str, model: Base, attribute_name: str | None = None
-) -> Callable[[datetime | None, datetime | None, datetime | None, datetime | None], RangeFilter]:
- def depends_datetime(
- lower_bound_gte: datetime | None = Query(alias=f"{filter_name}_gte", default=None),
- lower_bound_gt: datetime | None = Query(alias=f"{filter_name}_gt", default=None),
- upper_bound_lte: datetime | None = Query(alias=f"{filter_name}_lte", default=None),
- upper_bound_lt: datetime | None = Query(alias=f"{filter_name}_lt", default=None),
- ) -> RangeFilter:
- attr = getattr(model, attribute_name or filter_name)
- range_val = Range(
- lower_bound_gte=lower_bound_gte,
- lower_bound_gt=lower_bound_gt,
- upper_bound_lte=upper_bound_lte,
- upper_bound_lt=upper_bound_lt,
- )
- if filter_name in ("start_date", "end_date"):
- return NullableDatetimeRangeFilter(range_val, attr)
- return RangeFilter(range_val, attr)
-
- return depends_datetime
-
-
-def float_range_filter_factory(
- filter_name: str, model: Base
-) -> Callable[[float | None, float | None, float | None, float | None], RangeFilter]:
- def depends_float(
- lower_bound_gte: float | None = Query(alias=f"{filter_name}_gte", default=None),
- lower_bound_gt: float | None = Query(alias=f"{filter_name}_gt", default=None),
- upper_bound_lte: float | None = Query(alias=f"{filter_name}_lte", default=None),
- upper_bound_lt: float | None = Query(alias=f"{filter_name}_lt", default=None),
- ) -> RangeFilter:
- return RangeFilter(
- Range(
- lower_bound_gte=lower_bound_gte,
- lower_bound_gt=lower_bound_gt,
- upper_bound_lte=upper_bound_lte,
- upper_bound_lt=upper_bound_lt,
- ),
- getattr(model, filter_name),
- )
-
- return depends_float
-
-
-# Common Safe DateTime
-DateTimeQuery = Annotated[str, AfterValidator(_safe_parse_datetime)]
-OptionalDateTimeQuery = Annotated[str | None, AfterValidator(_safe_parse_datetime_optional)]
-
-# Dag
-QueryLimit = Annotated[LimitFilter, Depends(LimitFilter.depends)]
-QueryOffset = Annotated[OffsetFilter, Depends(OffsetFilter.depends)]
-QueryPausedFilter = Annotated[
- FilterParam[bool | None],
- Depends(filter_param_factory(DagModel.is_paused, bool | None, filter_name="paused")),
-]
-QueryHasImportErrorsFilter = Annotated[
- FilterParam[bool | None],
- Depends(
- filter_param_factory(
- DagModel.has_import_errors,
- bool | None,
- filter_name="has_import_errors",
- description="Filter Dags by having import errors. Only Dags that have been successfully loaded before will be returned.",
- )
- ),
-]
-QueryFavoriteFilter = Annotated[_FavoriteFilter, Depends(_FavoriteFilter.depends)]
-QueryExcludeStaleFilter = Annotated[_ExcludeStaleFilter, Depends(_ExcludeStaleFilter.depends)]
-QueryDagIdPatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(DagModel.dag_id, "dag_id_pattern"))
-]
-QueryDagIdPrefixPatternSearch = Annotated[
- _PrefixSearchParam, Depends(prefix_search_param_factory(DagModel.dag_id, "dag_id_prefix_pattern"))
-]
-QueryDagDisplayNamePatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(DagModel.dag_display_name, "dag_display_name_pattern"))
-]
-QueryDagDisplayNamePrefixPatternSearch = Annotated[
- _PrefixSearchParam,
- Depends(prefix_search_param_factory(DagModel.dag_display_name, "dag_display_name_prefix_pattern")),
-]
-QueryBundleNameFilter = Annotated[
- FilterParam[str | None],
- Depends(filter_param_factory(DagModel.bundle_name, str | None, filter_name="bundle_name")),
-]
-QueryBundleVersionFilter = Annotated[
- FilterParam[str | None],
- Depends(filter_param_factory(DagModel.bundle_version, str | None, filter_name="bundle_version")),
-]
-QueryDagIdPatternSearchWithNone = Annotated[
- _SearchParam, Depends(search_param_factory(DagModel.dag_id, "dag_id_pattern", False))
-]
-QueryDagIdPrefixPatternSearchWithNone = Annotated[
- _PrefixSearchParam,
- Depends(prefix_search_param_factory(DagModel.dag_id, "dag_id_prefix_pattern", False)),
-]
-QueryTagsFilter = Annotated[_TagsFilter, Depends(_TagsFilter.depends)]
-QueryOwnersFilter = Annotated[_OwnersFilter, Depends(_OwnersFilter.depends)]
-
-
-class _HasAssetScheduleFilter(BaseParam[bool]):
- """Filter Dags that have asset-based scheduling."""
-
- def to_orm(self, select: Select) -> Select:
- if self.value is None and self.skip_none:
- return select
-
- asset_ref_subquery = sql_select(DagScheduleAssetReference.dag_id).distinct()
-
- if self.value:
- # Filter Dags that have asset-based scheduling
- return select.where(DagModel.dag_id.in_(asset_ref_subquery))
-
- # Filter Dags that do NOT have asset-based scheduling
- return select.where(DagModel.dag_id.notin_(asset_ref_subquery))
-
- @classmethod
- def depends(
- cls,
- has_asset_schedule: bool | None = Query(None, description="Filter Dags with asset-based scheduling"),
- ) -> _HasAssetScheduleFilter:
- return cls().set_value(has_asset_schedule)
-
-
-class _AssetDependencyFilter(BaseParam[str]):
- """Filter Dags by specific asset dependencies."""
-
- def to_orm(self, select: Select) -> Select:
- if self.value is None:
- return select
-
- escaped = _escape_like_pattern(self.value)
- asset_dag_subquery = (
- sql_select(DagScheduleAssetReference.dag_id)
- .join(AssetModel, DagScheduleAssetReference.asset_id == AssetModel.id)
- .where(
- or_(
- AssetModel.name.ilike(f"%{escaped}%", escape=_LIKE_ESCAPE_CHAR),
- AssetModel.uri.ilike(f"%{escaped}%", escape=_LIKE_ESCAPE_CHAR),
- )
- )
- .distinct()
- )
-
- return select.where(DagModel.dag_id.in_(asset_dag_subquery))
-
- @classmethod
- def depends(
- cls,
- asset_dependency: str | None = Query(
- None, description="Filter Dags by asset dependency (name or URI)"
- ),
- ) -> _AssetDependencyFilter:
- return cls().set_value(asset_dependency)
-
-
-QueryHasAssetScheduleFilter = Annotated[_HasAssetScheduleFilter, Depends(_HasAssetScheduleFilter.depends)]
-QueryAssetDependencyFilter = Annotated[_AssetDependencyFilter, Depends(_AssetDependencyFilter.depends)]
-
-
-class _ConsumingAssetFilter(BaseParam[str | None]):
- """Filter Dag runs by consuming asset (name or URI)."""
-
- def to_orm(self, select: Select) -> Select:
- if not self.value:
- return select
-
- escaped = _escape_like_pattern(self.value)
- event_subquery = (
- sql_select(AssetEvent.id)
- .join(AssetModel, AssetEvent.asset_id == AssetModel.id)
- .where(
- or_(
- AssetModel.name.ilike(f"%{escaped}%", escape=_LIKE_ESCAPE_CHAR),
- AssetModel.uri.ilike(f"%{escaped}%", escape=_LIKE_ESCAPE_CHAR),
- )
- )
- .distinct()
- )
-
- dagrun_subquery = (
- sql_select(association_table.c.dag_run_id)
- .where(association_table.c.event_id.in_(event_subquery))
- .distinct()
- )
-
- return select.where(DagRun.id.in_(dagrun_subquery))
-
- @classmethod
- def depends(
- cls,
- consuming_asset_pattern: str | None = Query(
- None,
- description=(
- "Case-insensitive substring match against the consuming asset name or URI. "
- "Unlike the wildcard `*_pattern` parameters, `%` and `_` are matched literally, "
- "`|` is not an OR separator, and `~` does not match everything."
- ),
- ),
- ) -> _ConsumingAssetFilter:
- return cls().set_value(consuming_asset_pattern)
-
-
-QueryConsumingAssetPatternSearch = Annotated[_ConsumingAssetFilter, Depends(_ConsumingAssetFilter.depends)]
-
-
-class _PendingActionsFilter(BaseParam[bool]):
- """Filter Dags by having pending HITL actions (more than 1)."""
-
- def to_orm(self, select: Select) -> Select:
- if self.value is None and self.skip_none:
- return select
-
- from airflow.models.hitl import HITLDetail
- from airflow.models.taskinstance import TaskInstance
-
- # Join with HITLDetail and TaskInstance to find Dags
- pending_actions_count_subquery = (
- sql_select(func.count(HITLDetail.ti_id))
- .join(TaskInstance, HITLDetail.ti_id == TaskInstance.id)
- .where(
- HITLDetail.responded_at.is_(None),
- TaskInstance.state.in_((TaskInstanceState.DEFERRED, TaskInstanceState.AWAITING_INPUT)),
- )
- .where(TaskInstance.dag_id == DagModel.dag_id)
- .scalar_subquery()
- )
-
- if self.value is True:
- # Filter to show only Dags with pending actions
- where_clause = pending_actions_count_subquery >= 1
- else:
- # Filter to show only Dags without pending actions
- where_clause = pending_actions_count_subquery == 0
-
- return select.where(where_clause)
-
- @classmethod
- def depends(cls, has_pending_actions: bool | None = Query(None)) -> _PendingActionsFilter:
- return cls().set_value(has_pending_actions)
-
-
-QueryPendingActionsFilter = Annotated[_PendingActionsFilter, Depends(_PendingActionsFilter.depends)]
-
-
-class _AnyDagRunStateFilter(BaseParam[DagRunState | None]):
- """Filter Dags that have any DagRun in the given state, not only the latest one."""
-
- def to_orm(self, select: Select) -> Select:
- if self.value is None and self.skip_none:
- return select
-
- # Alias DagRun so this EXISTS subquery cannot auto-correlate to a DagRun the outer query
- # may already reference (e.g. the last_dag_run_state filter), which would strip the
- # subquery's FROM and raise. EXISTS resolves each Dag via the (dag_id, state) index.
- any_run = aliased(DagRun)
- has_run_in_state = (
- sql_select(any_run.dag_id)
- .where(any_run.dag_id == DagModel.dag_id, any_run.state == self.value)
- .exists()
- )
- return select.where(has_run_in_state)
-
- @classmethod
- def depends(
- cls,
- dag_run_state: DagRunState | None = Query(
- None,
- description="Filter Dags that have any DagRun in the given state.",
- ),
- ) -> _AnyDagRunStateFilter:
- return cls().set_value(dag_run_state)
-
-
-# DagRun
-QueryLastDagRunStateFilter = Annotated[
- FilterParam[DagRunState | None],
- Depends(filter_param_factory(DagRun.state, DagRunState | None, filter_name="last_dag_run_state")),
-]
-QueryAnyDagRunStateFilter = Annotated[_AnyDagRunStateFilter, Depends(_AnyDagRunStateFilter.depends)]
-
-
-def _transform_dag_run_states(states: Iterable[str] | None) -> list[DagRunState | None] | None:
- try:
- if not states:
- return None
- return [None if s in ("none", None) else DagRunState(s) for s in states]
- except ValueError:
- raise HTTPException(
- status_code=HTTP_422_UNPROCESSABLE_CONTENT,
- detail=f"Invalid value for state. Valid values are {', '.join(DagRunState)}",
- )
-
-
-QueryDagRunStateFilter = Annotated[
- FilterParam[list[str]],
- Depends(
- filter_param_factory(
- DagRun.state,
- list[str],
- FilterOptionEnum.ANY_EQUAL,
- default_factory=list,
- transform_callable=_transform_dag_run_states,
- )
- ),
-]
-
-
-def _transform_dag_run_types(types: list[str] | None) -> list[DagRunType | None] | None:
- try:
- if not types:
- return None
- return [None if run_type in ("none", None) else DagRunType(run_type) for run_type in types]
- except ValueError:
- raise HTTPException(
- status_code=HTTP_422_UNPROCESSABLE_CONTENT,
- detail=f"Invalid value for run type. Valid values are {', '.join(DagRunType)}",
- )
-
-
-QueryDagRunRunTypesFilter = Annotated[
- FilterParam[list[str]],
- Depends(
- filter_param_factory(
- attribute=DagRun.run_type,
- _type=list[str],
- filter_option=FilterOptionEnum.ANY_EQUAL,
- default_factory=list,
- transform_callable=_transform_dag_run_types,
- )
- ),
-]
-
-QueryDagRunTriggeringUserSearch = Annotated[
- _SearchParam, Depends(search_param_factory(DagRun.triggering_user_name, "triggering_user"))
-]
-QueryDagRunTriggeringUserPrefixSearch = Annotated[
- _PrefixSearchParam,
- Depends(prefix_search_param_factory(DagRun.triggering_user_name, "triggering_user_prefix")),
-]
-QueryDagRunPartitionKeySearch = Annotated[
- _SearchParam,
- Depends(search_param_factory(DagRun.partition_key, "partition_key_pattern", pipe_as_or=False)),
-]
-QueryDagRunPartitionKeyPrefixSearch = Annotated[
- _PrefixSearchParam,
- Depends(
- prefix_search_param_factory(DagRun.partition_key, "partition_key_prefix_pattern", pipe_as_or=False)
- ),
-]
-
-# DagTags
-QueryDagTagPatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(DagTag.name, "tag_name_pattern"))
-]
-QueryDagTagPrefixPatternSearch = Annotated[
- _PrefixSearchParam, Depends(prefix_search_param_factory(DagTag.name, "tag_name_prefix_pattern"))
-]
-
-
-# TI
-def _transform_ti_states(states: list[str] | None) -> list[TaskInstanceState | None] | None:
- """Transform a list of state strings into a list of TaskInstanceState enums handling special 'None' cases."""
- if not states:
- return None
-
- try:
- return [None if s in ("no_status", "none", None) else TaskInstanceState(s) for s in states]
- except ValueError:
- raise HTTPException(
- status_code=HTTP_422_UNPROCESSABLE_CONTENT,
- detail=f"Invalid value for state. Valid values are {', '.join(TaskInstanceState)}",
- )
-
-
-QueryTIStateFilter = Annotated[
- FilterParam[list[str]],
- Depends(
- filter_param_factory(
- TaskInstance.state,
- list[str],
- FilterOptionEnum.ANY_EQUAL,
- default_factory=list,
- transform_callable=_transform_ti_states,
- )
- ),
-]
-QueryTIPoolFilter = Annotated[
- FilterParam[list[str]],
- Depends(
- filter_param_factory(TaskInstance.pool, list[str], FilterOptionEnum.ANY_EQUAL, default_factory=list)
- ),
-]
-QueryTIQueueFilter = Annotated[
- FilterParam[list[str]],
- Depends(
- filter_param_factory(TaskInstance.queue, list[str], FilterOptionEnum.ANY_EQUAL, default_factory=list)
- ),
-]
-QueryTIPoolNamePatternSearch = Annotated[
- _SearchParam,
- Depends(search_param_factory(TaskInstance.pool, "pool_name_pattern")),
-]
-QueryTIPoolNamePrefixPatternSearch = Annotated[
- _PrefixSearchParam,
- Depends(prefix_search_param_factory(TaskInstance.pool, "pool_name_prefix_pattern")),
-]
-
-QueryTIQueueNamePatternSearch = Annotated[
- _SearchParam,
- Depends(search_param_factory(TaskInstance.queue, "queue_name_pattern")),
-]
-QueryTIQueueNamePrefixPatternSearch = Annotated[
- _PrefixSearchParam,
- Depends(prefix_search_param_factory(TaskInstance.queue, "queue_name_prefix_pattern")),
-]
-QueryTIExecutorFilter = Annotated[
- FilterParam[list[str]],
- Depends(
- filter_param_factory(
- TaskInstance.executor, list[str], FilterOptionEnum.ANY_EQUAL, default_factory=list
- )
- ),
-]
-QueryTITaskDisplayNamePatternSearch = Annotated[
- _SearchParam,
- Depends(search_param_factory(TaskInstance.task_display_name, "task_display_name_pattern")),
-]
-QueryTITaskDisplayNamePrefixPatternSearch = Annotated[
- _TaskDisplayNamePrefixPatternParam, Depends(_TaskDisplayNamePrefixPatternParam.depends)
-]
-QueryTITaskGroupFilter = Annotated[
- QueryTaskInstanceTaskGroupFilter, Depends(QueryTaskInstanceTaskGroupFilter.depends)
-]
-QueryTIDagVersionFilter = Annotated[
- FilterParam[list[int]],
- Depends(
- filter_param_factory(
- DagVersion.version_number,
- list[int],
- FilterOptionEnum.ANY_EQUAL,
- default_factory=list,
- )
- ),
-]
-QueryDagRunVersionFilter = Annotated[
- FilterParam[list[int]],
- Depends(
- filter_param_factory(
- DagVersion.version_number,
- list[int],
- FilterOptionEnum.ANY_EQUAL,
- default_factory=list,
- filter_name="dag_version",
- )
- ),
-]
-QueryTITryNumberFilter = Annotated[
- FilterParam[list[int]],
- Depends(
- filter_param_factory(
- TaskInstance.try_number, list[int], FilterOptionEnum.ANY_EQUAL, default_factory=list
- )
- ),
-]
-
-QueryTIOperatorFilter = Annotated[
- FilterParam[list[str]],
- Depends(
- filter_param_factory(
- TaskInstance.operator, list[str], FilterOptionEnum.ANY_EQUAL, default_factory=list
- )
- ),
-]
-QueryTIOperatorNamePatternSearch = Annotated[
- _SearchParam,
- Depends(
- search_param_factory(
- TaskInstance.custom_operator_name,
- "operator_name_pattern",
- )
- ),
-]
-QueryTIOperatorNamePrefixPatternSearch = Annotated[
- _PrefixSearchParam,
- Depends(
- prefix_search_param_factory(
- TaskInstance.custom_operator_name,
- "operator_name_prefix_pattern",
- )
- ),
-]
-
-QueryTIMapIndexFilter = Annotated[
- FilterParam[list[int]],
- Depends(
- filter_param_factory(
- TaskInstance.map_index, list[int], FilterOptionEnum.ANY_EQUAL, default_factory=list
- )
- ),
-]
-# On MySQL the CASE expression that backs rendered_map_index mixes a stored
-# VARCHAR column (utf8mb4_bin, IMPLICIT) with CAST(map_index AS CHAR)
-# (utf8mb4_0900_ai_ci, IMPLICIT), which gives the whole expression NONE
-# coercibility. Comparing it against a bound parameter then fails with
-# "Illegal mix of collations". _MySQLCollate wraps the expression so that
-# on MySQL an explicit COLLATE clause is emitted (giving EXPLICIT coercibility);
-# on PostgreSQL and SQLite the wrapper is transparent.
-_rendered_map_index_collated = _MySQLCollate(
- cast("ColumnElement[Any]", TaskInstance.rendered_map_index), "utf8mb4_0900_ai_ci"
-)
-
-QueryTIRenderedMapIndexPatternSearch = Annotated[
- _SearchParam,
- Depends(
- search_param_factory(
- _rendered_map_index_collated,
- "rendered_map_index_pattern",
- )
- ),
-]
-QueryTIRenderedMapIndexPrefixPatternSearch = Annotated[
- _PrefixSearchParam,
- Depends(
- prefix_search_param_factory(
- _rendered_map_index_collated,
- "rendered_map_index_prefix_pattern",
- )
- ),
-]
-
-# XCom
-QueryXComKeyPatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(XComModel.key, "xcom_key_pattern"))
-]
-QueryXComKeyPrefixPatternSearch = Annotated[
- _PrefixSearchParam, Depends(prefix_search_param_factory(XComModel.key, "xcom_key_prefix_pattern"))
-]
-
-QueryXComDagDisplayNamePatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(DagModel.dag_display_name, "dag_display_name_pattern"))
-]
-QueryXComDagDisplayNamePrefixPatternSearch = Annotated[
- _PrefixSearchParam,
- Depends(prefix_search_param_factory(DagModel.dag_display_name, "dag_display_name_prefix_pattern")),
-]
-QueryXComRunIdPatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(XComModel.run_id, "run_id_pattern"))
-]
-QueryXComRunIdPrefixPatternSearch = Annotated[
- _PrefixSearchParam, Depends(prefix_search_param_factory(XComModel.run_id, "run_id_prefix_pattern"))
-]
-QueryXComTaskIdPatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(XComModel.task_id, "task_id_pattern"))
-]
-QueryXComTaskIdPrefixPatternSearch = Annotated[
- _PrefixSearchParam, Depends(prefix_search_param_factory(XComModel.task_id, "task_id_prefix_pattern"))
-]
-
-# Assets
-QueryAssetNamePatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(AssetModel.name, "name_pattern"))
-]
-QueryAssetNamePrefixPatternSearch = Annotated[
- _PrefixSearchParam, Depends(prefix_search_param_factory(AssetModel.name, "name_prefix_pattern"))
-]
-QueryUriPatternSearch = Annotated[_SearchParam, Depends(search_param_factory(AssetModel.uri, "uri_pattern"))]
-QueryUriPrefixPatternSearch = Annotated[
- _PrefixSearchParam, Depends(prefix_search_param_factory(AssetModel.uri, "uri_prefix_pattern"))
-]
-QueryAssetAliasNamePatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(AssetAliasModel.name, "name_pattern"))
-]
-QueryAssetAliasNamePrefixPatternSearch = Annotated[
- _PrefixSearchParam, Depends(prefix_search_param_factory(AssetAliasModel.name, "name_prefix_pattern"))
-]
-QueryAssetDagIdPatternSearch = Annotated[
- _DagIdAssetReferenceFilter, Depends(_DagIdAssetReferenceFilter.depends)
-]
-QueryPartitionedDagRunHasCreatedDagRunIdFilter = Annotated[
- FilterParam[bool | None],
- Depends(
- filter_param_factory(
- AssetPartitionDagRun.created_dag_run_id,
- bool | None,
- FilterOptionEnum.IS_NONE,
- filter_name="has_created_dag_run_id",
- transform_callable=lambda v: not v if v is not None else None,
- )
- ),
-]
-QueryPartitionedDagRunDagIdFilter = Annotated[
- FilterParam[str | None],
- Depends(
- filter_param_factory(
- AssetPartitionDagRun.target_dag_id,
- str | None,
- filter_name="dag_id",
- )
- ),
-]
-
-# Variables
-QueryVariableKeyPatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(Variable.key, "variable_key_pattern"))
-]
-QueryVariableKeyPrefixPatternSearch = Annotated[
- _PrefixSearchParam,
- Depends(prefix_search_param_factory(Variable.key, "variable_key_prefix_pattern")),
-]
-
-# Pools
-QueryPoolNamePatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(Pool.pool, "pool_name_pattern"))
-]
-QueryPoolNamePrefixPatternSearch = Annotated[
- _PrefixSearchParam, Depends(prefix_search_param_factory(Pool.pool, "pool_name_prefix_pattern"))
-]
-
-
-# UI Shared
-def _optional_boolean(value: bool | None) -> bool | None:
- return value if value is not None else False
-
-
-QueryIncludeUpstream = Annotated[bool, AfterValidator(_optional_boolean)]
-QueryIncludeDownstream = Annotated[bool, AfterValidator(_optional_boolean)]
-
-state_priority: list[None | TaskInstanceState] = [
- TaskInstanceState.FAILED,
- TaskInstanceState.UPSTREAM_FAILED,
- TaskInstanceState.UP_FOR_RETRY,
- TaskInstanceState.UP_FOR_RESCHEDULE,
- TaskInstanceState.RUNNING,
- TaskInstanceState.RESTARTING,
- TaskInstanceState.DEFERRED,
- TaskInstanceState.AWAITING_INPUT,
- TaskInstanceState.QUEUED,
- TaskInstanceState.SCHEDULED,
- None,
- TaskInstanceState.SUCCESS,
- TaskInstanceState.SKIPPED,
- TaskInstanceState.REMOVED,
-]
-
-# Connections
-QueryConnectionIdPatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(Connection.conn_id, "connection_id_pattern"))
-]
-QueryConnectionIdPrefixPatternSearch = Annotated[
- _PrefixSearchParam,
- Depends(prefix_search_param_factory(Connection.conn_id, "connection_id_prefix_pattern")),
-]
-
-# Human in the loop
-QueryHITLDetailDagIdPatternSearch = Annotated[
- _SearchParam,
- Depends(
- search_param_factory(
- TaskInstance.dag_id,
- "dag_id_pattern",
- )
- ),
-]
-QueryHITLDetailDagIdPrefixPatternSearch = Annotated[
- _PrefixSearchParam,
- Depends(
- prefix_search_param_factory(
- TaskInstance.dag_id,
- "dag_id_prefix_pattern",
- )
- ),
-]
-QueryHITLDetailTaskIdPatternSearch = Annotated[
- _SearchParam,
- Depends(
- search_param_factory(
- TaskInstance.task_id,
- "task_id_pattern",
- )
- ),
-]
-QueryHITLDetailTaskIdPrefixPatternSearch = Annotated[
- _PrefixSearchParam,
- Depends(
- prefix_search_param_factory(
- TaskInstance.task_id,
- "task_id_prefix_pattern",
- )
- ),
-]
-QueryHITLDetailTaskIdFilter = Annotated[
- FilterParam[str | None],
- Depends(
- filter_param_factory(
- TaskInstance.task_id,
- str | None,
- filter_name="task_id",
- )
- ),
-]
-QueryHITLDetailMapIndexFilter = Annotated[
- FilterParam[int | None],
- Depends(
- filter_param_factory(
- TaskInstance.map_index,
- int | None,
- filter_name="map_index",
- )
- ),
-]
-QueryHITLDetailSubjectSearch = Annotated[
- _SearchParam,
- Depends(
- search_param_factory(
- HITLDetail.subject,
- "subject_search",
- )
- ),
-]
-QueryHITLDetailBodySearch = Annotated[
- _SearchParam,
- Depends(
- search_param_factory(
- HITLDetail.body,
- "body_search",
- )
- ),
-]
-QueryHITLDetailResponseReceivedFilter = Annotated[
- FilterParam[bool | None],
- Depends(
- filter_param_factory(
- HITLDetail.response_received,
- bool | None,
- filter_name="response_received",
- )
- ),
-]
-QueryHITLDetailRespondedUserIdFilter = Annotated[
- FilterParam[list[str]],
- Depends(
- filter_param_factory(
- HITLDetail.responded_by_user_id,
- list[str],
- FilterOptionEnum.ANY_EQUAL,
- default_factory=list,
- filter_name="responded_by_user_id",
- )
- ),
-]
-QueryHITLDetailRespondedUserNameFilter = Annotated[
- FilterParam[list[str]],
- Depends(
- filter_param_factory(
- HITLDetail.responded_by_user_name,
- list[str],
- FilterOptionEnum.ANY_EQUAL,
- default_factory=list,
- filter_name="responded_by_user_name",
- )
- ),
-]
-
-# Parse Import Errors
-QueryParseImportErrorFilenamePatternSearch = Annotated[
- _SearchParam, Depends(search_param_factory(ParseImportError.filename, "filename_pattern"))
-]
-QueryParseImportErrorFilenamePrefixPatternSearch = Annotated[
- _PrefixSearchParam,
- Depends(prefix_search_param_factory(ParseImportError.filename, "filename_prefix_pattern")),
-]
-QueryParseImportErrorFilenameFilter = Annotated[
- FilterParam,
- Depends(
- filter_param_factory(
- ParseImportError.filename,
- str | None,
- filter_name="filename",
- description="Exact filename match. Returns only the import error for this specific file path.",
- )
- ),
-]
-QueryParseImportErrorBundleNameFilter = Annotated[
- FilterParam,
- Depends(
- filter_param_factory(
- ParseImportError.bundle_name,
- str | None,
- filter_name="bundle_name",
- description="Exact bundle name match. Returns only import errors from this specific bundle.",
- )
- ),
-]
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/__init__.py b/airflow-core/src/airflow/api_fastapi/common/parameters/__init__.py
new file mode 100644
index 00000000000..263f8ca67a3
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/__init__.py
@@ -0,0 +1,152 @@
+# 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.
+
+"""Query parameter helpers for the FastAPI API, re-exported for backward compatibility."""
+
+from __future__ import annotations
+
+from airflow.api_fastapi.common.parameters.asset import (
+ QueryAssetAliasNamePatternSearch as QueryAssetAliasNamePatternSearch,
+ QueryAssetAliasNamePrefixPatternSearch as QueryAssetAliasNamePrefixPatternSearch,
+ QueryAssetDagIdPatternSearch as QueryAssetDagIdPatternSearch,
+ QueryAssetDependencyFilter as QueryAssetDependencyFilter,
+ QueryAssetNamePatternSearch as QueryAssetNamePatternSearch,
+ QueryAssetNamePrefixPatternSearch as QueryAssetNamePrefixPatternSearch,
+ QueryConsumingAssetPatternSearch as QueryConsumingAssetPatternSearch,
+ QueryHasAssetScheduleFilter as QueryHasAssetScheduleFilter,
+ QueryPartitionedDagRunDagIdFilter as QueryPartitionedDagRunDagIdFilter,
+ QueryPartitionedDagRunHasCreatedDagRunIdFilter as QueryPartitionedDagRunHasCreatedDagRunIdFilter,
+ QueryUriPatternSearch as QueryUriPatternSearch,
+ QueryUriPrefixPatternSearch as QueryUriPrefixPatternSearch,
+)
+from airflow.api_fastapi.common.parameters.base import (
+ BaseParam as BaseParam,
+ LimitFilter as LimitFilter,
+ OffsetFilter as OffsetFilter,
+ QueryLimit as QueryLimit,
+ QueryOffset as QueryOffset,
+)
+from airflow.api_fastapi.common.parameters.dag import (
+ QueryBundleNameFilter as QueryBundleNameFilter,
+ QueryBundleVersionFilter as QueryBundleVersionFilter,
+ QueryDagDisplayNamePatternSearch as QueryDagDisplayNamePatternSearch,
+ QueryDagDisplayNamePrefixPatternSearch as QueryDagDisplayNamePrefixPatternSearch,
+ QueryDagIdPatternSearch as QueryDagIdPatternSearch,
+ QueryDagIdPatternSearchWithNone as QueryDagIdPatternSearchWithNone,
+ QueryDagIdPrefixPatternSearch as QueryDagIdPrefixPatternSearch,
+ QueryDagIdPrefixPatternSearchWithNone as QueryDagIdPrefixPatternSearchWithNone,
+ QueryDagTagPatternSearch as QueryDagTagPatternSearch,
+ QueryDagTagPrefixPatternSearch as QueryDagTagPrefixPatternSearch,
+ QueryExcludeStaleFilter as QueryExcludeStaleFilter,
+ QueryFavoriteFilter as QueryFavoriteFilter,
+ QueryHasImportErrorsFilter as QueryHasImportErrorsFilter,
+ QueryOwnersFilter as QueryOwnersFilter,
+ QueryPausedFilter as QueryPausedFilter,
+ QueryTagsFilter as QueryTagsFilter,
+)
+from airflow.api_fastapi.common.parameters.dag_run import (
+ QueryAnyDagRunStateFilter as QueryAnyDagRunStateFilter,
+ QueryDagRunPartitionKeyPrefixSearch as QueryDagRunPartitionKeyPrefixSearch,
+ QueryDagRunPartitionKeySearch as QueryDagRunPartitionKeySearch,
+ QueryDagRunRunTypesFilter as QueryDagRunRunTypesFilter,
+ QueryDagRunStateFilter as QueryDagRunStateFilter,
+ QueryDagRunTriggeringUserPrefixSearch as QueryDagRunTriggeringUserPrefixSearch,
+ QueryDagRunTriggeringUserSearch as QueryDagRunTriggeringUserSearch,
+ QueryDagRunVersionFilter as QueryDagRunVersionFilter,
+ QueryLastDagRunStateFilter as QueryLastDagRunStateFilter,
+ _transform_dag_run_states as _transform_dag_run_states,
+)
+from airflow.api_fastapi.common.parameters.filter import (
+ FilterOptionEnum as FilterOptionEnum,
+ FilterParam as FilterParam,
+ filter_param_factory as filter_param_factory,
+)
+from airflow.api_fastapi.common.parameters.misc import (
+ QueryConnectionIdPatternSearch as QueryConnectionIdPatternSearch,
+ QueryConnectionIdPrefixPatternSearch as QueryConnectionIdPrefixPatternSearch,
+ QueryHITLDetailBodySearch as QueryHITLDetailBodySearch,
+ QueryHITLDetailDagIdPatternSearch as QueryHITLDetailDagIdPatternSearch,
+ QueryHITLDetailDagIdPrefixPatternSearch as QueryHITLDetailDagIdPrefixPatternSearch,
+ QueryHITLDetailMapIndexFilter as QueryHITLDetailMapIndexFilter,
+ QueryHITLDetailRespondedUserIdFilter as QueryHITLDetailRespondedUserIdFilter,
+ QueryHITLDetailRespondedUserNameFilter as QueryHITLDetailRespondedUserNameFilter,
+ QueryHITLDetailResponseReceivedFilter as QueryHITLDetailResponseReceivedFilter,
+ QueryHITLDetailSubjectSearch as QueryHITLDetailSubjectSearch,
+ QueryHITLDetailTaskIdFilter as QueryHITLDetailTaskIdFilter,
+ QueryHITLDetailTaskIdPatternSearch as QueryHITLDetailTaskIdPatternSearch,
+ QueryHITLDetailTaskIdPrefixPatternSearch as QueryHITLDetailTaskIdPrefixPatternSearch,
+ QueryIncludeDownstream as QueryIncludeDownstream,
+ QueryIncludeUpstream as QueryIncludeUpstream,
+ QueryParseImportErrorBundleNameFilter as QueryParseImportErrorBundleNameFilter,
+ QueryParseImportErrorFilenameFilter as QueryParseImportErrorFilenameFilter,
+ QueryParseImportErrorFilenamePatternSearch as QueryParseImportErrorFilenamePatternSearch,
+ QueryParseImportErrorFilenamePrefixPatternSearch as QueryParseImportErrorFilenamePrefixPatternSearch,
+ QueryPendingActionsFilter as QueryPendingActionsFilter,
+ QueryPoolNamePatternSearch as QueryPoolNamePatternSearch,
+ QueryPoolNamePrefixPatternSearch as QueryPoolNamePrefixPatternSearch,
+ QueryVariableKeyPatternSearch as QueryVariableKeyPatternSearch,
+ QueryVariableKeyPrefixPatternSearch as QueryVariableKeyPrefixPatternSearch,
+ state_priority as state_priority,
+)
+from airflow.api_fastapi.common.parameters.range import (
+ DateTimeQuery as DateTimeQuery,
+ OptionalDateTimeQuery as OptionalDateTimeQuery,
+ Range as Range,
+ RangeFilter as RangeFilter,
+ datetime_range_filter_factory as datetime_range_filter_factory,
+ float_range_filter_factory as float_range_filter_factory,
+)
+from airflow.api_fastapi.common.parameters.search import (
+ _PrefixSearchParam as _PrefixSearchParam,
+ _SearchParam as _SearchParam,
+ prefix_search_param_factory as prefix_search_param_factory,
+ search_param_factory as search_param_factory,
+)
+from airflow.api_fastapi.common.parameters.sort import (
+ SortParam as SortParam,
+)
+from airflow.api_fastapi.common.parameters.task_instance import (
+ QueryTIDagVersionFilter as QueryTIDagVersionFilter,
+ QueryTIExecutorFilter as QueryTIExecutorFilter,
+ QueryTIMapIndexFilter as QueryTIMapIndexFilter,
+ QueryTIOperatorFilter as QueryTIOperatorFilter,
+ QueryTIOperatorNamePatternSearch as QueryTIOperatorNamePatternSearch,
+ QueryTIOperatorNamePrefixPatternSearch as QueryTIOperatorNamePrefixPatternSearch,
+ QueryTIPoolFilter as QueryTIPoolFilter,
+ QueryTIPoolNamePatternSearch as QueryTIPoolNamePatternSearch,
+ QueryTIPoolNamePrefixPatternSearch as QueryTIPoolNamePrefixPatternSearch,
+ QueryTIQueueFilter as QueryTIQueueFilter,
+ QueryTIQueueNamePatternSearch as QueryTIQueueNamePatternSearch,
+ QueryTIQueueNamePrefixPatternSearch as QueryTIQueueNamePrefixPatternSearch,
+ QueryTIRenderedMapIndexPatternSearch as QueryTIRenderedMapIndexPatternSearch,
+ QueryTIRenderedMapIndexPrefixPatternSearch as QueryTIRenderedMapIndexPrefixPatternSearch,
+ QueryTIStateFilter as QueryTIStateFilter,
+ QueryTITaskDisplayNamePatternSearch as QueryTITaskDisplayNamePatternSearch,
+ QueryTITaskDisplayNamePrefixPatternSearch as QueryTITaskDisplayNamePrefixPatternSearch,
+ QueryTITaskGroupFilter as QueryTITaskGroupFilter,
+ QueryTITryNumberFilter as QueryTITryNumberFilter,
+)
+from airflow.api_fastapi.common.parameters.xcom import (
+ QueryXComDagDisplayNamePatternSearch as QueryXComDagDisplayNamePatternSearch,
+ QueryXComDagDisplayNamePrefixPatternSearch as QueryXComDagDisplayNamePrefixPatternSearch,
+ QueryXComKeyPatternSearch as QueryXComKeyPatternSearch,
+ QueryXComKeyPrefixPatternSearch as QueryXComKeyPrefixPatternSearch,
+ QueryXComRunIdPatternSearch as QueryXComRunIdPatternSearch,
+ QueryXComRunIdPrefixPatternSearch as QueryXComRunIdPrefixPatternSearch,
+ QueryXComTaskIdPatternSearch as QueryXComTaskIdPatternSearch,
+ QueryXComTaskIdPrefixPatternSearch as QueryXComTaskIdPrefixPatternSearch,
+)
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/asset.py b/airflow-core/src/airflow/api_fastapi/common/parameters/asset.py
new file mode 100644
index 00000000000..c3ca9ccf72a
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/asset.py
@@ -0,0 +1,237 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import (
+ TYPE_CHECKING,
+ Annotated,
+)
+
+from fastapi import Depends, Query
+from sqlalchemy import or_, select as sql_select
+
+from airflow.api_fastapi.common.parameters.base import BaseParam
+from airflow.api_fastapi.common.parameters.filter import FilterOptionEnum, FilterParam, filter_param_factory
+from airflow.api_fastapi.common.parameters.search import (
+ _LIKE_ESCAPE_CHAR,
+ _escape_like_pattern,
+ _PrefixSearchParam,
+ _SearchParam,
+ prefix_search_param_factory,
+ search_param_factory,
+)
+from airflow.models.asset import (
+ AssetAliasModel,
+ AssetEvent,
+ AssetModel,
+ AssetPartitionDagRun,
+ DagScheduleAssetReference,
+ TaskInletAssetReference,
+ TaskOutletAssetReference,
+ association_table,
+)
+from airflow.models.dag import DagModel
+from airflow.models.dagrun import DagRun
+
+if TYPE_CHECKING:
+ from sqlalchemy.sql import Select
+
+
+class _DagIdAssetReferenceFilter(BaseParam[list[str]]):
+ """Search on dag_id."""
+
+ def __init__(self, skip_none: bool = True) -> None:
+ super().__init__(skip_none=skip_none)
+
+ @classmethod
+ def depends(cls, dag_ids: list[str] = Query(None)) -> _DagIdAssetReferenceFilter:
+ # needed to handle cases where dag_ids=a1,b1
+ if dag_ids and len(dag_ids) == 1 and "," in dag_ids[0]:
+ dag_ids = dag_ids[0].split(",")
+ return cls().set_value(dag_ids)
+
+ def to_orm(self, select: Select) -> Select:
+ if self.value is None and self.skip_none:
+ return select
+
+ # At this point, self.value is either a list[str] or None -> coerce falsy None to an empty list
+ dag_ids = self.value or []
+ return select.where(
+ (AssetModel.scheduled_dags.any(DagScheduleAssetReference.dag_id.in_(dag_ids)))
+ | (AssetModel.producing_tasks.any(TaskOutletAssetReference.dag_id.in_(dag_ids)))
+ | (AssetModel.consuming_tasks.any(TaskInletAssetReference.dag_id.in_(dag_ids)))
+ )
+
+
+class _HasAssetScheduleFilter(BaseParam[bool]):
+ """Filter Dags that have asset-based scheduling."""
+
+ def to_orm(self, select: Select) -> Select:
+ if self.value is None and self.skip_none:
+ return select
+
+ asset_ref_subquery = sql_select(DagScheduleAssetReference.dag_id).distinct()
+
+ if self.value:
+ # Filter Dags that have asset-based scheduling
+ return select.where(DagModel.dag_id.in_(asset_ref_subquery))
+
+ # Filter Dags that do NOT have asset-based scheduling
+ return select.where(DagModel.dag_id.notin_(asset_ref_subquery))
+
+ @classmethod
+ def depends(
+ cls,
+ has_asset_schedule: bool | None = Query(None, description="Filter Dags with asset-based scheduling"),
+ ) -> _HasAssetScheduleFilter:
+ return cls().set_value(has_asset_schedule)
+
+
+class _AssetDependencyFilter(BaseParam[str]):
+ """Filter Dags by specific asset dependencies."""
+
+ def to_orm(self, select: Select) -> Select:
+ if self.value is None:
+ return select
+
+ escaped = _escape_like_pattern(self.value)
+ asset_dag_subquery = (
+ sql_select(DagScheduleAssetReference.dag_id)
+ .join(AssetModel, DagScheduleAssetReference.asset_id == AssetModel.id)
+ .where(
+ or_(
+ AssetModel.name.ilike(f"%{escaped}%", escape=_LIKE_ESCAPE_CHAR),
+ AssetModel.uri.ilike(f"%{escaped}%", escape=_LIKE_ESCAPE_CHAR),
+ )
+ )
+ .distinct()
+ )
+
+ return select.where(DagModel.dag_id.in_(asset_dag_subquery))
+
+ @classmethod
+ def depends(
+ cls,
+ asset_dependency: str | None = Query(
+ None, description="Filter Dags by asset dependency (name or URI)"
+ ),
+ ) -> _AssetDependencyFilter:
+ return cls().set_value(asset_dependency)
+
+
+QueryHasAssetScheduleFilter = Annotated[_HasAssetScheduleFilter, Depends(_HasAssetScheduleFilter.depends)]
+
+QueryAssetDependencyFilter = Annotated[_AssetDependencyFilter, Depends(_AssetDependencyFilter.depends)]
+
+
+class _ConsumingAssetFilter(BaseParam[str | None]):
+ """Filter Dag runs by consuming asset (name or URI)."""
+
+ def to_orm(self, select: Select) -> Select:
+ if not self.value:
+ return select
+
+ escaped = _escape_like_pattern(self.value)
+ event_subquery = (
+ sql_select(AssetEvent.id)
+ .join(AssetModel, AssetEvent.asset_id == AssetModel.id)
+ .where(
+ or_(
+ AssetModel.name.ilike(f"%{escaped}%", escape=_LIKE_ESCAPE_CHAR),
+ AssetModel.uri.ilike(f"%{escaped}%", escape=_LIKE_ESCAPE_CHAR),
+ )
+ )
+ .distinct()
+ )
+
+ dagrun_subquery = (
+ sql_select(association_table.c.dag_run_id)
+ .where(association_table.c.event_id.in_(event_subquery))
+ .distinct()
+ )
+
+ return select.where(DagRun.id.in_(dagrun_subquery))
+
+ @classmethod
+ def depends(
+ cls,
+ consuming_asset_pattern: str | None = Query(
+ None,
+ description=(
+ "Case-insensitive substring match against the consuming asset name or URI. "
+ "Unlike the wildcard `*_pattern` parameters, `%` and `_` are matched literally, "
+ "`|` is not an OR separator, and `~` does not match everything."
+ ),
+ ),
+ ) -> _ConsumingAssetFilter:
+ return cls().set_value(consuming_asset_pattern)
+
+
+QueryConsumingAssetPatternSearch = Annotated[_ConsumingAssetFilter, Depends(_ConsumingAssetFilter.depends)]
+
+
+# Assets
+QueryAssetNamePatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(AssetModel.name, "name_pattern"))
+]
+
+QueryAssetNamePrefixPatternSearch = Annotated[
+ _PrefixSearchParam, Depends(prefix_search_param_factory(AssetModel.name, "name_prefix_pattern"))
+]
+
+QueryUriPatternSearch = Annotated[_SearchParam, Depends(search_param_factory(AssetModel.uri, "uri_pattern"))]
+
+QueryUriPrefixPatternSearch = Annotated[
+ _PrefixSearchParam, Depends(prefix_search_param_factory(AssetModel.uri, "uri_prefix_pattern"))
+]
+
+QueryAssetAliasNamePatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(AssetAliasModel.name, "name_pattern"))
+]
+
+QueryAssetAliasNamePrefixPatternSearch = Annotated[
+ _PrefixSearchParam, Depends(prefix_search_param_factory(AssetAliasModel.name, "name_prefix_pattern"))
+]
+
+QueryAssetDagIdPatternSearch = Annotated[
+ _DagIdAssetReferenceFilter, Depends(_DagIdAssetReferenceFilter.depends)
+]
+
+QueryPartitionedDagRunHasCreatedDagRunIdFilter = Annotated[
+ FilterParam[bool | None],
+ Depends(
+ filter_param_factory(
+ AssetPartitionDagRun.created_dag_run_id,
+ bool | None,
+ FilterOptionEnum.IS_NONE,
+ filter_name="has_created_dag_run_id",
+ transform_callable=lambda v: not v if v is not None else None,
+ )
+ ),
+]
+
+QueryPartitionedDagRunDagIdFilter = Annotated[
+ FilterParam[str | None],
+ Depends(
+ filter_param_factory(
+ AssetPartitionDagRun.target_dag_id,
+ str | None,
+ filter_name="dag_id",
+ )
+ ),
+]
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/base.py b/airflow-core/src/airflow/api_fastapi/common/parameters/base.py
new file mode 100644
index 00000000000..b95732e69bb
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/base.py
@@ -0,0 +1,139 @@
+# 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 abc import ABC, abstractmethod
+from typing import (
+ TYPE_CHECKING,
+ Annotated,
+ Any,
+ TypeVar,
+)
+
+from fastapi import Depends
+from pydantic import NonNegativeInt
+from sqlalchemy import String
+from sqlalchemy.ext.compiler import compiles
+from sqlalchemy.sql.functions import FunctionElement
+
+from airflow.api_fastapi.core_api.base import OrmClause
+from airflow.configuration import conf
+from airflow.typing_compat import Self
+
+if TYPE_CHECKING:
+ from sqlalchemy.orm.attributes import InstrumentedAttribute
+ from sqlalchemy.sql import ColumnElement, Select
+ from sqlalchemy.sql.compiler import SQLCompiler
+
+
+T = TypeVar("T")
+
+
+_FALLBACK_PAGE_LIMIT: int = conf.getint("api", "fallback_page_limit")
+
+
+class _MySQLCollate(FunctionElement):
+ """
+ Wraps a SQL expression so that on MySQL it is emitted with an explicit ``COLLATE`` clause.
+
+ On every other dialect the expression is passed through unchanged.
+
+ This is needed when a computed expression (e.g. a ``CASE … END`` that mixes
+ a stored ``VARCHAR`` column with a ``CAST(integer AS CHAR)``) ends up with
+ MySQL coercibility ``NONE`` because the two branches carry different implicit
+ collations. Comparing such an expression with a bound parameter fails with
+ "Illegal mix of collations". Wrapping the expression in an explicit
+ ``COLLATE`` gives it ``EXPLICIT`` coercibility, which MySQL accepts in all
+ comparison operators.
+ """
+
+ type = String()
+ inherit_cache = True
+
+ def __init__(self, expr: ColumnElement[Any], collation: str) -> None:
+ super().__init__(expr)
+ self.collation = collation
+
+
+@compiles(_MySQLCollate)
+def _compile_mysql_collate_default(element: _MySQLCollate, compiler: SQLCompiler, **kw: Any) -> str:
+ """Non-MySQL: render the inner expression without any COLLATE clause."""
+ (expr,) = element.clauses
+ return compiler.process(expr, **kw)
+
+
+@compiles(_MySQLCollate, "mysql")
+def _compile_mysql_collate_mysql(element: _MySQLCollate, compiler: SQLCompiler, **kw: Any) -> str:
+ """MySQL: wrap the inner expression with the requested COLLATE clause."""
+ (expr,) = element.clauses
+ inner = compiler.process(expr, **kw)
+ return f"({inner}) COLLATE {element.collation}"
+
+
+class BaseParam(OrmClause[T], ABC):
+ """Base class for path or query parameters with ORM transformation."""
+
+ def __init__(self, value: T | None = None, skip_none: bool = True) -> None:
+ super().__init__(value)
+ self.attribute: ColumnElement | InstrumentedAttribute | None = None
+ self.skip_none = skip_none
+
+ def set_value(self, value: T | None) -> Self:
+ self.value = value
+ return self
+
+ @classmethod
+ @abstractmethod
+ def depends(cls, *args: Any, **kwargs: Any) -> Self:
+ pass
+
+
+class LimitFilter(BaseParam[NonNegativeInt]):
+ """Filter on the limit."""
+
+ def to_orm(self, select: Select) -> Select:
+ if self.value is None and self.skip_none:
+ return select
+
+ return select.limit(self.value)
+
+ @classmethod
+ def depends(cls, limit: NonNegativeInt = _FALLBACK_PAGE_LIMIT) -> LimitFilter:
+ return cls().set_value(min(limit, conf.getint("api", "maximum_page_limit")))
+
+
+class OffsetFilter(BaseParam[NonNegativeInt]):
+ """Filter on offset."""
+
+ def to_orm(self, select: Select) -> Select:
+ if self.value is None and self.skip_none:
+ return select
+ return select.offset(self.value)
+
+ @classmethod
+ def depends(
+ cls,
+ offset: NonNegativeInt = 0,
+ ) -> OffsetFilter:
+ return cls().set_value(offset)
+
+
+# Dag
+QueryLimit = Annotated[LimitFilter, Depends(LimitFilter.depends)]
+
+QueryOffset = Annotated[OffsetFilter, Depends(OffsetFilter.depends)]
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/dag.py b/airflow-core/src/airflow/api_fastapi/common/parameters/dag.py
new file mode 100644
index 00000000000..ff4a0e25a37
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/dag.py
@@ -0,0 +1,213 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import (
+ TYPE_CHECKING,
+ Annotated,
+ Literal,
+)
+
+from fastapi import Depends, Query
+from pydantic import BaseModel
+from sqlalchemy import and_, not_, or_, select as sql_select
+
+from airflow.api_fastapi.common.parameters.base import BaseParam, T
+from airflow.api_fastapi.common.parameters.filter import FilterParam, filter_param_factory
+from airflow.api_fastapi.common.parameters.search import (
+ _LIKE_ESCAPE_CHAR,
+ _escape_like_pattern,
+ _PrefixSearchParam,
+ _SearchParam,
+ prefix_search_param_factory,
+ search_param_factory,
+)
+from airflow.api_fastapi.core_api.security import GetUserDep
+from airflow.models.dag import DagModel, DagTag
+from airflow.models.dag_favorite import DagFavorite
+
+if TYPE_CHECKING:
+ from sqlalchemy.sql import Select
+
+
+class _FavoriteFilter(BaseParam[bool]):
+ """Filter Dags by favorite status."""
+
+ def __init__(self, user_id: str, value: T | None = None, skip_none: bool = True) -> None:
+ super().__init__(skip_none=skip_none)
+ self.user_id = user_id
+
+ def to_orm(self, select_stmt: Select) -> Select:
+ if self.value is None and self.skip_none:
+ return select_stmt
+
+ if self.value:
+ select_stmt = select_stmt.join(DagFavorite, DagFavorite.dag_id == DagModel.dag_id).where(
+ DagFavorite.user_id == self.user_id
+ )
+ else:
+ select_stmt = select_stmt.where(
+ not_(
+ sql_select(DagFavorite)
+ .where(and_(DagFavorite.dag_id == DagModel.dag_id, DagFavorite.user_id == self.user_id))
+ .exists()
+ )
+ )
+
+ return select_stmt
+
+ @classmethod
+ def depends(cls, user: GetUserDep, is_favorite: bool | None = Query(None)) -> _FavoriteFilter:
+ instance = cls(user_id=str(user.get_id())).set_value(is_favorite)
+ return instance
+
+
+class _ExcludeStaleFilter(BaseParam[bool]):
+ """Filter on is_stale."""
+
+ def to_orm(self, select: Select) -> Select:
+ if self.value and self.skip_none:
+ return select.where(DagModel.is_stale != self.value)
+ return select
+
+ @classmethod
+ def depends(cls, exclude_stale: bool = True) -> _ExcludeStaleFilter:
+ return cls().set_value(exclude_stale)
+
+
+class _TagFilterModel(BaseModel):
+ """Tag Filter Model with a match mode parameter."""
+
+ tags: list[str]
+ tags_match_mode: Literal["any", "all"] | None
+
+
+class _TagsFilter(BaseParam[_TagFilterModel]):
+ """Filter on tags."""
+
+ def to_orm(self, select: Select) -> Select:
+ if self.skip_none is False:
+ raise ValueError(f"Cannot set 'skip_none' to False on a {type(self)}")
+
+ if not self.value or not self.value.tags:
+ return select
+
+ conditions = [DagModel.tags.any(DagTag.name == tag) for tag in self.value.tags]
+ operator = or_ if not self.value.tags_match_mode or self.value.tags_match_mode == "any" else and_
+ return select.where(operator(*conditions))
+
+ @classmethod
+ def depends(
+ cls,
+ tags: list[str] = Query(default_factory=list),
+ tags_match_mode: Literal["any", "all"] | None = None,
+ ) -> _TagsFilter:
+ return cls().set_value(_TagFilterModel(tags=tags, tags_match_mode=tags_match_mode))
+
+
+class _OwnersFilter(BaseParam[list[str]]):
+ """Filter on owners."""
+
+ def to_orm(self, select: Select) -> Select:
+ if self.skip_none is False:
+ raise ValueError(f"Cannot set 'skip_none' to False on a {type(self)}")
+
+ if not self.value:
+ return select
+
+ conditions = [
+ DagModel.owners.ilike(f"%{_escape_like_pattern(owner)}%", escape=_LIKE_ESCAPE_CHAR)
+ for owner in self.value
+ ]
+ return select.where(or_(*conditions))
+
+ @classmethod
+ def depends(cls, owners: list[str] = Query(default_factory=list)) -> _OwnersFilter:
+ return cls().set_value(owners)
+
+
+QueryPausedFilter = Annotated[
+ FilterParam[bool | None],
+ Depends(filter_param_factory(DagModel.is_paused, bool | None, filter_name="paused")),
+]
+
+QueryHasImportErrorsFilter = Annotated[
+ FilterParam[bool | None],
+ Depends(
+ filter_param_factory(
+ DagModel.has_import_errors,
+ bool | None,
+ filter_name="has_import_errors",
+ description="Filter Dags by having import errors. Only Dags that have been successfully loaded before will be returned.",
+ )
+ ),
+]
+
+QueryFavoriteFilter = Annotated[_FavoriteFilter, Depends(_FavoriteFilter.depends)]
+
+QueryExcludeStaleFilter = Annotated[_ExcludeStaleFilter, Depends(_ExcludeStaleFilter.depends)]
+
+QueryDagIdPatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(DagModel.dag_id, "dag_id_pattern"))
+]
+
+QueryDagIdPrefixPatternSearch = Annotated[
+ _PrefixSearchParam, Depends(prefix_search_param_factory(DagModel.dag_id, "dag_id_prefix_pattern"))
+]
+
+QueryDagDisplayNamePatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(DagModel.dag_display_name, "dag_display_name_pattern"))
+]
+
+QueryDagDisplayNamePrefixPatternSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(prefix_search_param_factory(DagModel.dag_display_name, "dag_display_name_prefix_pattern")),
+]
+
+QueryBundleNameFilter = Annotated[
+ FilterParam[str | None],
+ Depends(filter_param_factory(DagModel.bundle_name, str | None, filter_name="bundle_name")),
+]
+
+QueryBundleVersionFilter = Annotated[
+ FilterParam[str | None],
+ Depends(filter_param_factory(DagModel.bundle_version, str | None, filter_name="bundle_version")),
+]
+
+QueryDagIdPatternSearchWithNone = Annotated[
+ _SearchParam, Depends(search_param_factory(DagModel.dag_id, "dag_id_pattern", False))
+]
+
+QueryDagIdPrefixPatternSearchWithNone = Annotated[
+ _PrefixSearchParam,
+ Depends(prefix_search_param_factory(DagModel.dag_id, "dag_id_prefix_pattern", False)),
+]
+
+QueryTagsFilter = Annotated[_TagsFilter, Depends(_TagsFilter.depends)]
+
+QueryOwnersFilter = Annotated[_OwnersFilter, Depends(_OwnersFilter.depends)]
+
+
+# DagTags
+QueryDagTagPatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(DagTag.name, "tag_name_pattern"))
+]
+
+QueryDagTagPrefixPatternSearch = Annotated[
+ _PrefixSearchParam, Depends(prefix_search_param_factory(DagTag.name, "tag_name_prefix_pattern"))
+]
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/dag_run.py b/airflow-core/src/airflow/api_fastapi/common/parameters/dag_run.py
new file mode 100644
index 00000000000..533356c6e8f
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/dag_run.py
@@ -0,0 +1,171 @@
+# 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 Iterable
+from typing import (
+ TYPE_CHECKING,
+ Annotated,
+)
+
+from fastapi import Depends, HTTPException, Query
+from sqlalchemy import select as sql_select
+from sqlalchemy.orm import aliased
+
+from airflow.api_fastapi.common.parameters.base import BaseParam
+from airflow.api_fastapi.common.parameters.filter import FilterOptionEnum, FilterParam, filter_param_factory
+from airflow.api_fastapi.common.parameters.search import (
+ _PrefixSearchParam,
+ _SearchParam,
+ prefix_search_param_factory,
+ search_param_factory,
+)
+from airflow.api_fastapi.compat import HTTP_422_UNPROCESSABLE_CONTENT
+from airflow.models.dag import DagModel
+from airflow.models.dag_version import DagVersion
+from airflow.models.dagrun import DagRun
+from airflow.utils.state import DagRunState
+from airflow.utils.types import DagRunType
+
+if TYPE_CHECKING:
+ from sqlalchemy.sql import Select
+
+
+class _AnyDagRunStateFilter(BaseParam[DagRunState | None]):
+ """Filter Dags that have any DagRun in the given state, not only the latest one."""
+
+ def to_orm(self, select: Select) -> Select:
+ if self.value is None and self.skip_none:
+ return select
+
+ # Alias DagRun so this EXISTS subquery cannot auto-correlate to a DagRun the outer query
+ # may already reference (e.g. the last_dag_run_state filter), which would strip the
+ # subquery's FROM and raise. EXISTS resolves each Dag via the (dag_id, state) index.
+ any_run = aliased(DagRun)
+ has_run_in_state = (
+ sql_select(any_run.dag_id)
+ .where(any_run.dag_id == DagModel.dag_id, any_run.state == self.value)
+ .exists()
+ )
+ return select.where(has_run_in_state)
+
+ @classmethod
+ def depends(
+ cls,
+ dag_run_state: DagRunState | None = Query(
+ None,
+ description="Filter Dags that have any DagRun in the given state.",
+ ),
+ ) -> _AnyDagRunStateFilter:
+ return cls().set_value(dag_run_state)
+
+
+# DagRun
+QueryLastDagRunStateFilter = Annotated[
+ FilterParam[DagRunState | None],
+ Depends(filter_param_factory(DagRun.state, DagRunState | None, filter_name="last_dag_run_state")),
+]
+
+QueryAnyDagRunStateFilter = Annotated[_AnyDagRunStateFilter, Depends(_AnyDagRunStateFilter.depends)]
+
+
+def _transform_dag_run_states(states: Iterable[str] | None) -> list[DagRunState | None] | None:
+ try:
+ if not states:
+ return None
+ return [None if s in ("none", None) else DagRunState(s) for s in states]
+ except ValueError:
+ raise HTTPException(
+ status_code=HTTP_422_UNPROCESSABLE_CONTENT,
+ detail=f"Invalid value for state. Valid values are {', '.join(DagRunState)}",
+ )
+
+
+QueryDagRunStateFilter = Annotated[
+ FilterParam[list[str]],
+ Depends(
+ filter_param_factory(
+ DagRun.state,
+ list[str],
+ FilterOptionEnum.ANY_EQUAL,
+ default_factory=list,
+ transform_callable=_transform_dag_run_states,
+ )
+ ),
+]
+
+
+def _transform_dag_run_types(types: list[str] | None) -> list[DagRunType | None] | None:
+ try:
+ if not types:
+ return None
+ return [None if run_type in ("none", None) else DagRunType(run_type) for run_type in types]
+ except ValueError:
+ raise HTTPException(
+ status_code=HTTP_422_UNPROCESSABLE_CONTENT,
+ detail=f"Invalid value for run type. Valid values are {', '.join(DagRunType)}",
+ )
+
+
+QueryDagRunRunTypesFilter = Annotated[
+ FilterParam[list[str]],
+ Depends(
+ filter_param_factory(
+ attribute=DagRun.run_type,
+ _type=list[str],
+ filter_option=FilterOptionEnum.ANY_EQUAL,
+ default_factory=list,
+ transform_callable=_transform_dag_run_types,
+ )
+ ),
+]
+
+
+QueryDagRunTriggeringUserSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(DagRun.triggering_user_name, "triggering_user"))
+]
+
+QueryDagRunTriggeringUserPrefixSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(prefix_search_param_factory(DagRun.triggering_user_name, "triggering_user_prefix")),
+]
+
+QueryDagRunPartitionKeySearch = Annotated[
+ _SearchParam,
+ Depends(search_param_factory(DagRun.partition_key, "partition_key_pattern", pipe_as_or=False)),
+]
+
+QueryDagRunPartitionKeyPrefixSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(
+ prefix_search_param_factory(DagRun.partition_key, "partition_key_prefix_pattern", pipe_as_or=False)
+ ),
+]
+
+QueryDagRunVersionFilter = Annotated[
+ FilterParam[list[int]],
+ Depends(
+ filter_param_factory(
+ DagVersion.version_number,
+ list[int],
+ FilterOptionEnum.ANY_EQUAL,
+ default_factory=list,
+ filter_name="dag_version",
+ )
+ ),
+]
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/filter.py b/airflow-core/src/airflow/api_fastapi/common/parameters/filter.py
new file mode 100644
index 00000000000..8869af8f7d3
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/filter.py
@@ -0,0 +1,156 @@
+# 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 Callable
+from enum import Enum
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ cast,
+)
+
+from fastapi import HTTPException, Query
+from sqlalchemy import and_, or_
+
+from airflow.api_fastapi.common.parameters.base import BaseParam, T
+from airflow.typing_compat import Self
+
+if TYPE_CHECKING:
+ from sqlalchemy.orm.attributes import InstrumentedAttribute
+ from sqlalchemy.sql import ColumnElement, Select
+
+
+class FilterOptionEnum(Enum):
+ """Filter options for FilterParam."""
+
+ EQUAL = "eq"
+ NOT_EQUAL = "ne"
+ LESS_THAN = "lt"
+ LESS_THAN_EQUAL = "le"
+ GREATER_THAN = "gt"
+ GREATER_THAN_EQUAL = "ge"
+ IN = "in"
+ NOT_IN = "not_in"
+ ANY_EQUAL = "any_eq"
+ ALL_EQUAL = "all_eq"
+ IS_NONE = "is_none"
+ CONTAINS = "contains"
+
+
+class FilterParam(BaseParam[T]):
+ """Filter on attribute."""
+
+ def __init__(
+ self,
+ attribute: InstrumentedAttribute,
+ value: T | None = None,
+ filter_option: FilterOptionEnum = FilterOptionEnum.EQUAL,
+ skip_none: bool = True,
+ ) -> None:
+ super().__init__(value, skip_none)
+ self.attribute: InstrumentedAttribute = attribute
+ self.value: T | None = value
+ self.filter_option: FilterOptionEnum = filter_option
+
+ def to_orm(self, select: Select) -> Select:
+ if isinstance(self.value, (list, str)) and not self.value and self.skip_none:
+ return select
+ if self.value is None and self.skip_none:
+ return select
+
+ if isinstance(self.value, list):
+ if self.filter_option == FilterOptionEnum.IN:
+ return select.where(self.attribute.in_(self.value))
+ if self.filter_option == FilterOptionEnum.NOT_IN:
+ return select.where(self.attribute.notin_(self.value))
+ if self.filter_option == FilterOptionEnum.ANY_EQUAL:
+ conditions = [self.attribute == val for val in self.value]
+ return select.where(or_(*conditions))
+ if self.filter_option == FilterOptionEnum.ALL_EQUAL:
+ conditions = [self.attribute == val for val in self.value]
+ return select.where(and_(*conditions))
+ raise HTTPException(
+ 400, f"Invalid filter option {self.filter_option} for list value {self.value}"
+ )
+
+ if self.filter_option == FilterOptionEnum.EQUAL:
+ return select.where(self.attribute == self.value)
+ if self.filter_option == FilterOptionEnum.NOT_EQUAL:
+ return select.where(self.attribute != self.value)
+ if self.filter_option == FilterOptionEnum.LESS_THAN:
+ return select.where(self.attribute < self.value)
+ if self.filter_option == FilterOptionEnum.LESS_THAN_EQUAL:
+ return select.where(self.attribute <= self.value)
+ if self.filter_option == FilterOptionEnum.GREATER_THAN:
+ return select.where(self.attribute > self.value)
+ if self.filter_option == FilterOptionEnum.GREATER_THAN_EQUAL:
+ return select.where(self.attribute >= self.value)
+ if self.filter_option == FilterOptionEnum.IS_NONE:
+ if self.value is None:
+ return select
+ if self.value is False:
+ return select.where(self.attribute.is_not(None))
+ if self.value is True:
+ return select.where(self.attribute.is_(None))
+ if self.filter_option == FilterOptionEnum.CONTAINS:
+ # For JSON/JSONB columns, convert to text before applying LIKE
+ from sqlalchemy import Text, cast
+
+ if str(self.attribute.type).upper() in ("JSON", "JSONB"):
+ return select.where(cast(self.attribute, Text).contains(self.value))
+ return select.where(self.attribute.contains(self.value))
+ raise ValueError(f"Invalid filter option {self.filter_option} for value {self.value}")
+
+ @classmethod
+ def depends(cls, *args: Any, **kwargs: Any) -> Self:
+ raise NotImplementedError("Use filter_param_factory instead , depends is not implemented.")
+
+
+def filter_param_factory(
+ attribute: ColumnElement | InstrumentedAttribute,
+ _type: type,
+ filter_option: FilterOptionEnum = FilterOptionEnum.EQUAL,
+ filter_name: str | None = None,
+ default_value: T | None = None,
+ default_factory: Callable[[], T | None] | None = None,
+ skip_none: bool = True,
+ transform_callable: Callable[[T | None], Any] | None = None,
+ *,
+ description: str | None = None,
+) -> Callable[[T | None], FilterParam[T | None]]:
+ # if filter_name is not provided, use the attribute name as the default
+ filter_name = filter_name or getattr(attribute, "name", str(attribute))
+ # can only set either default_value or default_factory
+ query = (
+ Query(alias=filter_name, default_factory=default_factory, description=description)
+ if default_factory is not None
+ else Query(alias=filter_name, default=default_value, description=description)
+ )
+
+ def depends_filter(value: T | None = query) -> FilterParam[T | None]:
+ if transform_callable:
+ value = transform_callable(value)
+ # Cast to InstrumentedAttribute for type compatibility
+ attr = cast("InstrumentedAttribute", attribute)
+ return FilterParam(attr, value, filter_option, skip_none)
+
+ # add type hint to value at runtime
+ depends_filter.__annotations__["value"] = _type
+
+ return depends_filter
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/misc.py b/airflow-core/src/airflow/api_fastapi/common/parameters/misc.py
new file mode 100644
index 00000000000..d718d2e132e
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/misc.py
@@ -0,0 +1,302 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import (
+ TYPE_CHECKING,
+ Annotated,
+)
+
+from fastapi import Depends, Query
+from pydantic import AfterValidator
+from sqlalchemy import func, select as sql_select
+
+from airflow.api_fastapi.common.parameters.base import BaseParam
+from airflow.api_fastapi.common.parameters.filter import FilterOptionEnum, FilterParam, filter_param_factory
+from airflow.api_fastapi.common.parameters.search import (
+ _PrefixSearchParam,
+ _SearchParam,
+ prefix_search_param_factory,
+ search_param_factory,
+)
+from airflow.models.connection import Connection
+from airflow.models.dag import DagModel
+from airflow.models.errors import ParseImportError
+from airflow.models.hitl import HITLDetail
+from airflow.models.pool import Pool
+from airflow.models.taskinstance import TaskInstance
+from airflow.models.variable import Variable
+from airflow.utils.state import TaskInstanceState
+
+if TYPE_CHECKING:
+ from sqlalchemy.sql import Select
+
+
+class _PendingActionsFilter(BaseParam[bool]):
+ """Filter Dags by having pending HITL actions (more than 1)."""
+
+ def to_orm(self, select: Select) -> Select:
+ if self.value is None and self.skip_none:
+ return select
+
+ from airflow.models.hitl import HITLDetail
+ from airflow.models.taskinstance import TaskInstance
+
+ # Join with HITLDetail and TaskInstance to find Dags
+ pending_actions_count_subquery = (
+ sql_select(func.count(HITLDetail.ti_id))
+ .join(TaskInstance, HITLDetail.ti_id == TaskInstance.id)
+ .where(
+ HITLDetail.responded_at.is_(None),
+ TaskInstance.state.in_((TaskInstanceState.DEFERRED, TaskInstanceState.AWAITING_INPUT)),
+ )
+ .where(TaskInstance.dag_id == DagModel.dag_id)
+ .scalar_subquery()
+ )
+
+ if self.value is True:
+ # Filter to show only Dags with pending actions
+ where_clause = pending_actions_count_subquery >= 1
+ else:
+ # Filter to show only Dags without pending actions
+ where_clause = pending_actions_count_subquery == 0
+
+ return select.where(where_clause)
+
+ @classmethod
+ def depends(cls, has_pending_actions: bool | None = Query(None)) -> _PendingActionsFilter:
+ return cls().set_value(has_pending_actions)
+
+
+QueryPendingActionsFilter = Annotated[_PendingActionsFilter, Depends(_PendingActionsFilter.depends)]
+
+
+# Variables
+QueryVariableKeyPatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(Variable.key, "variable_key_pattern"))
+]
+
+QueryVariableKeyPrefixPatternSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(prefix_search_param_factory(Variable.key, "variable_key_prefix_pattern")),
+]
+
+
+# Pools
+QueryPoolNamePatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(Pool.pool, "pool_name_pattern"))
+]
+
+QueryPoolNamePrefixPatternSearch = Annotated[
+ _PrefixSearchParam, Depends(prefix_search_param_factory(Pool.pool, "pool_name_prefix_pattern"))
+]
+
+
+# UI Shared
+def _optional_boolean(value: bool | None) -> bool | None:
+ return value if value is not None else False
+
+
+QueryIncludeUpstream = Annotated[bool, AfterValidator(_optional_boolean)]
+
+QueryIncludeDownstream = Annotated[bool, AfterValidator(_optional_boolean)]
+
+
+state_priority: list[None | TaskInstanceState] = [
+ TaskInstanceState.FAILED,
+ TaskInstanceState.UPSTREAM_FAILED,
+ TaskInstanceState.UP_FOR_RETRY,
+ TaskInstanceState.UP_FOR_RESCHEDULE,
+ TaskInstanceState.RUNNING,
+ TaskInstanceState.RESTARTING,
+ TaskInstanceState.DEFERRED,
+ TaskInstanceState.AWAITING_INPUT,
+ TaskInstanceState.QUEUED,
+ TaskInstanceState.SCHEDULED,
+ None,
+ TaskInstanceState.SUCCESS,
+ TaskInstanceState.SKIPPED,
+ TaskInstanceState.REMOVED,
+]
+
+
+# Connections
+QueryConnectionIdPatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(Connection.conn_id, "connection_id_pattern"))
+]
+
+QueryConnectionIdPrefixPatternSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(prefix_search_param_factory(Connection.conn_id, "connection_id_prefix_pattern")),
+]
+
+
+# Human in the loop
+QueryHITLDetailDagIdPatternSearch = Annotated[
+ _SearchParam,
+ Depends(
+ search_param_factory(
+ TaskInstance.dag_id,
+ "dag_id_pattern",
+ )
+ ),
+]
+
+QueryHITLDetailDagIdPrefixPatternSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(
+ prefix_search_param_factory(
+ TaskInstance.dag_id,
+ "dag_id_prefix_pattern",
+ )
+ ),
+]
+
+QueryHITLDetailTaskIdPatternSearch = Annotated[
+ _SearchParam,
+ Depends(
+ search_param_factory(
+ TaskInstance.task_id,
+ "task_id_pattern",
+ )
+ ),
+]
+
+QueryHITLDetailTaskIdPrefixPatternSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(
+ prefix_search_param_factory(
+ TaskInstance.task_id,
+ "task_id_prefix_pattern",
+ )
+ ),
+]
+
+QueryHITLDetailTaskIdFilter = Annotated[
+ FilterParam[str | None],
+ Depends(
+ filter_param_factory(
+ TaskInstance.task_id,
+ str | None,
+ filter_name="task_id",
+ )
+ ),
+]
+
+QueryHITLDetailMapIndexFilter = Annotated[
+ FilterParam[int | None],
+ Depends(
+ filter_param_factory(
+ TaskInstance.map_index,
+ int | None,
+ filter_name="map_index",
+ )
+ ),
+]
+
+QueryHITLDetailSubjectSearch = Annotated[
+ _SearchParam,
+ Depends(
+ search_param_factory(
+ HITLDetail.subject,
+ "subject_search",
+ )
+ ),
+]
+
+QueryHITLDetailBodySearch = Annotated[
+ _SearchParam,
+ Depends(
+ search_param_factory(
+ HITLDetail.body,
+ "body_search",
+ )
+ ),
+]
+
+QueryHITLDetailResponseReceivedFilter = Annotated[
+ FilterParam[bool | None],
+ Depends(
+ filter_param_factory(
+ HITLDetail.response_received,
+ bool | None,
+ filter_name="response_received",
+ )
+ ),
+]
+
+QueryHITLDetailRespondedUserIdFilter = Annotated[
+ FilterParam[list[str]],
+ Depends(
+ filter_param_factory(
+ HITLDetail.responded_by_user_id,
+ list[str],
+ FilterOptionEnum.ANY_EQUAL,
+ default_factory=list,
+ filter_name="responded_by_user_id",
+ )
+ ),
+]
+
+QueryHITLDetailRespondedUserNameFilter = Annotated[
+ FilterParam[list[str]],
+ Depends(
+ filter_param_factory(
+ HITLDetail.responded_by_user_name,
+ list[str],
+ FilterOptionEnum.ANY_EQUAL,
+ default_factory=list,
+ filter_name="responded_by_user_name",
+ )
+ ),
+]
+
+
+# Parse Import Errors
+QueryParseImportErrorFilenamePatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(ParseImportError.filename, "filename_pattern"))
+]
+
+QueryParseImportErrorFilenamePrefixPatternSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(prefix_search_param_factory(ParseImportError.filename, "filename_prefix_pattern")),
+]
+
+QueryParseImportErrorFilenameFilter = Annotated[
+ FilterParam,
+ Depends(
+ filter_param_factory(
+ ParseImportError.filename,
+ str | None,
+ filter_name="filename",
+ description="Exact filename match. Returns only the import error for this specific file path.",
+ )
+ ),
+]
+
+QueryParseImportErrorBundleNameFilter = Annotated[
+ FilterParam,
+ Depends(
+ filter_param_factory(
+ ParseImportError.bundle_name,
+ str | None,
+ filter_name="bundle_name",
+ description="Exact bundle name match. Returns only import errors from this specific bundle.",
+ )
+ ),
+]
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/range.py b/airflow-core/src/airflow/api_fastapi/common/parameters/range.py
new file mode 100644
index 00000000000..aa3d03f86db
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/range.py
@@ -0,0 +1,216 @@
+# 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 Callable
+from datetime import datetime
+from typing import (
+ TYPE_CHECKING,
+ Annotated,
+ Any,
+ Generic,
+ overload,
+)
+
+from fastapi import HTTPException, Query
+from pendulum.parsing.exceptions import ParserError
+from pydantic import AfterValidator, BaseModel
+from sqlalchemy import and_, func, or_
+
+from airflow._shared.timezones import timezone
+from airflow.api_fastapi.common.parameters.base import BaseParam, T
+from airflow.models import Base
+from airflow.typing_compat import Self
+
+if TYPE_CHECKING:
+ from sqlalchemy.orm.attributes import InstrumentedAttribute
+ from sqlalchemy.sql import Select
+
+
+def _safe_parse_datetime(date_to_check: str) -> datetime:
+ """
+ Parse datetime and raise error for invalid dates.
+
+ :param date_to_check: the string value to be parsed
+ """
+ if not date_to_check:
+ raise ValueError(f"{date_to_check} cannot be None.")
+ return _safe_parse_datetime_optional(date_to_check)
+
+
+@overload
+def _safe_parse_datetime_optional(date_to_check: str) -> datetime: ...
+
+
+@overload
+def _safe_parse_datetime_optional(date_to_check: None) -> None: ...
+
+
+def _safe_parse_datetime_optional(date_to_check: str | None) -> datetime | None:
+ """
+ Parse datetime and raise error for invalid dates.
+
+ Allow None values.
+
+ :param date_to_check: the string value to be parsed
+ """
+ if date_to_check is None:
+ return None
+ try:
+ return timezone.parse(date_to_check, strict=True)
+ except (TypeError, ParserError):
+ raise HTTPException(
+ 400, f"Invalid datetime: {date_to_check!r}. Please check the date parameter have this value."
+ )
+
+
+class Range(BaseModel, Generic[T]):
+ """Range with a lower and upper bound."""
+
+ lower_bound_gte: T | None
+ lower_bound_gt: T | None
+ upper_bound_lte: T | None
+ upper_bound_lt: T | None
+
+
+class RangeFilter(BaseParam[Range]):
+ """Filter on range in between the lower and upper bound."""
+
+ def __init__(self, value: Range | None, attribute: InstrumentedAttribute) -> None:
+ super().__init__(value)
+ self.attribute: InstrumentedAttribute = attribute
+
+ def to_orm(self, select: Select) -> Select:
+ if self.skip_none is False:
+ raise ValueError(f"Cannot set 'skip_none' to False on a {type(self)}")
+
+ if self.value is None:
+ return select
+
+ if self.value.lower_bound_gte:
+ select = select.where(self.attribute >= self.value.lower_bound_gte)
+ if self.value.lower_bound_gt:
+ select = select.where(self.attribute > self.value.lower_bound_gt)
+ if self.value.upper_bound_lte:
+ select = select.where(self.attribute <= self.value.upper_bound_lte)
+ if self.value.upper_bound_lt:
+ select = select.where(self.attribute < self.value.upper_bound_lt)
+
+ return select
+
+ @classmethod
+ def depends(cls, *args: Any, **kwargs: Any) -> Self:
+ raise NotImplementedError("Use the `range_filter_factory` function to create the dependency")
+
+ def is_active(self) -> bool:
+ """Check if the range filter has any active bounds."""
+ return self.value is not None and (
+ self.value.lower_bound_gte is not None
+ or self.value.lower_bound_gt is not None
+ or self.value.upper_bound_lte is not None
+ or self.value.upper_bound_lt is not None
+ )
+
+
+class NullableDatetimeRangeFilter(RangeFilter):
+ """
+ RangeFilter for nullable datetime columns (``start_date``, ``end_date``), rewritten for index use.
+
+ ``COALESCE(column, now())`` wraps the column in a function call that prevents PostgreSQL from
+ using btree indexes, forcing sequential scans on large tables. This class emits equivalent
+ ``OR`` predicates so each branch can be satisfied by an independent index scan.
+
+ NULL semantics: ``start_date=NULL`` means the task has not started yet; ``end_date=NULL`` means
+ the task is still running. For lower bounds the NULL branch passes unconditionally — a not-yet-
+ started/ended task will eventually satisfy any past lower bound. For upper bounds the NULL branch
+ is ``col IS NULL AND now() <= x``, preserving the COALESCE(col, now()) semantics without the
+ function-wrap index penalty.
+ """
+
+ def to_orm(self, select: Select) -> Select:
+ if self.skip_none is False:
+ raise ValueError(f"Cannot set 'skip_none' to False on a {type(self)}")
+
+ if self.value is None:
+ return select
+
+ if self.value.lower_bound_gte:
+ x = self.value.lower_bound_gte
+ select = select.where(or_(self.attribute >= x, self.attribute.is_(None)))
+ if self.value.lower_bound_gt:
+ x = self.value.lower_bound_gt
+ select = select.where(or_(self.attribute > x, self.attribute.is_(None)))
+ if self.value.upper_bound_lte:
+ x = self.value.upper_bound_lte
+ select = select.where(or_(self.attribute <= x, and_(self.attribute.is_(None), func.now() <= x)))
+ if self.value.upper_bound_lt:
+ x = self.value.upper_bound_lt
+ select = select.where(or_(self.attribute < x, and_(self.attribute.is_(None), func.now() < x)))
+
+ return select
+
+
+def datetime_range_filter_factory(
+ filter_name: str, model: Base, attribute_name: str | None = None
+) -> Callable[[datetime | None, datetime | None, datetime | None, datetime | None], RangeFilter]:
+ def depends_datetime(
+ lower_bound_gte: datetime | None = Query(alias=f"{filter_name}_gte", default=None),
+ lower_bound_gt: datetime | None = Query(alias=f"{filter_name}_gt", default=None),
+ upper_bound_lte: datetime | None = Query(alias=f"{filter_name}_lte", default=None),
+ upper_bound_lt: datetime | None = Query(alias=f"{filter_name}_lt", default=None),
+ ) -> RangeFilter:
+ attr = getattr(model, attribute_name or filter_name)
+ range_val = Range(
+ lower_bound_gte=lower_bound_gte,
+ lower_bound_gt=lower_bound_gt,
+ upper_bound_lte=upper_bound_lte,
+ upper_bound_lt=upper_bound_lt,
+ )
+ if filter_name in ("start_date", "end_date"):
+ return NullableDatetimeRangeFilter(range_val, attr)
+ return RangeFilter(range_val, attr)
+
+ return depends_datetime
+
+
+def float_range_filter_factory(
+ filter_name: str, model: Base
+) -> Callable[[float | None, float | None, float | None, float | None], RangeFilter]:
+ def depends_float(
+ lower_bound_gte: float | None = Query(alias=f"{filter_name}_gte", default=None),
+ lower_bound_gt: float | None = Query(alias=f"{filter_name}_gt", default=None),
+ upper_bound_lte: float | None = Query(alias=f"{filter_name}_lte", default=None),
+ upper_bound_lt: float | None = Query(alias=f"{filter_name}_lt", default=None),
+ ) -> RangeFilter:
+ return RangeFilter(
+ Range(
+ lower_bound_gte=lower_bound_gte,
+ lower_bound_gt=lower_bound_gt,
+ upper_bound_lte=upper_bound_lte,
+ upper_bound_lt=upper_bound_lt,
+ ),
+ getattr(model, filter_name),
+ )
+
+ return depends_float
+
+
+# Common Safe DateTime
+DateTimeQuery = Annotated[str, AfterValidator(_safe_parse_datetime)]
+
+OptionalDateTimeQuery = Annotated[str | None, AfterValidator(_safe_parse_datetime_optional)]
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/search.py b/airflow-core/src/airflow/api_fastapi/common/parameters/search.py
new file mode 100644
index 00000000000..8c136ab1ba0
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/search.py
@@ -0,0 +1,306 @@
+# 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 abc import ABC, abstractmethod
+from collections.abc import Callable
+from typing import (
+ TYPE_CHECKING,
+ Any,
+)
+
+from fastapi import Query
+from sqlalchemy import and_, or_, true as sql_true
+
+from airflow.api_fastapi.common.parameters.base import BaseParam
+from airflow.models.taskinstance import TaskInstance
+from airflow.typing_compat import Self
+
+if TYPE_CHECKING:
+ from sqlalchemy.sql import ColumnElement, Select
+
+
+class _PrefixPatternParam(BaseParam[str], ABC):
+ """
+ Shared prefix pattern: pipe ``|`` for OR, ``~`` → empty (match all), Unicode prefix range.
+
+ .. note::
+ Trailing non-alphanumeric characters in a search term are stripped before the range
+ is computed. A range scan with a punctuation-terminated upper bound is unsafe under
+ PostgreSQL's default locale-aware collation (``en_US.utf8`` sorts punctuation in a
+ way that breaks the range), and additionally stopping the range at an alphanumeric
+ character keeps the upper bound alphanumeric too, so the predicate stays usable by
+ default btree indexes. A user who asks for prefix ``"test_"`` gets matches starting
+ with ``"test"`` — a small over-match trade-off made explicit in the public
+ ``*_prefix_pattern`` query-param description.
+ """
+
+ pipe_as_or: bool = True
+
+ @staticmethod
+ def _prefix_range_upper(term: str) -> str | None:
+ """
+ Compute the exclusive upper bound for a prefix range scan.
+
+ Returns ``None`` if the term has no alphanumeric characters. Trailing non-alphanumeric
+ characters are dropped before bumping the last character so the resulting upper bound
+ is itself alphanumeric and behaves predictably under locale-aware collations. If
+ incrementing would land outside the alphanumeric range (e.g. ``'z' → '{'``), we drop
+ that character and retry.
+ """
+ while term and not term[-1].isalnum():
+ term = term[:-1]
+ if not term:
+ return None
+ last = ord(term[-1])
+ if last >= 0x10FFFF:
+ return _PrefixPatternParam._prefix_range_upper(term[:-1])
+ bumped = chr(last + 1)
+ if not bumped.isalnum():
+ return _PrefixPatternParam._prefix_range_upper(term[:-1])
+ return term[:-1] + bumped
+
+ @staticmethod
+ def _prefix_lower_bound(term: str) -> str:
+ """Return the matching lower bound: strip trailing non-alphanumeric chars to pair with the upper."""
+ while term and not term[-1].isalnum():
+ term = term[:-1]
+ return term
+
+ @abstractmethod
+ def _prefix_clause(self, term: str):
+ """Return the SQL boolean for one prefix term (including empty string after ``~`` alias)."""
+
+ def to_orm(self, select: Select) -> Select:
+ # ``skip_none`` only gates the "no value" behavior for the callers that must keep
+ # the filter slot present (e.g. ``QueryDagIdPrefixPatternSearchWithNone``); applying
+ # a ``None`` value as a filter produces nonsense predicates, so always skip it here.
+ if self.value is None:
+ return select
+
+ val_str = str(self.value)
+ if self.pipe_as_or and "|" in val_str:
+ search_terms = [term.strip() for term in val_str.split("|") if term.strip()]
+ if search_terms:
+ return select.where(or_(*(self._prefix_clause(term) for term in search_terms)))
+
+ return select.where(self._prefix_clause(val_str))
+
+ def transform_aliases(self, value: str | None) -> str | None:
+ if value == "~":
+ value = ""
+ return value
+
+
+def _build_pipe_clause(pipe_as_or: bool) -> str:
+ """Build the per-parameter pipe note. OR is the documented default (see the API description), so only the literal exception is spelled out."""
+ return "" if pipe_as_or else "Here `|` is matched literally, not as OR. "
+
+
+_LIKE_ESCAPE_CHAR = "\\"
+
+
+def _escape_like_pattern(value: str) -> str:
+ r"""
+ Escape SQL ``LIKE`` / ``ILIKE`` metacharacters in a user-supplied value.
+
+ Use together with ``column.ilike(f"%{_escape_like_pattern(value)}%", escape="\\")`` on filter
+ parameters that intend literal substring matching (so a user-supplied ``%`` or ``_`` does not
+ widen the match beyond what the filter semantics promise). Search parameters that explicitly
+ expose wildcard semantics (see :class:`_SearchParam`) must not call this — they want the
+ metacharacters to pass through.
+ """
+ return (
+ value.replace(_LIKE_ESCAPE_CHAR, _LIKE_ESCAPE_CHAR * 2)
+ .replace("%", _LIKE_ESCAPE_CHAR + "%")
+ .replace("_", _LIKE_ESCAPE_CHAR + "_")
+ )
+
+
+class _SearchParam(BaseParam[str]):
+ """
+ Substring search on a column using ``ILIKE '%term%'`` (case-insensitive).
+
+ .. note::
+ This full-match substring search most of the time prevents the database
+ from using B-tree indexes on ``attribute``, which can be very slow on
+ large tables. Prefer :class:`_PrefixSearchParam` (the ``*_prefix_pattern``
+ query-param counterpart) when matching from the beginning of the value
+ is acceptable.
+ """
+
+ def __init__(self, attribute: ColumnElement, skip_none: bool = True, pipe_as_or: bool = True) -> None:
+ super().__init__(skip_none=skip_none)
+ self.attribute: ColumnElement = attribute
+ self.pipe_as_or = pipe_as_or
+
+ def to_orm(self, select: Select) -> Select:
+ if self.value is None and self.skip_none:
+ return select
+
+ val_str = str(self.value)
+ if self.pipe_as_or and "|" in val_str:
+ search_terms = [term.strip() for term in val_str.split("|") if term.strip()]
+ if search_terms:
+ return select.where(or_(*(self.attribute.ilike(f"%{term}%") for term in search_terms)))
+
+ return select.where(self.attribute.ilike(f"%{val_str}%"))
+
+ def transform_aliases(self, value: str | None) -> str | None:
+ if value == "~":
+ value = "%"
+ return value
+
+ @classmethod
+ def depends(cls, *args: Any, **kwargs: Any) -> Self:
+ raise NotImplementedError("Use search_param_factory instead , depends is not implemented.")
+
+
+class _PrefixSearchParam(_PrefixPatternParam):
+ """
+ Prefix search on a column using range comparison (case-sensitive, index-friendly).
+
+ Unlike :class:`_SearchParam`, wildcard characters are treated as literals and the query
+ plan can use the column's default B-tree index for the range scan. Trailing
+ non-alphanumeric characters in ``term`` are stripped first (see
+ :class:`_PrefixPatternParam` for why).
+ """
+
+ def __init__(self, attribute: ColumnElement, skip_none: bool = True, pipe_as_or: bool = True) -> None:
+ super().__init__(skip_none=skip_none)
+ self.attribute: ColumnElement = attribute
+ self.pipe_as_or = pipe_as_or
+
+ def _prefix_clause(self, term: str):
+ lower = self._prefix_lower_bound(term)
+ if not lower:
+ return self.attribute.is_not(None)
+ upper = self._prefix_range_upper(term)
+ if upper is None:
+ return self.attribute >= lower
+ return and_(self.attribute >= lower, self.attribute < upper)
+
+ @classmethod
+ def depends(cls, *args: Any, **kwargs: Any) -> Self:
+ raise NotImplementedError("Use prefix_search_param_factory instead, depends is not implemented.")
+
+
+class _TaskDisplayNamePrefixPatternParam(_PrefixPatternParam):
+ """
+ Prefix filter equivalent to :attr:`TaskInstance.task_display_name`, rewritten for composite-index use.
+
+ The hybrid expression ``coalesce(_task_display_property_value, task_id)`` cannot use those indexes;
+ this implementation applies an equivalent ``OR`` of simpler range predicates instead. Trailing
+ non-alphanumeric characters in ``term`` are stripped first (see :class:`_PrefixPatternParam`).
+ """
+
+ def _prefix_clause(self, term: str):
+ lower = self._prefix_lower_bound(term)
+ if not lower:
+ return sql_true()
+ upper = self._prefix_range_upper(term)
+ if upper is None:
+ return or_(
+ and_(
+ TaskInstance._task_display_property_value.is_(None),
+ TaskInstance.task_id >= lower,
+ ),
+ and_(
+ TaskInstance._task_display_property_value.is_not(None),
+ TaskInstance._task_display_property_value >= lower,
+ ),
+ )
+ return or_(
+ and_(
+ TaskInstance._task_display_property_value.is_(None),
+ TaskInstance.task_id >= lower,
+ TaskInstance.task_id < upper,
+ ),
+ and_(
+ TaskInstance._task_display_property_value.is_not(None),
+ TaskInstance._task_display_property_value >= lower,
+ TaskInstance._task_display_property_value < upper,
+ ),
+ )
+
+ @classmethod
+ def depends(
+ cls,
+ task_display_name_prefix_pattern: str | None = Query(
+ default=None,
+ description=(
+ "Case-sensitive prefix match on task display name (`_task_display_property_value` else "
+ "`task_id`). Index-friendly alternative to `task_display_name_pattern`; on large databases "
+ "combine with `dag_id_prefix_pattern` (or a specific Dag in the path) so composite indexes "
+ 'apply. See "Filtering with pattern parameters".'
+ ),
+ ),
+ ) -> Self:
+ param = cls()
+ return param.set_value(param.transform_aliases(task_display_name_prefix_pattern))
+
+
+def search_param_factory(
+ attribute: ColumnElement,
+ pattern_name: str,
+ skip_none: bool = True,
+ pipe_as_or: bool = True,
+) -> Callable[[str | None], _SearchParam]:
+ prefix_pattern_name = pattern_name.replace("_pattern", "_prefix_pattern")
+ DESCRIPTION = (
+ "Case-insensitive substring match (SQL `ILIKE`). "
+ f"{_build_pipe_clause(pipe_as_or)}"
+ f'Slower than `{prefix_pattern_name}` on large tables — see "Filtering with pattern parameters".'
+ )
+
+ def depends_search(
+ value: str | None = Query(alias=pattern_name, default=None, description=DESCRIPTION),
+ ) -> _SearchParam:
+ search_parm = _SearchParam(attribute, skip_none, pipe_as_or=pipe_as_or)
+ value = search_parm.transform_aliases(value)
+ return search_parm.set_value(value)
+
+ return depends_search
+
+
+def prefix_search_param_factory(
+ attribute: ColumnElement,
+ prefix_pattern_name: str,
+ skip_none: bool = True,
+ pipe_as_or: bool = True,
+) -> Callable[[str | None], _PrefixSearchParam]:
+ """
+ Build a FastAPI ``Depends`` returning a :class:`_PrefixSearchParam` for prefix matching.
+
+ Prefer this over :func:`search_param_factory` for performance: prefix matching uses a
+ B-tree index range scan, while substring matching requires a full table scan.
+ """
+ DESCRIPTION = (
+ "Case-sensitive, index-friendly prefix match. "
+ f"{_build_pipe_clause(pipe_as_or)}"
+ 'See "Filtering with pattern parameters".'
+ )
+
+ def depends_prefix_search(
+ value: str | None = Query(alias=prefix_pattern_name, default=None, description=DESCRIPTION),
+ ) -> _PrefixSearchParam:
+ search_parm = _PrefixSearchParam(attribute, skip_none, pipe_as_or=pipe_as_or)
+ value = search_parm.transform_aliases(value)
+ return search_parm.set_value(value)
+
+ return depends_prefix_search
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/sort.py b/airflow-core/src/airflow/api_fastapi/common/parameters/sort.py
new file mode 100644
index 00000000000..2e01e695422
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/sort.py
@@ -0,0 +1,205 @@
+# 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 Callable, Sequence
+from typing import (
+ TYPE_CHECKING,
+ Any,
+)
+
+from fastapi import HTTPException, Query
+from sqlalchemy import Column
+from sqlalchemy.inspection import inspect
+
+from airflow.api_fastapi.common.parameters.base import BaseParam
+from airflow.models import Base
+from airflow.typing_compat import Self
+
+if TYPE_CHECKING:
+ from sqlalchemy.sql import ColumnElement, Select
+
+
+class SortParam(BaseParam[list[str]]):
+ """Order result by the attribute."""
+
+ MAX_SORT_PARAMS = 10
+
+ def __init__(
+ self,
+ allowed_attrs: list[str],
+ model: Base,
+ to_replace: dict[str, str | Column | list[Column]] | None = None,
+ ) -> None:
+ super().__init__()
+ self.allowed_attrs = allowed_attrs
+ self.model = model
+ self.to_replace = to_replace
+ self._cached_resolution: list[tuple[str, ColumnElement, bool]] | None = None
+
+ def set_value(self, value: list[str] | None) -> Self:
+ self._cached_resolution = None
+ return super().set_value(value)
+
+ def _resolve(self) -> list[tuple[str, ColumnElement, bool]]:
+ """Resolve sort columns as (attr_name, column, is_descending) tuples. Cached after first call."""
+ if self._cached_resolution is not None:
+ return self._cached_resolution
+
+ if self.value is None:
+ self.value = [self.get_primary_key_string()]
+
+ order_by_values = self.value
+ if len(order_by_values) > self.MAX_SORT_PARAMS:
+ raise HTTPException(
+ 400,
+ f"Ordering with more than {self.MAX_SORT_PARAMS} parameters is not allowed. Provided: {order_by_values}",
+ )
+
+ resolved: list[tuple[str, ColumnElement, bool]] = []
+ for order_by_value in order_by_values:
+ lstriped_orderby = order_by_value.lstrip("-")
+ # Store the user-facing name in the resolved tuple. ``row_value`` resolves
+ # it back to the actual row accessor via ``to_replace`` when reading values
+ # for cursor encoding.
+ attr_name = lstriped_orderby
+ column: Column | None = None
+ if self.to_replace:
+ replacement = self.to_replace.get(lstriped_orderby, lstriped_orderby)
+ if isinstance(replacement, str):
+ lstriped_orderby = replacement
+ elif isinstance(replacement, list):
+ # Compound sort: expand the list into multiple sort entries.
+ # Each column's ORM key becomes its attr_name so that
+ # row_value() can read the corresponding attribute via
+ # getattr(row, attr_name) without further to_replace lookups.
+ is_desc = order_by_value.startswith("-")
+ for col in replacement:
+ col_attr_name = col.key
+ resolved.append((col_attr_name, col, is_desc))
+ continue
+ else:
+ column = replacement
+
+ if (self.allowed_attrs and lstriped_orderby not in self.allowed_attrs) and column is None:
+ raise HTTPException(
+ 400,
+ f"Ordering with '{lstriped_orderby}' is disallowed or "
+ f"the attribute does not exist on the model",
+ )
+ if column is None:
+ column = getattr(self.model, lstriped_orderby)
+
+ resolved.append((attr_name, column, order_by_value.startswith("-")))
+
+ primary_key_column = self.get_primary_key_column()
+ pk_name = self.get_primary_key_string()
+ resolved_column_keys = {getattr(col, "key", None) for _, col, _ in resolved}
+ if pk_name not in resolved_column_keys:
+ pk_desc = bool(order_by_values and order_by_values[0].startswith("-"))
+ resolved.append((pk_name, primary_key_column, pk_desc))
+
+ self._cached_resolution = resolved
+ return self._cached_resolution
+
+ def to_orm(self, select: Select, *, reversed: bool = False) -> Select:
+ if self.skip_none is False:
+ raise ValueError(f"Cannot set 'skip_none' to False on a {type(self)}")
+
+ resolved = self._resolve()
+ if reversed:
+ columns = [col.asc() if is_desc else col.desc() for _, col, is_desc in resolved]
+ else:
+ columns = [col.desc() if is_desc else col.asc() for _, col, is_desc in resolved]
+ return select.order_by(None).order_by(*columns)
+
+ def get_resolved_columns(self) -> list[tuple[str, ColumnElement, bool]]:
+ """Return resolved sort columns as (attr_name, column_element, is_descending) tuples."""
+ return self._resolve()
+
+ def row_value(self, row: Any, name: str) -> Any:
+ """
+ Extract the sort-key value for ``name`` from a result row.
+
+ Resolves the accessor through ``to_replace`` for string aliases
+ (e.g. ``{"dag_run_id": "run_id"}``). For column-form mappings
+ (e.g. ``{"run_after": DagRun.run_after}``), resolves through the
+ primary model's attribute so association proxies can still be used
+ for cursor values. Raises ``NotImplementedError`` when the model
+ exposes no such attribute rather than emitting a ``None`` cursor token.
+ """
+ if self.to_replace:
+ replacement = self.to_replace.get(name)
+ if isinstance(replacement, str):
+ return getattr(row, replacement, None)
+ if replacement is not None and not isinstance(replacement, list):
+ # Column-form mapping resolves through the primary model's attribute,
+ # often an association proxy onto the joined entity
+ # (``TaskInstance.run_after`` -> ``dag_run.run_after``). Fail loudly if the
+ # model exposes no such attribute, rather than emitting a ``None`` cursor token.
+ try:
+ return getattr(row, name)
+ except AttributeError:
+ raise NotImplementedError(
+ f"Cursor pagination cannot resolve column-form ``to_replace`` for "
+ f"``{name}``: the primary model exposes no such attribute. Add an "
+ f"association proxy, use a string alias, or sort by a primary-model column."
+ )
+ # List-form replacements are expanded in _resolve() into individual entries
+ # each using the column's own ORM key as attr_name, so ``name`` at this point
+ # is already a concrete model attribute (e.g. ``_rendered_map_index`` or
+ # ``map_index``) — fall through to the getattr below.
+ return getattr(row, name, None)
+
+ def get_primary_key_column(self) -> Column:
+ """Get the primary key column of the model of SortParam object."""
+ return inspect(self.model).primary_key[0]
+
+ def get_primary_key_string(self) -> str:
+ """Get the primary key string of the model of SortParam object."""
+ return self.get_primary_key_column().name
+
+ @classmethod
+ def depends(cls, *args: Any, **kwargs: Any) -> Self:
+ raise NotImplementedError("Use dynamic_depends, depends not implemented.")
+
+ def dynamic_depends(self, default: str | Sequence[str] | None = None) -> Callable:
+ # Include to_replace keys that are not already in allowed_attrs to avoid
+ # duplicate entries in the spec description.
+ allowed_set = set(self.allowed_attrs)
+ to_replace_attrs = [k for k in self.to_replace if k not in allowed_set] if self.to_replace else []
+
+ all_attrs = self.allowed_attrs + to_replace_attrs
+
+ if default is None:
+ default_list = [self.get_primary_key_string()]
+ elif isinstance(default, str):
+ default_list = [default]
+ else:
+ default_list = list(default)
+
+ _order_by_query = Query(
+ default=default_list,
+ description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
+ f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
+ )
+
+ def inner(order_by: list[str] = _order_by_query) -> SortParam:
+ return SortParam(self.allowed_attrs, self.model, self.to_replace).set_value(order_by)
+
+ return inner
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/task_instance.py b/airflow-core/src/airflow/api_fastapi/common/parameters/task_instance.py
new file mode 100644
index 00000000000..e34dc931a02
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/task_instance.py
@@ -0,0 +1,276 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import (
+ TYPE_CHECKING,
+ Annotated,
+ Any,
+ cast,
+)
+
+from fastapi import Depends, HTTPException, Query, status
+
+from airflow.api_fastapi.common.parameters.base import BaseParam, _MySQLCollate
+from airflow.api_fastapi.common.parameters.filter import FilterOptionEnum, FilterParam, filter_param_factory
+from airflow.api_fastapi.common.parameters.search import (
+ _PrefixSearchParam,
+ _SearchParam,
+ _TaskDisplayNamePrefixPatternParam,
+ prefix_search_param_factory,
+ search_param_factory,
+)
+from airflow.api_fastapi.compat import HTTP_422_UNPROCESSABLE_CONTENT
+from airflow.models.dag_version import DagVersion
+from airflow.models.taskinstance import TaskInstance
+from airflow.utils.state import TaskInstanceState
+
+if TYPE_CHECKING:
+ from sqlalchemy.sql import ColumnElement, Select
+
+ from airflow.serialization.definitions.dag import SerializedDAG
+
+
+class QueryTaskInstanceTaskGroupFilter(BaseParam[str]):
+ """Task group filter - returns all tasks in the specified group."""
+
+ def __init__(self, dag=None, skip_none: bool = True):
+ super().__init__(skip_none=skip_none)
+ self._dag: None | SerializedDAG = dag
+
+ @property
+ def dag(self) -> None | SerializedDAG:
+ return self._dag
+
+ @dag.setter
+ def dag(self, value: None | SerializedDAG) -> None:
+ self._dag = value
+
+ def to_orm(self, select: Select) -> Select:
+ if self.value is None and self.skip_none:
+ return select
+
+ if not self.dag:
+ raise ValueError("Dag must be set before calling to_orm")
+
+ if not hasattr(self.dag, "task_group"):
+ return select
+
+ # Exact matching on group_id
+ task_groups = self.dag.task_group.get_task_group_dict()
+ task_group = task_groups.get(self.value)
+ if not task_group:
+ raise HTTPException(
+ status.HTTP_404_NOT_FOUND,
+ detail={
+ "reason": "not_found",
+ "message": f"Task group {self.value} not found",
+ },
+ )
+
+ return select.where(TaskInstance.task_id.in_(task.task_id for task in task_group.iter_tasks()))
+
+ @classmethod
+ def depends(
+ cls,
+ value: str | None = Query(
+ alias="task_group_id",
+ default=None,
+ description="Filter by exact task group ID. Returns all tasks within the specified task group.",
+ ),
+ ) -> QueryTaskInstanceTaskGroupFilter:
+ return cls(dag=None).set_value(value)
+
+
+# TI
+def _transform_ti_states(states: list[str] | None) -> list[TaskInstanceState | None] | None:
+ """Transform a list of state strings into a list of TaskInstanceState enums handling special 'None' cases."""
+ if not states:
+ return None
+
+ try:
+ return [None if s in ("no_status", "none", None) else TaskInstanceState(s) for s in states]
+ except ValueError:
+ raise HTTPException(
+ status_code=HTTP_422_UNPROCESSABLE_CONTENT,
+ detail=f"Invalid value for state. Valid values are {', '.join(TaskInstanceState)}",
+ )
+
+
+QueryTIStateFilter = Annotated[
+ FilterParam[list[str]],
+ Depends(
+ filter_param_factory(
+ TaskInstance.state,
+ list[str],
+ FilterOptionEnum.ANY_EQUAL,
+ default_factory=list,
+ transform_callable=_transform_ti_states,
+ )
+ ),
+]
+
+QueryTIPoolFilter = Annotated[
+ FilterParam[list[str]],
+ Depends(
+ filter_param_factory(TaskInstance.pool, list[str], FilterOptionEnum.ANY_EQUAL, default_factory=list)
+ ),
+]
+
+QueryTIQueueFilter = Annotated[
+ FilterParam[list[str]],
+ Depends(
+ filter_param_factory(TaskInstance.queue, list[str], FilterOptionEnum.ANY_EQUAL, default_factory=list)
+ ),
+]
+
+QueryTIPoolNamePatternSearch = Annotated[
+ _SearchParam,
+ Depends(search_param_factory(TaskInstance.pool, "pool_name_pattern")),
+]
+
+QueryTIPoolNamePrefixPatternSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(prefix_search_param_factory(TaskInstance.pool, "pool_name_prefix_pattern")),
+]
+
+
+QueryTIQueueNamePatternSearch = Annotated[
+ _SearchParam,
+ Depends(search_param_factory(TaskInstance.queue, "queue_name_pattern")),
+]
+
+QueryTIQueueNamePrefixPatternSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(prefix_search_param_factory(TaskInstance.queue, "queue_name_prefix_pattern")),
+]
+
+QueryTIExecutorFilter = Annotated[
+ FilterParam[list[str]],
+ Depends(
+ filter_param_factory(
+ TaskInstance.executor, list[str], FilterOptionEnum.ANY_EQUAL, default_factory=list
+ )
+ ),
+]
+
+QueryTITaskDisplayNamePatternSearch = Annotated[
+ _SearchParam,
+ Depends(search_param_factory(TaskInstance.task_display_name, "task_display_name_pattern")),
+]
+
+QueryTITaskDisplayNamePrefixPatternSearch = Annotated[
+ _TaskDisplayNamePrefixPatternParam, Depends(_TaskDisplayNamePrefixPatternParam.depends)
+]
+
+QueryTITaskGroupFilter = Annotated[
+ QueryTaskInstanceTaskGroupFilter, Depends(QueryTaskInstanceTaskGroupFilter.depends)
+]
+
+QueryTIDagVersionFilter = Annotated[
+ FilterParam[list[int]],
+ Depends(
+ filter_param_factory(
+ DagVersion.version_number,
+ list[int],
+ FilterOptionEnum.ANY_EQUAL,
+ default_factory=list,
+ )
+ ),
+]
+
+QueryTITryNumberFilter = Annotated[
+ FilterParam[list[int]],
+ Depends(
+ filter_param_factory(
+ TaskInstance.try_number, list[int], FilterOptionEnum.ANY_EQUAL, default_factory=list
+ )
+ ),
+]
+
+
+QueryTIOperatorFilter = Annotated[
+ FilterParam[list[str]],
+ Depends(
+ filter_param_factory(
+ TaskInstance.operator, list[str], FilterOptionEnum.ANY_EQUAL, default_factory=list
+ )
+ ),
+]
+
+QueryTIOperatorNamePatternSearch = Annotated[
+ _SearchParam,
+ Depends(
+ search_param_factory(
+ TaskInstance.custom_operator_name,
+ "operator_name_pattern",
+ )
+ ),
+]
+
+QueryTIOperatorNamePrefixPatternSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(
+ prefix_search_param_factory(
+ TaskInstance.custom_operator_name,
+ "operator_name_prefix_pattern",
+ )
+ ),
+]
+
+
+QueryTIMapIndexFilter = Annotated[
+ FilterParam[list[int]],
+ Depends(
+ filter_param_factory(
+ TaskInstance.map_index, list[int], FilterOptionEnum.ANY_EQUAL, default_factory=list
+ )
+ ),
+]
+
+# On MySQL the CASE expression that backs rendered_map_index mixes a stored
+# VARCHAR column (utf8mb4_bin, IMPLICIT) with CAST(map_index AS CHAR)
+# (utf8mb4_0900_ai_ci, IMPLICIT), which gives the whole expression NONE
+# coercibility. Comparing it against a bound parameter then fails with
+# "Illegal mix of collations". _MySQLCollate wraps the expression so that
+# on MySQL an explicit COLLATE clause is emitted (giving EXPLICIT coercibility);
+# on PostgreSQL and SQLite the wrapper is transparent.
+_rendered_map_index_collated = _MySQLCollate(
+ cast("ColumnElement[Any]", TaskInstance.rendered_map_index), "utf8mb4_0900_ai_ci"
+)
+
+
+QueryTIRenderedMapIndexPatternSearch = Annotated[
+ _SearchParam,
+ Depends(
+ search_param_factory(
+ _rendered_map_index_collated,
+ "rendered_map_index_pattern",
+ )
+ ),
+]
+
+QueryTIRenderedMapIndexPrefixPatternSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(
+ prefix_search_param_factory(
+ _rendered_map_index_collated,
+ "rendered_map_index_prefix_pattern",
+ )
+ ),
+]
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/xcom.py b/airflow-core/src/airflow/api_fastapi/common/parameters/xcom.py
new file mode 100644
index 00000000000..46b3f7fe263
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/xcom.py
@@ -0,0 +1,68 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import (
+ Annotated,
+)
+
+from fastapi import Depends
+
+from airflow.api_fastapi.common.parameters.search import (
+ _PrefixSearchParam,
+ _SearchParam,
+ prefix_search_param_factory,
+ search_param_factory,
+)
+from airflow.models.dag import DagModel
+from airflow.models.xcom import XComModel
+
+# XCom
+QueryXComKeyPatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(XComModel.key, "xcom_key_pattern"))
+]
+
+QueryXComKeyPrefixPatternSearch = Annotated[
+ _PrefixSearchParam, Depends(prefix_search_param_factory(XComModel.key, "xcom_key_prefix_pattern"))
+]
+
+
+QueryXComDagDisplayNamePatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(DagModel.dag_display_name, "dag_display_name_pattern"))
+]
+
+QueryXComDagDisplayNamePrefixPatternSearch = Annotated[
+ _PrefixSearchParam,
+ Depends(prefix_search_param_factory(DagModel.dag_display_name, "dag_display_name_prefix_pattern")),
+]
+
+QueryXComRunIdPatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(XComModel.run_id, "run_id_pattern"))
+]
+
+QueryXComRunIdPrefixPatternSearch = Annotated[
+ _PrefixSearchParam, Depends(prefix_search_param_factory(XComModel.run_id, "run_id_prefix_pattern"))
+]
+
+QueryXComTaskIdPatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(XComModel.task_id, "task_id_pattern"))
+]
+
+QueryXComTaskIdPrefixPatternSearch = Annotated[
+ _PrefixSearchParam, Depends(prefix_search_param_factory(XComModel.task_id, "task_id_prefix_pattern"))
+]
diff --git a/airflow-core/tests/unit/api_fastapi/common/test_parameters.py b/airflow-core/tests/unit/api_fastapi/common/test_parameters.py
index 4d2f473f4f8..4e4bfac6255 100644
--- a/airflow-core/tests/unit/api_fastapi/common/test_parameters.py
+++ b/airflow-core/tests/unit/api_fastapi/common/test_parameters.py
@@ -28,20 +28,21 @@ from sqlalchemy import select
from airflow.api_fastapi.common.parameters import (
FilterParam,
- NullableDatetimeRangeFilter,
RangeFilter,
SortParam,
- _AssetDependencyFilter,
- _ConsumingAssetFilter,
- _escape_like_pattern,
- _OwnersFilter,
- _PrefixPatternParam,
_PrefixSearchParam,
_SearchParam,
- _TaskDisplayNamePrefixPatternParam,
datetime_range_filter_factory,
filter_param_factory,
)
+from airflow.api_fastapi.common.parameters.asset import _AssetDependencyFilter, _ConsumingAssetFilter
+from airflow.api_fastapi.common.parameters.dag import _OwnersFilter
+from airflow.api_fastapi.common.parameters.range import NullableDatetimeRangeFilter
+from airflow.api_fastapi.common.parameters.search import (
+ _escape_like_pattern,
+ _PrefixPatternParam,
+ _TaskDisplayNamePrefixPatternParam,
+)
from airflow.models import DagModel, DagRun, Log
from airflow.models.errors import ParseImportError
from airflow.models.taskinstance import TaskInstance
(airflow) 09/09: [v3-3-test] Decode deadline alert interval and callback without generic deserialization (#72651) (#73304)
Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit f1093760c344f73b58d628a06e5a0788f6134461
Author: github-actions[bot] <41...@users.noreply.github.com>
AuthorDate: Fri Sep 18 07:29:01 2026 +0200
[v3-3-test] Decode deadline alert interval and callback without generic deserialization (#72651) (#73304)
* Decode deadline alert interval and callback without generic deserialization
decode_deadline_alert passed the Dag-author controlled interval and callback to
airflow.sdk.serde.deserialize, which imports the class named in the payload and
instantiates it with the encoded arguments. These decoders run in the scheduler
and the API server whenever a serialized Dag is loaded, so any class under the
airflow.* allow list could be constructed there.
The security model says a Dag author reaches those processes only through
registered plugins and providers, and the codebase enforces that at decode time
for timetables, priority weight strategies and operator extra links. Deadline
fields had no equivalent gate.
Both fields are now rebuilt from their encoded form directly:
* interval accepts a number, a timedelta payload, or a variable-interval payload
carrying a key, each reconstructed from primitives.
* callback accepts AsyncCallback or SyncCallback, selected from a fixed map
rather than imported by name, with path as a string, queue/executor as
optional strings, and unexpected fields refused.
Neither reaches serde.deserialize, so the class a Dag author names in either
field is never imported.
Filtering in front of deserialize was tried first and was not sufficient. serde
normalises the legacy {__type, __var} shape into __classname__ *inside*
deserialize, so a payload inspected beforehand carries no class name to reject.
Payloads are normalised before inspection here, and that case is tested.
Known residual, deliberately not closed here: callback kwargs are still passed
through generic deserialization, so a legitimate callback can carry an arbitrary
allow-listed class under its kwargs. Deferring that decode to the process that
runs the callback would close it, but the kwargs are consumed through two paths
using two different encodings, and getting either wrong hands user code an
encoded dict in place of its argument. The residual is not specific to
deadlines -- it is the general property of deserializing Dag-author data, shared
with every other serde call site. A test asserts the current behaviour so the
gap stays visible and any change to it has to be deliberate.
Tests assert the class is never constructed rather than that an error is raised.
Against unpatched sources the callback case reports DID NOT RAISE and the
interval case names the instance that had already been built.
* Accept pre-3.2 callback paths and validate fields per callback class
Review feedback on the deadline decoding gate.
Callbacks moved out of airflow.sdk.definitions.deadline in 3.2, so alerts
serialized by an earlier version name the old module. The allow list only held
the current path, which would have made those rows undecodable on upgrade --
the same backward-compatibility case the interval allow list already covers.
The permitted callback fields were a hardcoded set covering both subclasses at
once, so a payload could carry queue on a SyncCallback or executor on an
AsyncCallback. The set is per class, and each class already declares its own
via serialized_fields(), so ask it rather than restating the answer here and
letting the two drift. That also turns a TypeError raised from inside the
rebuild into the intended refusal, and the check now runs before anything is
reconstructed from the payload.
(cherry picked from commit c614c57062a95aade62cd5df774ae3c33bd57003)
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_012zrnHJHPchB83FtwrYRf5q
Co-authored-by: Jarek Potiuk <ja...@potiuk.com>
---
airflow-core/src/airflow/serialization/decoders.py | 150 ++++++++++++++---
.../unit/serialization/test_serialized_objects.py | 186 +++++++++++++++++++++
generated/known_sdk_imports_in_core.txt | 2 +-
3 files changed, 316 insertions(+), 22 deletions(-)
diff --git a/airflow-core/src/airflow/serialization/decoders.py b/airflow-core/src/airflow/serialization/decoders.py
index f561594f57f..d3bcb6f13a7 100644
--- a/airflow-core/src/airflow/serialization/decoders.py
+++ b/airflow-core/src/airflow/serialization/decoders.py
@@ -56,6 +56,7 @@ if TYPE_CHECKING:
from airflow.partition_mappers.base import PartitionMapper
from airflow.partition_mappers.wait_policy import WaitPolicy
from airflow.partition_mappers.window import Window
+ from airflow.sdk.definitions.callback import AsyncCallback, SyncCallback # noqa: SDK001
from airflow.timetables.base import Timetable as CoreTimetable
R = TypeVar("R")
@@ -181,15 +182,138 @@ def decode_deadline_reference(reference_data: dict):
return reference_class.deserialize_reference(reference_data)
+_TIMEDELTA_CLASSNAME = "datetime.timedelta"
+_VARIABLE_INTERVAL_CLASSNAMES = frozenset(
+ {
+ "airflow.sdk.definitions.deadline.VariableInterval",
+ "airflow.serialization.definitions.deadline.SerializedVariableInterval",
+ }
+)
+# The callback classes lived here until 3.2 moved them to ``...definitions.callback``,
+# so alerts serialized by an earlier version name this module instead.
+_LEGACY_CALLBACK_MODULE = "airflow.sdk.definitions.deadline"
+
+
+def _normalised_payload(encoded: Any, field: str) -> tuple[str, Any]:
+ """
+ Return ``(classname, data)`` for a serde payload, in either encoding.
+
+ ``serde`` accepts a legacy ``{"__type": ..., "__var": ...}`` shape and rewrites it
+ into the current one *inside* ``deserialize``. Anything inspecting the payload before
+ that call therefore has to normalise it first, or the legacy spelling carries no
+ ``__classname__`` at the moment it is looked at and slips past unexamined.
+ """
+ from airflow.sdk.serde import CLASSNAME, DATA, _convert
+
+ if not isinstance(encoded, dict):
+ raise ValueError(f"Deadline {field} is not a serialized object.")
+ converted = _convert(encoded)
+ if not isinstance(converted, dict):
+ raise ValueError(f"Deadline {field} is not a serialized object.")
+ if CLASSNAME not in converted:
+ raise ValueError(f"Deadline {field} does not name a class.")
+ return converted[CLASSNAME], converted.get(DATA)
+
+
+def _decode_deadline_interval(raw_interval: Any) -> datetime.timedelta | SerializedVariableInterval:
+ """
+ Build the interval from its encoded form without importing what the payload names.
+
+ Only three shapes are legitimate, and each is reconstructed from primitives directly.
+ Nothing here reaches ``serde.deserialize``, so no class named by a Dag author is
+ imported or instantiated in the scheduler or the API server.
+ """
+ # Backward compatibility: previously stored as total_seconds().
+ if isinstance(raw_interval, (int, float)) and not isinstance(raw_interval, bool):
+ return datetime.timedelta(seconds=raw_interval)
+
+ classname, data = _normalised_payload(raw_interval, "interval")
+
+ if classname == _TIMEDELTA_CLASSNAME:
+ if isinstance(data, (int, float)) and not isinstance(data, bool):
+ return datetime.timedelta(seconds=data)
+ raise ValueError("Deadline interval timedelta payload is not a number.")
+
+ if classname in _VARIABLE_INTERVAL_CLASSNAMES:
+ key = data.get("key") if isinstance(data, dict) else None
+ if not isinstance(key, str):
+ raise ValueError("Deadline interval variable payload has no string key.")
+ return SerializedVariableInterval(key=key)
+
+ raise ValueError(
+ f"Refusing to deserialize {classname!r} as a deadline interval. "
+ f"Permitted: {_TIMEDELTA_CLASSNAME}, {', '.join(sorted(_VARIABLE_INTERVAL_CLASSNAMES))}."
+ )
+
+
+def _decode_deadline_callback(raw_callback: Any) -> AsyncCallback | SyncCallback:
+ """
+ Build the callback from its encoded form without importing what the payload names.
+
+ ``kwargs`` is still passed through generic deserialization, and that is a deliberate,
+ documented limit rather than an oversight. Leaving it encoded would close a real
+ residual -- a legitimate callback can carry an arbitrary allow-listed class under its
+ kwargs, which serde constructs while the outer payload looks entirely valid -- but the
+ kwargs are consumed through two different paths that use two different encodings
+ (``BaseSerialization`` in the triggerer, serde here), and deferring the decode without
+ getting both exactly right silently hands user code an encoded dict in place of its
+ argument. That residual is not specific to deadlines: it is the general property of
+ deserializing Dag-author data, shared with every other serde call site. Closing it
+ belongs with that broader work, not smuggled in here.
+ """
+ from airflow.sdk.definitions.callback import (
+ AsyncCallback,
+ SyncCallback,
+ _SerializedCallbackPath,
+ )
+
+ permitted = {
+ f"{module}.{cls.__qualname__}": cls
+ for cls in (AsyncCallback, SyncCallback)
+ for module in (cls.__module__, _LEGACY_CALLBACK_MODULE)
+ }
+ classname, data = _normalised_payload(raw_callback, "callback")
+ callback_cls = permitted.get(classname)
+ if callback_cls is None:
+ raise ValueError(
+ f"Refusing to deserialize {classname!r} as a deadline callback. "
+ f"Permitted: {', '.join(sorted(permitted))}."
+ )
+ if not isinstance(data, dict):
+ raise ValueError("Deadline callback payload is not a mapping.")
+
+ # The fields each class declares are the fields it can be rebuilt from, so an unknown
+ # one means the payload does not describe this callback and nothing below should run.
+ # ``queue`` and ``executor`` belong to one subclass each, never to both.
+ permitted_fields = set(callback_cls.serialized_fields())
+ unexpected = set(data) - permitted_fields
+ if unexpected:
+ raise ValueError(f"Unexpected deadline callback fields: {', '.join(sorted(unexpected))}.")
+
+ path = data.get("path")
+ if not isinstance(path, str):
+ raise ValueError("Deadline callback payload has no string path.")
+
+ from airflow.sdk.serde import deserialize
+
+ raw_kwargs = data.get("kwargs") or {}
+ fields: dict[str, Any] = {"kwargs": deserialize(raw_kwargs) if raw_kwargs else {}}
+ for optional in sorted(permitted_fields - {"path", "kwargs"}):
+ if optional in data:
+ value = data[optional]
+ if value is not None and not isinstance(value, str):
+ raise ValueError(f"Deadline callback {optional} is not a string.")
+ fields[optional] = value
+
+ return callback_cls(callback_callable=_SerializedCallbackPath(path), **fields)
+
+
def decode_deadline_alert(encoded_data: dict):
"""
Decode a previously serialized deadline alert.
:meta private:
"""
- from airflow.sdk.definitions.deadline import VariableInterval
- from airflow.sdk.serde import deserialize
-
data = encoded_data.get(Encoding.VAR, encoded_data)
reference_data = data[DeadlineAlertFields.REFERENCE]
@@ -203,28 +327,12 @@ def decode_deadline_alert(encoded_data: dict):
"from a version that supports VariableInterval. Downgrade is not fully reversible."
)
- interval: datetime.timedelta | SerializedVariableInterval
-
- # Backward compatibility: previously interval was stored as total_seconds() (float/int).
- # Handle numeric values by converting to timedelta.
- if isinstance(raw_interval, (int, float)):
- interval = datetime.timedelta(seconds=raw_interval)
- else:
- deserialized = deserialize(raw_interval)
-
- if isinstance(deserialized, datetime.timedelta):
- interval = deserialized
- elif isinstance(deserialized, SerializedVariableInterval):
- interval = deserialized
- elif isinstance(deserialized, VariableInterval):
- interval = SerializedVariableInterval(key=deserialized.key)
- else:
- raise TypeError(f"Invalid interval type: {type(deserialized).__name__}")
+ interval = _decode_deadline_interval(raw_interval)
return SerializedDeadlineAlert(
reference=reference,
interval=interval,
- callback=deserialize(data[DeadlineAlertFields.CALLBACK]),
+ callback=_decode_deadline_callback(data[DeadlineAlertFields.CALLBACK]),
name=data.get(DeadlineAlertFields.NAME),
)
diff --git a/airflow-core/tests/unit/serialization/test_serialized_objects.py b/airflow-core/tests/unit/serialization/test_serialized_objects.py
index 24d4cd8b70c..495be4c3b32 100644
--- a/airflow-core/tests/unit/serialization/test_serialized_objects.py
+++ b/airflow-core/tests/unit/serialization/test_serialized_objects.py
@@ -79,6 +79,7 @@ from airflow.sdk.definitions.deadline import (
AsyncCallback,
DeadlineAlert,
DeadlineReference,
+ SyncCallback,
VariableInterval,
)
from airflow.sdk.definitions.decorators import task
@@ -1602,3 +1603,188 @@ def test_serialized_dag_getitem_returns_task_group(dag_maker):
ser_dag = DagSerialization.from_dict(ser_dict)
assert isinstance(ser_dag, SerializedDAG)
assert ser_dag["section"].group_id == tg.group_id
+
+
+class _DeadlineGadget:
+ """Stands in for any allow-listed class a Dag author could name in a deadline field.
+
+ Deliberately constructible by serde -- it carries the ``serialize`` /
+ ``deserialize`` pair serde requires. A gadget serde cannot build would make the
+ test pass for the wrong reason: deserialization would fail on its own and the
+ "was it constructed" assertion would never be exercised.
+ """
+
+ instantiated = False
+
+ def __init__(self, **kwargs):
+ type(self).instantiated = True
+ self.kwargs = kwargs
+
+ def serialize(self):
+ return {}
+
+ @staticmethod
+ def deserialize(data, version):
+ return _DeadlineGadget(**(data or {}))
+
+
+def _encode_as(classname: str) -> dict:
+ """Build a serde payload naming ``classname``."""
+ from airflow.sdk.serde import CLASSNAME, DATA, VERSION
+
+ return {CLASSNAME: classname, VERSION: 1, DATA: {}}
+
+
+@pytest.mark.parametrize("field", [DeadlineAlertFields.CALLBACK, DeadlineAlertFields.INTERVAL])
+def test_deadline_fields_refuse_unexpected_classes_without_constructing_them(field):
+ """A Dag author must not be able to name an arbitrary class in a deadline field.
+
+ These decoders run in the scheduler and API server whenever a serialized Dag is
+ loaded, and the Security Model says a Dag author reaches those processes only
+ through registered plugins. The assertion that matters is that the class is never
+ *constructed*: `deserialize()` instantiates before returning, so a check on the
+ returned object would already be too late for a class with side effects in
+ `__init__`.
+ """
+ from unittest import mock
+
+ from airflow.sdk import serde
+
+ gadget_name = f"{_DeadlineGadget.__module__}.{_DeadlineGadget.__qualname__}"
+ _DeadlineGadget.instantiated = False
+
+ valid = DeadlineAlert(
+ reference=DeadlineReference.DAGRUN_QUEUED_AT,
+ interval=timedelta(hours=1),
+ callback=AsyncCallback(TEST_CALLBACK_PATH, kwargs=TEST_CALLBACK_KWARGS),
+ )
+ serialized = BaseSerialization.serialize(valid)
+ serialized[Encoding.VAR][field] = _encode_as(gadget_name)
+
+ # Make the gadget acceptable to serde itself, so the refusal comes from the
+ # deadline gate rather than from the general deserialization allow list.
+ with mock.patch.object(serde, "_extra_allowed", serde._extra_allowed | {gadget_name}):
+ with pytest.raises(ValueError, match="Refusing to deserialize"):
+ BaseSerialization.deserialize(serialized)
+
+ assert _DeadlineGadget.instantiated is False, "the class was constructed before being rejected"
+
+
+def test_deadline_callback_kwargs_still_construct_nested_classes():
+ """Documents a known residual: kwargs are still generically deserialized.
+
+ The outer payload is a valid AsyncCallback and the class sits under ``kwargs``,
+ which serde deserializes recursively. This is NOT closed here, and the test asserts
+ the current behaviour rather than the desired one so the gap is visible and a future
+ change has to update it deliberately.
+
+ It is not specific to deadlines -- it is the general property of deserializing
+ Dag-author data, shared with every other serde call site. Closing it means deferring
+ the kwargs decode to the process that runs the callback, which spans two consumption
+ paths using two different encodings and belongs with that broader work.
+ """
+ from unittest import mock
+
+ from airflow.sdk import serde
+
+ gadget_name = f"{_DeadlineGadget.__module__}.{_DeadlineGadget.__qualname__}"
+ _DeadlineGadget.instantiated = False
+
+ valid = DeadlineAlert(
+ reference=DeadlineReference.DAGRUN_QUEUED_AT,
+ interval=timedelta(hours=1),
+ callback=AsyncCallback(TEST_CALLBACK_PATH, kwargs=TEST_CALLBACK_KWARGS),
+ )
+ serialized = BaseSerialization.serialize(valid)
+ callback_payload = serialized[Encoding.VAR][DeadlineAlertFields.CALLBACK]
+ callback_payload[serde.DATA]["kwargs"] = {"evil": _encode_as(gadget_name)}
+
+ with mock.patch.object(serde, "_extra_allowed", serde._extra_allowed | {gadget_name}):
+ BaseSerialization.deserialize(serialized)
+
+ assert _DeadlineGadget.instantiated is True, (
+ "kwargs deserialization behaviour changed -- if this is now closed, update the "
+ "docstring on _decode_deadline_callback and this test together"
+ )
+
+
+@pytest.mark.parametrize("field", [DeadlineAlertFields.CALLBACK, DeadlineAlertFields.INTERVAL])
+def test_deadline_fields_refuse_legacy_encoded_classes(field):
+ """The legacy ``{"__type", "__var"}`` spelling must not slip past the check.
+
+ serde rewrites that shape into ``__classname__`` *inside* deserialize, so a payload
+ inspected beforehand carries no ``__classname__`` at all and an unnormalised check
+ sees nothing to reject.
+ """
+ from unittest import mock
+
+ from airflow.sdk import serde
+ from airflow.sdk._shared.serialization import OLD_DATA, OLD_TYPE
+
+ gadget_name = f"{_DeadlineGadget.__module__}.{_DeadlineGadget.__qualname__}"
+ _DeadlineGadget.instantiated = False
+
+ valid = DeadlineAlert(
+ reference=DeadlineReference.DAGRUN_QUEUED_AT,
+ interval=timedelta(hours=1),
+ callback=AsyncCallback(TEST_CALLBACK_PATH, kwargs=TEST_CALLBACK_KWARGS),
+ )
+ serialized = BaseSerialization.serialize(valid)
+ serialized[Encoding.VAR][field] = {OLD_TYPE: gadget_name, OLD_DATA: {}}
+
+ with mock.patch.object(serde, "_extra_allowed", serde._extra_allowed | {gadget_name}):
+ with pytest.raises(ValueError, match="Refusing to deserialize"):
+ BaseSerialization.deserialize(serialized)
+
+ assert _DeadlineGadget.instantiated is False
+
+
+@pytest.mark.parametrize("callback_cls", [AsyncCallback, SyncCallback])
+def test_deadline_callback_accepts_pre_3_2_module_path(callback_cls):
+ """Callbacks serialized before 3.2 name the module they were defined in back then.
+
+ 3.2 moved them out of ``airflow.sdk.definitions.deadline`` into
+ ``...definitions.callback``, so an alert stored by an earlier version carries the old
+ path. Rejecting it would make those rows undecodable on upgrade.
+ """
+ from airflow.sdk import serde
+
+ valid = DeadlineAlert(
+ reference=DeadlineReference.DAGRUN_QUEUED_AT,
+ interval=timedelta(hours=1),
+ callback=callback_cls(TEST_CALLBACK_PATH, kwargs=TEST_CALLBACK_KWARGS),
+ )
+ serialized = BaseSerialization.serialize(valid)
+ payload = serialized[Encoding.VAR][DeadlineAlertFields.CALLBACK]
+ payload[serde.CLASSNAME] = f"airflow.sdk.definitions.deadline.{callback_cls.__qualname__}"
+
+ decoded = BaseSerialization.deserialize(serialized)
+
+ assert isinstance(decoded.callback, callback_cls)
+ assert decoded.callback.path == TEST_CALLBACK_PATH
+ assert decoded.callback.kwargs == TEST_CALLBACK_KWARGS
+
+
+@pytest.mark.parametrize(
+ ("callback_cls", "foreign_field"),
+ [(AsyncCallback, "executor"), (SyncCallback, "queue")],
+ ids=["async-rejects-executor", "sync-rejects-queue"],
+)
+def test_deadline_callback_rejects_field_belonging_to_the_other_subclass(callback_cls, foreign_field):
+ """``queue`` and ``executor`` belong to one subclass each, not to both.
+
+ Accepting either for both classes would hand the constructor an argument it does not
+ take, turning a malformed payload into a TypeError from deep inside the rebuild.
+ """
+ from airflow.sdk import serde
+
+ valid = DeadlineAlert(
+ reference=DeadlineReference.DAGRUN_QUEUED_AT,
+ interval=timedelta(hours=1),
+ callback=callback_cls(TEST_CALLBACK_PATH, kwargs=TEST_CALLBACK_KWARGS),
+ )
+ serialized = BaseSerialization.serialize(valid)
+ serialized[Encoding.VAR][DeadlineAlertFields.CALLBACK][serde.DATA][foreign_field] = "something"
+
+ with pytest.raises(ValueError, match=f"Unexpected deadline callback fields: {foreign_field}"):
+ BaseSerialization.deserialize(serialized)
diff --git a/generated/known_sdk_imports_in_core.txt b/generated/known_sdk_imports_in_core.txt
index c5a26b3e508..fe955c7f1d1 100644
--- a/generated/known_sdk_imports_in_core.txt
+++ b/generated/known_sdk_imports_in_core.txt
@@ -23,7 +23,7 @@ airflow-core/src/airflow/models/xcom_arg.py::1
airflow-core/src/airflow/plugins_manager.py::1
airflow-core/src/airflow/providers_manager.py::5
airflow-core/src/airflow/secrets/__init__.py::1
-airflow-core/src/airflow/serialization/decoders.py::2
+airflow-core/src/airflow/serialization/decoders.py::3
airflow-core/src/airflow/serialization/definitions/baseoperator.py::1
airflow-core/src/airflow/serialization/definitions/dag.py::1
airflow-core/src/airflow/serialization/definitions/mappedoperator.py::5
(airflow) 01/09: [v3-3-test] Clarify that AccessView.JOBS is the Edge worker management permission (#72627) (#73073)
Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 9d2a71d4b8dddbb5e670f38eb7cb41dc2f074763
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Sun Sep 13 16:45:30 2026 +0630
[v3-3-test] Clarify that AccessView.JOBS is the Edge worker management permission (#72627) (#73073)
The Edge UI plugin docs say that "can read on Plugins" and "can read on
Jobs" let you view the UI and manage the workers, but they do not say how
the two permissions differ, and they do not mention what the default
Viewer role already holds.
Both gaps matter, because the endpoints and the navigation are gated
differently:
- The worker management endpoints under /edge_worker/ui/ check only
AccessView.JOBS, and the check is not method-aware -- the same
dependency guards the GET reads and the POST/PATCH/DELETE mutations.
- "can read on Plugins" only controls whether the plugin shows up in the
UI navigation. It is not required in order to call the endpoints.
So "can read on Jobs" alone is enough to shut down, delete, re-queue and
retune Edge workers, whether or not the plugin is visible to that user.
That is intentional -- AccessView.JOBS is the management permission for
the plugin rather than a read-only grant -- but it reads as surprising
from the code alone, where a permission named "can read" guards mutating
routes. It is more surprising in a default Flask AppBuilder setup, where
the Viewer role includes (ACTION_CAN_READ, RESOURCE_JOB) but not the
Plugins read: such a user cannot see the Edge plugin and can still reach
its management endpoints.
Adds a warning to the UI plugin docs stating the intent, the split
between the two permissions, the consequence for the default Viewer role,
and the concrete action for deployments where Viewers must not manage
workers. Points at the existing "fine granular access control" entry in
architecture.rst rather than restating it.
Documentation only; no behaviour change.
(cherry picked from commit 1391b0934240aa70c90d7cad186d71a235bdfdd2)
---
providers/edge3/docs/ui_plugin.rst | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/providers/edge3/docs/ui_plugin.rst b/providers/edge3/docs/ui_plugin.rst
index d524b555e3d..ff2c969e44d 100644
--- a/providers/edge3/docs/ui_plugin.rst
+++ b/providers/edge3/docs/ui_plugin.rst
@@ -64,5 +64,25 @@ To be able to use the UI plugin you need to be in role "Admin" or "Op" or have t
configure the remote workers (Technical key: AccessView.JOBS). With this permission you can also manage
the workers like adjusting queues, concurrency, set them to maintenance mode or shutdown the workers.
+.. warning::
+
+ "can read on Jobs" (``AccessView.JOBS``) is the **management** permission for Edge workers, not a
+ read-only one. It is deliberately the single permission gating the whole plugin, and the worker
+ management endpoints under ``/edge_worker/ui/`` check only this permission -- the HTTP method is not
+ part of the check. "can read on Plugins" governs only whether the plugin appears in the UI
+ navigation; it is not required in order to call the endpoints.
+
+ A principal holding "can read on Jobs" can therefore shut down, delete, re-queue and retune Edge
+ workers by calling those endpoints directly, whether or not the plugin is visible to them.
+
+ This matters for the default ``Viewer`` role, which **includes "can read on Jobs"**. In a default
+ Flask AppBuilder setup a Viewer does not see the Edge plugin in the navigation (Viewer has no
+ "can read on Plugins"), but can still reach the worker management endpoints. If Viewers in your
+ deployment must not manage Edge workers, remove "can read on Jobs" from that role or give those
+ users a custom role without it.
+
+ Finer-grained separation of read and management permissions for Edge workers is not implemented;
+ it is listed under :doc:`architecture` as a known missing feature.
+
Note that maintenance mode can also be adjusted via CLI.
See :ref:`deployment:maintenance` for more details.