From 5b243792b6d736179a4a5617661db267e6b38887 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 8 Apr 2026 14:17:15 +0800 Subject: [PATCH 01/75] Add Task Coordinators and Dag File Processor --- .../src/airflow/provider.yaml.schema.json | 14 ++++++++++++ .../src/airflow/provider_info.schema.json | 14 ++++++++++++ airflow-core/src/airflow/providers_manager.py | 22 +++++++++++++++++++ .../airflow/sdk/providers_manager_runtime.py | 22 +++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/airflow-core/src/airflow/provider.yaml.schema.json b/airflow-core/src/airflow/provider.yaml.schema.json index 5714b8db658c5..2412347a56c30 100644 --- a/airflow-core/src/airflow/provider.yaml.schema.json +++ b/airflow-core/src/airflow/provider.yaml.schema.json @@ -624,6 +624,20 @@ } } }, + "task-coordinators": { + "type": "array", + "description": "Task Coordinator class names", + "items": { + "type": "string" + } + }, + "dag-file-processors": { + "type": "array", + "description": "Dag File Processor class names", + "items": { + "type": "string" + } + }, "source-date-epoch": { "type": "integer", "description": "Source date epoch - seconds since epoch (gmtime) when the release documentation was prepared. Used to generate reproducible package builds with flint.", diff --git a/airflow-core/src/airflow/provider_info.schema.json b/airflow-core/src/airflow/provider_info.schema.json index 86fc726a05168..1f9ea31bf1274 100644 --- a/airflow-core/src/airflow/provider_info.schema.json +++ b/airflow-core/src/airflow/provider_info.schema.json @@ -446,6 +446,20 @@ "type": "string" } } + }, + "task-coordinators": { + "type": "array", + "description": "Task Coordinator class names", + "items": { + "type": "string" + } + }, + "dag-file-processors": { + "type": "array", + "description": "Dag File Processor class names", + "items": { + "type": "string" + } } }, "definitions": { diff --git a/airflow-core/src/airflow/providers_manager.py b/airflow-core/src/airflow/providers_manager.py index 6fefcbc39b06d..c74a8aa97a4a7 100644 --- a/airflow-core/src/airflow/providers_manager.py +++ b/airflow-core/src/airflow/providers_manager.py @@ -448,6 +448,7 @@ def __init__(self): ) # Set of plugins contained in providers self._plugins_set: set[PluginInfo] = set() + self._dag_file_processors: list[str] = [] self._init_airflow_core_hooks() self._runtime_manager = None @@ -625,6 +626,12 @@ def initialize_providers_configuration(self): self.initialize_providers_list() self._discover_config() + @provider_info_cache("dag_file_processors") + def initialize_providers_dag_file_processors(self): + """Lazy initialization of providers dag file processors.""" + self.initialize_providers_list() + self._discover_dag_file_processors() + @provider_info_cache("plugins") def initialize_providers_plugins(self): self.initialize_providers_list() @@ -1280,6 +1287,14 @@ def _discover_config(self) -> None: if provider.data.get("config"): self._provider_configs[provider_package] = provider.data.get("config") # type: ignore[assignment] + def _discover_dag_file_processors(self) -> None: + """Retrieve all dag file processors defined in the providers.""" + for provider_package, provider in self._provider_dict.items(): + for dag_file_processor_class_path in provider.data.get("dag-file-processors", []): + if _correctness_check(provider_package, dag_file_processor_class_path, provider): + self._dag_file_processors.append(dag_file_processor_class_path) + self._dag_file_processors = sorted(set(self._dag_file_processors)) + def _discover_plugins(self) -> None: """Retrieve all plugins defined in the providers.""" for provider_package, provider in self._provider_dict.items(): @@ -1477,6 +1492,12 @@ def db_managers(self) -> list[str]: self.initialize_providers_db_managers() return sorted(self._db_manager_class_name_set) + @property + def dag_file_processors(self) -> list[str]: + """Returns dag file processor class paths available in providers.""" + self.initialize_providers_dag_file_processors() + return self._dag_file_processors + @property def filesystem_module_names(self) -> list[str]: self.initialize_providers_filesystems() @@ -1548,6 +1569,7 @@ def _cleanup(self): self._trigger_info_set.clear() self._notification_info_set.clear() self._plugins_set.clear() + self._dag_file_processors.clear() self._cli_command_functions_set.clear() self._cli_command_provider_name_set.clear() diff --git a/task-sdk/src/airflow/sdk/providers_manager_runtime.py b/task-sdk/src/airflow/sdk/providers_manager_runtime.py index e28ed3fe14a83..716c69f7cf575 100644 --- a/task-sdk/src/airflow/sdk/providers_manager_runtime.py +++ b/task-sdk/src/airflow/sdk/providers_manager_runtime.py @@ -150,6 +150,7 @@ def __init__(self): # Keeps dict of hooks keyed by connection type. They are lazy evaluated at access time self._hooks_lazy_dict: LazyDictWithCache[str, HookInfo | Callable] = LazyDictWithCache() self._plugins_set: set[PluginInfo] = set() + self._task_coordinators: list[str] = [] self._provider_schema_validator = _create_provider_info_schema_validator() self._init_airflow_core_hooks() # Populated by initialize_provider_configs(); holds provider-contributed config sections. @@ -220,6 +221,12 @@ def initialize_providers_taskflow_decorator(self): self.initialize_providers_list() self._discover_taskflow_decorators() + @provider_info_cache("task_coordinators") + def initialize_providers_task_coordinators(self): + """Lazy initialization of providers workload coordinators.""" + self.initialize_providers_list() + self._discover_task_coordinators() + @provider_info_cache("provider_configs") def initialize_provider_configs(self): """Lazy initialization of provider configuration metadata and merge it into SDK ``conf``.""" @@ -464,6 +471,14 @@ def _import_hook( connection_testable=hasattr(hook_class, "test_connection"), ) + def _discover_task_coordinators(self) -> None: + """Retrieve all workload coordinators defined in the providers.""" + for provider_package, provider in self._provider_dict.items(): + for coordinator_class_path in provider.data.get("task-coordinators", []): + if _correctness_check(provider_package, coordinator_class_path, provider): + self._task_coordinators.append(coordinator_class_path) + self._task_coordinators = sorted(set(self._task_coordinators)) + def _discover_filesystems(self) -> None: """Retrieve all filesystems defined in the providers.""" for provider_package, provider in self._provider_dict.items(): @@ -611,6 +626,12 @@ def plugins(self) -> list[PluginInfo]: self.initialize_providers_plugins() return sorted(self._plugins_set, key=lambda x: x.plugin_class) + @property + def task_coordinators(self) -> list[str]: + """Returns workload coordinator class paths available in providers.""" + self.initialize_providers_task_coordinators() + return self._task_coordinators + @property def provider_configs(self) -> list[tuple[str, dict[str, Any]]]: self.initialize_provider_configs() @@ -643,6 +664,7 @@ def _cleanup(self): self._asset_uri_handlers.clear() self._asset_factories.clear() self._asset_to_openlineage_converters.clear() + self._task_coordinators.clear() self._provider_configs.clear() # Imported lazily to preserve SDK conf lazy initialization and avoid a configuration/runtime cycle. From 3e1f0bc2de67bff5ca805f74f6c4431492b49712 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 8 Apr 2026 15:40:05 +0800 Subject: [PATCH 02/75] Add initial Java provider for Apache Airflow - Introduced the `apache-airflow-providers-languages-java` package with version 0.1.0. - Added Java-specific task coordinators and DAG file processors. - Created documentation including README, changelog, and installation instructions. - Implemented provider info retrieval and commit tracking. - Established testing framework with initial unit tests for Java provider components. --- .github/boring-cyborg.yml | 3 + .../unit/always/test_providers_manager.py | 22 ++ providers/languages/java/.gitignore | 1 + providers/languages/java/LICENSE | 201 ++++++++++++++++++ providers/languages/java/NOTICE | 5 + providers/languages/java/README.rst | 60 ++++++ .../java/docs/.latest-doc-only-change.txt | 1 + providers/languages/java/docs/changelog.rst | 40 ++++ providers/languages/java/docs/commits.rst | 35 +++ providers/languages/java/docs/conf.py | 27 +++ providers/languages/java/docs/index.rst | 88 ++++++++ .../installing-providers-from-sources.rst | 18 ++ providers/languages/java/docs/security.rst | 18 ++ providers/languages/java/provider.yaml | 45 ++++ providers/languages/java/pyproject.toml | 111 ++++++++++ .../languages/java/src/airflow/__init__.py | 17 ++ .../java/src/airflow/providers/__init__.py | 17 ++ .../airflow/providers/languages/__init__.py | 17 ++ .../providers/languages/java/__init__.py | 39 ++++ .../languages/java/dag_file_processors.py | 25 +++ .../languages/java/get_provider_info.py | 38 ++++ .../languages/java/task_coordinators.py | 25 +++ providers/languages/java/tests/conftest.py | 19 ++ .../languages/java/tests/unit/__init__.py | 17 ++ .../java/tests/unit/languages/__init__.py | 16 ++ .../tests/unit/languages/java/__init__.py | 16 ++ .../unit/languages/java/test_java_provider.py | 47 ++++ pyproject.toml | 10 + scripts/ci/docker-compose/remove-sources.yml | 1 + scripts/ci/docker-compose/tests-sources.yml | 1 + .../sdk/execution_time/task_coordinator.py | 20 ++ .../test_providers_manager_runtime.py | 21 ++ uv.lock | 37 ++++ 33 files changed, 1058 insertions(+) create mode 100644 providers/languages/java/.gitignore create mode 100644 providers/languages/java/LICENSE create mode 100644 providers/languages/java/NOTICE create mode 100644 providers/languages/java/README.rst create mode 100644 providers/languages/java/docs/.latest-doc-only-change.txt create mode 100644 providers/languages/java/docs/changelog.rst create mode 100644 providers/languages/java/docs/commits.rst create mode 100644 providers/languages/java/docs/conf.py create mode 100644 providers/languages/java/docs/index.rst create mode 100644 providers/languages/java/docs/installing-providers-from-sources.rst create mode 100644 providers/languages/java/docs/security.rst create mode 100644 providers/languages/java/provider.yaml create mode 100644 providers/languages/java/pyproject.toml create mode 100644 providers/languages/java/src/airflow/__init__.py create mode 100644 providers/languages/java/src/airflow/providers/__init__.py create mode 100644 providers/languages/java/src/airflow/providers/languages/__init__.py create mode 100644 providers/languages/java/src/airflow/providers/languages/java/__init__.py create mode 100644 providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py create mode 100644 providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py create mode 100644 providers/languages/java/src/airflow/providers/languages/java/task_coordinators.py create mode 100644 providers/languages/java/tests/conftest.py create mode 100644 providers/languages/java/tests/unit/__init__.py create mode 100644 providers/languages/java/tests/unit/languages/__init__.py create mode 100644 providers/languages/java/tests/unit/languages/java/__init__.py create mode 100644 providers/languages/java/tests/unit/languages/java/test_java_provider.py create mode 100644 task-sdk/src/airflow/sdk/execution_time/task_coordinator.py diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml index 21b44a9fb840f..afdcb80deaaf7 100644 --- a/.github/boring-cyborg.yml +++ b/.github/boring-cyborg.yml @@ -189,6 +189,9 @@ labelPRBasedOnFilePath: provider:keycloak: - providers/keycloak/** + provider:languages-java: + - providers/languages/java/** + provider:microsoft-azure: - providers/microsoft/azure/** diff --git a/airflow-core/tests/unit/always/test_providers_manager.py b/airflow-core/tests/unit/always/test_providers_manager.py index afa473e80a4f0..bce1125e98a3d 100644 --- a/airflow-core/tests/unit/always/test_providers_manager.py +++ b/airflow-core/tests/unit/always/test_providers_manager.py @@ -258,6 +258,28 @@ def test_dialects(self): assert len(dialect_class_names) == 3 assert dialect_class_names == ["default", "mssql", "postgresql"] + @patch("airflow.providers_manager.import_string") + def test_dag_file_processors(self, mock_import_string): + mock_import_string.return_value = object() + providers_manager = ProvidersManager() + providers_manager._provider_dict = LazyDictWithCache() + providers_manager._provider_dict["apache-airflow-providers-languages-java"] = ProviderInfo( + version="0.0.1", + data={ + "dag-file-processors": [ + "airflow.providers.languages.java.dag_file_processors.ZProcessor", + "airflow.providers.languages.java.dag_file_processors.AProcessor", + "airflow.providers.languages.java.dag_file_processors.ZProcessor", + ] + }, + ) + + with patch.object(providers_manager, "initialize_providers_list"): + assert providers_manager.dag_file_processors == [ + "airflow.providers.languages.java.dag_file_processors.AProcessor", + "airflow.providers.languages.java.dag_file_processors.ZProcessor", + ] + class TestWithoutCheckProviderManager: @pytest.fixture(autouse=True) diff --git a/providers/languages/java/.gitignore b/providers/languages/java/.gitignore new file mode 100644 index 0000000000000..bff2d7629604d --- /dev/null +++ b/providers/languages/java/.gitignore @@ -0,0 +1 @@ +*.iml diff --git a/providers/languages/java/LICENSE b/providers/languages/java/LICENSE new file mode 100644 index 0000000000000..11069edd79019 --- /dev/null +++ b/providers/languages/java/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed 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. diff --git a/providers/languages/java/NOTICE b/providers/languages/java/NOTICE new file mode 100644 index 0000000000000..a51bd9390d030 --- /dev/null +++ b/providers/languages/java/NOTICE @@ -0,0 +1,5 @@ +Apache Airflow +Copyright 2016-2026 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/providers/languages/java/README.rst b/providers/languages/java/README.rst new file mode 100644 index 0000000000000..19527bd4191a5 --- /dev/null +++ b/providers/languages/java/README.rst @@ -0,0 +1,60 @@ + +.. 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. + +.. NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN! + +.. IF YOU WANT TO MODIFY TEMPLATE FOR THIS FILE, YOU SHOULD MODIFY THE TEMPLATE + ``PROVIDER_README_TEMPLATE.rst.jinja2`` IN the ``dev/breeze/src/airflow_breeze/templates`` DIRECTORY + +Package ``apache-airflow-providers-languages-java`` + +Release: ``0.1.0`` + + +Java language support for Apache Airflow task coordinators and DAG file processors. + + +Provider package +---------------- + +This is a provider package for ``languages.java`` provider. All classes for this provider package +are in ``airflow.providers.languages.java`` python package. + +You can find package information and changelog for the provider +in the `documentation `_. + +Installation +------------ + +You can install this package on top of an existing Airflow installation (see ``Requirements`` below +for the minimum Airflow version supported) via +``pip install apache-airflow-providers-languages-java`` + +The package supports the following python versions: 3.10,3.11,3.12,3.13,3.14 + +Requirements +------------ + +================== ================== +PIP package Version required +================== ================== +``apache-airflow`` ``>=3.0.0`` +================== ================== + +The changelog for the provider package can be found in the +`changelog `_. diff --git a/providers/languages/java/docs/.latest-doc-only-change.txt b/providers/languages/java/docs/.latest-doc-only-change.txt new file mode 100644 index 0000000000000..2c1ab461a9c8e --- /dev/null +++ b/providers/languages/java/docs/.latest-doc-only-change.txt @@ -0,0 +1 @@ +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/languages/java/docs/changelog.rst b/providers/languages/java/docs/changelog.rst new file mode 100644 index 0000000000000..e88eef968d669 --- /dev/null +++ b/providers/languages/java/docs/changelog.rst @@ -0,0 +1,40 @@ + .. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + .. http://www.apache.org/licenses/LICENSE-2.0 + + .. Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + + +.. NOTE TO CONTRIBUTORS: + Please, only add notes to the Changelog just below the "Changelog" header when there are some breaking changes + and you want to add an explanation to the users on how they are supposed to deal with them. + The changelog is updated and maintained semi-automatically by release manager. + + +``apache-airflow-providers-languages-java`` + + +Changelog +--------- + +0.1.0 +..... + +Features +~~~~~~~~ + +* ``Add the initial Java provider with task coordinators and DAG file processors`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): diff --git a/providers/languages/java/docs/commits.rst b/providers/languages/java/docs/commits.rst new file mode 100644 index 0000000000000..5b15c6b7169d6 --- /dev/null +++ b/providers/languages/java/docs/commits.rst @@ -0,0 +1,35 @@ + + .. 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. + + .. NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN! + + .. IF YOU WANT TO MODIFY THIS FILE, YOU SHOULD MODIFY THE TEMPLATE + `PROVIDER_COMMITS_TEMPLATE.rst.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY + + .. THE REMAINDER OF THE FILE IS AUTOMATICALLY GENERATED. IT WILL BE OVERWRITTEN! + +Package apache-airflow-providers-languages-java +------------------------------------------------------ + +Java language support for Apache Airflow task coordinators and DAG file processors. + + +This is detailed commit list of changes for versions provider package: ``languages.java``. +For high-level changelog, see :doc:`package information including changelog `. + +.. airflow-providers-commits:: diff --git a/providers/languages/java/docs/conf.py b/providers/languages/java/docs/conf.py new file mode 100644 index 0000000000000..bbda22da2f1d5 --- /dev/null +++ b/providers/languages/java/docs/conf.py @@ -0,0 +1,27 @@ +# Disable Flake8 because of all the sphinx imports +# +# 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. +"""Configuration of Providers docs building.""" + +from __future__ import annotations + +import os + +os.environ["AIRFLOW_PACKAGE_NAME"] = "apache-airflow-providers-languages-java" + +from docs.provider_conf import * # noqa: F403 diff --git a/providers/languages/java/docs/index.rst b/providers/languages/java/docs/index.rst new file mode 100644 index 0000000000000..0c161234ff871 --- /dev/null +++ b/providers/languages/java/docs/index.rst @@ -0,0 +1,88 @@ + .. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + .. http://www.apache.org/licenses/LICENSE-2.0 + + .. Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +``apache-airflow-providers-languages-java`` +=========================================== + +The Languages: Java provider registers Java-specific task coordinator and DAG file processor classes for Apache Airflow. + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Basics + + Home + Changelog + Security + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: References + + Python API <_api/airflow/providers/languages/java/index> + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Resources + + PyPI Repository + Installing from sources + +.. THE REMAINDER OF THE FILE IS AUTOMATICALLY GENERATED. IT WILL BE OVERWRITTEN AT RELEASE TIME! + + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Commits + + Detailed list of commits + + +apache-airflow-providers-languages-java package +------------------------------------------------------ + +Java language support for Apache Airflow task coordinators and DAG file processors. + + +Release: 0.1.0 + +Provider package +---------------- + +This package is for the ``languages.java`` provider. +All classes for this package are included in the ``airflow.providers.languages.java`` python package. + +Installation +------------ + +You can install this package on top of an existing Airflow installation via +``pip install apache-airflow-providers-languages-java``. +For the minimum Airflow version supported, see ``Requirements`` below. + +Requirements +------------ + +The minimum Apache Airflow version supported by this provider distribution is ``3.0.0``. + +================== ================== +PIP package Version required +================== ================== +``apache-airflow`` ``>=3.0.0`` +================== ================== diff --git a/providers/languages/java/docs/installing-providers-from-sources.rst b/providers/languages/java/docs/installing-providers-from-sources.rst new file mode 100644 index 0000000000000..a72b45ffaa6e8 --- /dev/null +++ b/providers/languages/java/docs/installing-providers-from-sources.rst @@ -0,0 +1,18 @@ + .. 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. + +.. include:: /../../../devel-common/src/sphinx_exts/includes/installing-providers-from-sources.rst diff --git a/providers/languages/java/docs/security.rst b/providers/languages/java/docs/security.rst new file mode 100644 index 0000000000000..15a0ebbb2d054 --- /dev/null +++ b/providers/languages/java/docs/security.rst @@ -0,0 +1,18 @@ + .. 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. + +.. include:: /../../../devel-common/src/sphinx_exts/includes/security.rst diff --git a/providers/languages/java/provider.yaml b/providers/languages/java/provider.yaml new file mode 100644 index 0000000000000..f7602b4bdee19 --- /dev/null +++ b/providers/languages/java/provider.yaml @@ -0,0 +1,45 @@ +# 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. + +--- +package-name: apache-airflow-providers-languages-java +name: "Languages: Java" +description: | + Java language support for Apache Airflow task coordinators and DAG file processors. + +state: ready +lifecycle: incubation +source-date-epoch: 1775631151 +# Note that those versions are maintained by release manager - do not update them manually +# with the exception of case where other provider in sources has >= new provider version. +# In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have +# to be done in the same PR +versions: + - 0.1.0 + +integrations: + - integration-name: Java + external-doc-url: https://openjdk.org/ + tags: + - language + - java + +task-coordinators: + - airflow.providers.languages.java.task_coordinators.JavaTaskCoordinator + +dag-file-processors: + - airflow.providers.languages.java.dag_file_processors.JavaDagFileProcessor diff --git a/providers/languages/java/pyproject.toml b/providers/languages/java/pyproject.toml new file mode 100644 index 0000000000000..f5a62794a077d --- /dev/null +++ b/providers/languages/java/pyproject.toml @@ -0,0 +1,111 @@ +# 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. + +# NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN! + +# IF YOU WANT TO MODIFY THIS FILE EXCEPT DEPENDENCIES, YOU SHOULD MODIFY THE TEMPLATE +# `pyproject_TEMPLATE.toml.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY +[build-system] +requires = ["flit_core==3.12.0"] +build-backend = "flit_core.buildapi" + +[project] +name = "apache-airflow-providers-languages-java" +version = "0.1.0" +description = "Provider package apache-airflow-providers-languages-java for Apache Airflow" +readme = "README.rst" +license = "Apache-2.0" +license-files = ['LICENSE', 'NOTICE'] +authors = [ + {name="Apache Software Foundation", email="dev@airflow.apache.org"}, +] +maintainers = [ + {name="Apache Software Foundation", email="dev@airflow.apache.org"}, +] +keywords = [ "airflow-provider", "languages.java", "airflow", "integration" ] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Framework :: Apache Airflow", + "Framework :: Apache Airflow :: Provider", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: System :: Monitoring", +] +requires-python = ">=3.10" + +# The dependencies should be modified in place in the generated file. +# Any change in the dependencies is preserved when the file is regenerated +# Make sure to run ``prek update-providers-dependencies --all-files`` +# After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build`` +dependencies = [ + "apache-airflow>=3.0.0", +] + +[dependency-groups] +dev = [ + "apache-airflow", + "apache-airflow-task-sdk", + "apache-airflow-devel-common", + # Additional devel dependencies (do not remove this line and add extra development dependencies) +] + +# To build docs: +# +# uv run --group docs build-docs +# +# To enable auto-refreshing build with server: +# +# uv run --group docs build-docs --autobuild +# +# To see more options: +# +# uv run --group docs build-docs --help +# +docs = [ + "apache-airflow-devel-common[docs]" +] + +[tool.uv.sources] +# These names must match the names as defined in the pyproject.toml of the workspace items, +# *not* the workspace folder paths +apache-airflow = {workspace = true} +apache-airflow-devel-common = {workspace = true} +apache-airflow-task-sdk = {workspace = true} +apache-airflow-providers-common-sql = {workspace = true} +apache-airflow-providers-standard = {workspace = true} + +[project.urls] +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-languages-java/0.1.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-languages-java/0.1.0/changelog.html" +"Bug Tracker" = "https://github.com/apache/airflow/issues" +"Source Code" = "https://github.com/apache/airflow" +"Slack Chat" = "https://s.apache.org/airflow-slack" +"Mastodon" = "https://fosstodon.org/@airflow" +"YouTube" = "https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/" + +[project.entry-points."apache_airflow_provider"] +provider_info = "airflow.providers.languages.java.get_provider_info:get_provider_info" + +[tool.flit.module] +name = "airflow.providers.languages.java" diff --git a/providers/languages/java/src/airflow/__init__.py b/providers/languages/java/src/airflow/__init__.py new file mode 100644 index 0000000000000..5966d6b1d5261 --- /dev/null +++ b/providers/languages/java/src/airflow/__init__.py @@ -0,0 +1,17 @@ +# 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. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/providers/languages/java/src/airflow/providers/__init__.py b/providers/languages/java/src/airflow/providers/__init__.py new file mode 100644 index 0000000000000..5966d6b1d5261 --- /dev/null +++ b/providers/languages/java/src/airflow/providers/__init__.py @@ -0,0 +1,17 @@ +# 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. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/providers/languages/java/src/airflow/providers/languages/__init__.py b/providers/languages/java/src/airflow/providers/languages/__init__.py new file mode 100644 index 0000000000000..5966d6b1d5261 --- /dev/null +++ b/providers/languages/java/src/airflow/providers/languages/__init__.py @@ -0,0 +1,17 @@ +# 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. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/providers/languages/java/src/airflow/providers/languages/java/__init__.py b/providers/languages/java/src/airflow/providers/languages/java/__init__.py new file mode 100644 index 0000000000000..d90a03c004ffd --- /dev/null +++ b/providers/languages/java/src/airflow/providers/languages/java/__init__.py @@ -0,0 +1,39 @@ +# 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. +# +# NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE +# OVERWRITTEN WHEN PREPARING DOCUMENTATION FOR THE PACKAGES. +# +# IF YOU WANT TO MODIFY THIS FILE, YOU SHOULD MODIFY THE TEMPLATE +# `PROVIDER__INIT__PY_TEMPLATE.py.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY +# +from __future__ import annotations + +import packaging.version + +from airflow import __version__ as airflow_version + +__all__ = ["__version__"] + +__version__ = "0.1.0" + +if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( + "3.0.0" +): + raise RuntimeError( + f"The package `apache-airflow-providers-languages-java:{__version__}` needs Apache Airflow 3.0.0+" + ) diff --git a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py new file mode 100644 index 0000000000000..b9e72fdffe7cb --- /dev/null +++ b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py @@ -0,0 +1,25 @@ +# 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. +"""Java DAG file processor implementations.""" + +from __future__ import annotations + + +class JavaDagFileProcessor: + """Placeholder DAG file processor entry point for Java workloads.""" + + language = "java" diff --git a/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py b/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py new file mode 100644 index 0000000000000..b7772a9241715 --- /dev/null +++ b/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py @@ -0,0 +1,38 @@ +# 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. + +# NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN! +# +# IF YOU WANT TO MODIFY THIS FILE, YOU SHOULD MODIFY THE TEMPLATE +# `get_provider_info_TEMPLATE.py.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY + + +def get_provider_info(): + return { + "package-name": "apache-airflow-providers-languages-java", + "name": "Languages: Java", + "description": "Java language support for Apache Airflow task coordinators and DAG file processors.\n", + "integrations": [ + { + "integration-name": "Java", + "external-doc-url": "https://openjdk.org/", + "tags": ["language", "java"], + } + ], + "task-coordinators": ["airflow.providers.languages.java.task_coordinators.JavaTaskCoordinator"], + "dag-file-processors": ["airflow.providers.languages.java.dag_file_processors.JavaDagFileProcessor"], + } diff --git a/providers/languages/java/src/airflow/providers/languages/java/task_coordinators.py b/providers/languages/java/src/airflow/providers/languages/java/task_coordinators.py new file mode 100644 index 0000000000000..c5b0437140326 --- /dev/null +++ b/providers/languages/java/src/airflow/providers/languages/java/task_coordinators.py @@ -0,0 +1,25 @@ +# 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. +"""Java task coordinator implementations.""" + +from __future__ import annotations + + +class JavaTaskCoordinator: + """Placeholder task coordinator entry point for Java workloads.""" + + language = "java" diff --git a/providers/languages/java/tests/conftest.py b/providers/languages/java/tests/conftest.py new file mode 100644 index 0000000000000..f56ccce0a3f69 --- /dev/null +++ b/providers/languages/java/tests/conftest.py @@ -0,0 +1,19 @@ +# 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 + +pytest_plugins = "tests_common.pytest_plugin" diff --git a/providers/languages/java/tests/unit/__init__.py b/providers/languages/java/tests/unit/__init__.py new file mode 100644 index 0000000000000..5966d6b1d5261 --- /dev/null +++ b/providers/languages/java/tests/unit/__init__.py @@ -0,0 +1,17 @@ +# 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. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/providers/languages/java/tests/unit/languages/__init__.py b/providers/languages/java/tests/unit/languages/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/languages/java/tests/unit/languages/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/providers/languages/java/tests/unit/languages/java/__init__.py b/providers/languages/java/tests/unit/languages/java/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/languages/java/tests/unit/languages/java/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/providers/languages/java/tests/unit/languages/java/test_java_provider.py b/providers/languages/java/tests/unit/languages/java/test_java_provider.py new file mode 100644 index 0000000000000..2403573b58cf8 --- /dev/null +++ b/providers/languages/java/tests/unit/languages/java/test_java_provider.py @@ -0,0 +1,47 @@ +# 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 airflow.providers.languages.java.dag_file_processors import JavaDagFileProcessor +from airflow.providers.languages.java.get_provider_info import get_provider_info +from airflow.providers.languages.java.task_coordinators import JavaTaskCoordinator + + +def test_get_provider_info_exposes_java_runtime_components(): + assert get_provider_info() == { + "package-name": "apache-airflow-providers-languages-java", + "name": "Languages: Java", + "description": "Java language support for Apache Airflow task coordinators and DAG file processors.\n", + "integrations": [ + { + "integration-name": "Java", + "external-doc-url": "https://openjdk.org/", + "tags": ["language", "java"], + } + ], + "task-coordinators": [ + "airflow.providers.languages.java.task_coordinators.JavaTaskCoordinator", + ], + "dag-file-processors": [ + "airflow.providers.languages.java.dag_file_processors.JavaDagFileProcessor", + ], + } + + +def test_java_provider_entrypoints_are_importable(): + assert JavaTaskCoordinator.language == "java" + assert JavaDagFileProcessor.language == "java" diff --git a/pyproject.toml b/pyproject.toml index a0202dca38377..5508763a5159c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -266,6 +266,9 @@ apache-airflow = "airflow.__main__:main" "keycloak" = [ "apache-airflow-providers-keycloak>=0.0.1" ] +"languages.java" = [ + "apache-airflow-providers-languages-java>=0.1.0" # Set from local provider pyproject.toml +] "microsoft.azure" = [ "apache-airflow-providers-microsoft-azure>=10.5.1" ] @@ -457,6 +460,7 @@ apache-airflow = "airflow.__main__:main" "apache-airflow-providers-jdbc>=4.5.2", "apache-airflow-providers-jenkins>=3.7.2", "apache-airflow-providers-keycloak>=0.0.1", + "apache-airflow-providers-languages-java>=0.1.0", # Set from local provider pyproject.toml "apache-airflow-providers-microsoft-azure>=10.5.1", "apache-airflow-providers-microsoft-mssql>=3.9.2", "apache-airflow-providers-microsoft-psrp>=3.0.0", @@ -1186,6 +1190,8 @@ mypy_path = [ "$MYPY_CONFIG_FILE_DIR/providers/jenkins/tests", "$MYPY_CONFIG_FILE_DIR/providers/keycloak/src", "$MYPY_CONFIG_FILE_DIR/providers/keycloak/tests", + "$MYPY_CONFIG_FILE_DIR/providers/languages/java/src", + "$MYPY_CONFIG_FILE_DIR/providers/languages/java/tests", "$MYPY_CONFIG_FILE_DIR/providers/microsoft/azure/src", "$MYPY_CONFIG_FILE_DIR/providers/microsoft/azure/tests", "$MYPY_CONFIG_FILE_DIR/providers/microsoft/mssql/src", @@ -1466,6 +1472,7 @@ apache-airflow-providers-informatica = false apache-airflow-providers-jdbc = false apache-airflow-providers-jenkins = false apache-airflow-providers-keycloak = false +apache-airflow-providers-languages-java = false apache-airflow-providers-microsoft-azure = false apache-airflow-providers-microsoft-mssql = false apache-airflow-providers-microsoft-psrp = false @@ -1616,6 +1623,7 @@ apache-airflow-providers-informatica = false apache-airflow-providers-jdbc = false apache-airflow-providers-jenkins = false apache-airflow-providers-keycloak = false +apache-airflow-providers-languages-java = false apache-airflow-providers-microsoft-azure = false apache-airflow-providers-microsoft-mssql = false apache-airflow-providers-microsoft-psrp = false @@ -1777,6 +1785,7 @@ apache-airflow-providers-informatica = { workspace = true } apache-airflow-providers-jdbc = { workspace = true } apache-airflow-providers-jenkins = { workspace = true } apache-airflow-providers-keycloak = { workspace = true } +apache-airflow-providers-languages-java = { workspace = true } apache-airflow-providers-microsoft-azure = { workspace = true } apache-airflow-providers-microsoft-mssql = { workspace = true } apache-airflow-providers-microsoft-psrp = { workspace = true } @@ -1914,6 +1923,7 @@ members = [ "providers/jdbc", "providers/jenkins", "providers/keycloak", + "providers/languages/java", "providers/microsoft/azure", "providers/microsoft/mssql", "providers/microsoft/psrp", diff --git a/scripts/ci/docker-compose/remove-sources.yml b/scripts/ci/docker-compose/remove-sources.yml index a2f7d3a035766..aac26d76bcf24 100644 --- a/scripts/ci/docker-compose/remove-sources.yml +++ b/scripts/ci/docker-compose/remove-sources.yml @@ -83,6 +83,7 @@ services: - ../../../empty:/opt/airflow/providers/jdbc/src - ../../../empty:/opt/airflow/providers/jenkins/src - ../../../empty:/opt/airflow/providers/keycloak/src + - ../../../empty:/opt/airflow/providers/languages/java/src - ../../../empty:/opt/airflow/providers/microsoft/azure/src - ../../../empty:/opt/airflow/providers/microsoft/mssql/src - ../../../empty:/opt/airflow/providers/microsoft/psrp/src diff --git a/scripts/ci/docker-compose/tests-sources.yml b/scripts/ci/docker-compose/tests-sources.yml index 9c02d1c271412..a18a6539ee536 100644 --- a/scripts/ci/docker-compose/tests-sources.yml +++ b/scripts/ci/docker-compose/tests-sources.yml @@ -96,6 +96,7 @@ services: - ../../../providers/jdbc/tests:/opt/airflow/providers/jdbc/tests - ../../../providers/jenkins/tests:/opt/airflow/providers/jenkins/tests - ../../../providers/keycloak/tests:/opt/airflow/providers/keycloak/tests + - ../../../providers/languages/java/tests:/opt/airflow/providers/languages/java/tests - ../../../providers/microsoft/azure/tests:/opt/airflow/providers/microsoft/azure/tests - ../../../providers/microsoft/mssql/tests:/opt/airflow/providers/microsoft/mssql/tests - ../../../providers/microsoft/psrp/tests:/opt/airflow/providers/microsoft/psrp/tests diff --git a/task-sdk/src/airflow/sdk/execution_time/task_coordinator.py b/task-sdk/src/airflow/sdk/execution_time/task_coordinator.py new file mode 100644 index 0000000000000..d418b2b384b79 --- /dev/null +++ b/task-sdk/src/airflow/sdk/execution_time/task_coordinator.py @@ -0,0 +1,20 @@ +# +# 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. +"""Task Coordinator for Airflow Task Execution Time SDK.""" + +from __future__ import annotations diff --git a/task-sdk/tests/task_sdk/test_providers_manager_runtime.py b/task-sdk/tests/task_sdk/test_providers_manager_runtime.py index 1cae21d53c764..d7cdf0abe068c 100644 --- a/task-sdk/tests/task_sdk/test_providers_manager_runtime.py +++ b/task-sdk/tests/task_sdk/test_providers_manager_runtime.py @@ -243,6 +243,27 @@ def test_already_initialized_provider_configs_emits_deprecation_warning(self): with pytest.warns(DeprecationWarning, match="already_initialized_provider_configs.*deprecated"): pm.already_initialized_provider_configs + @patch("airflow.sdk.providers_manager_runtime.import_string") + def test_task_coordinators(self, mock_import_string): + mock_import_string.return_value = object() + providers_manager = ProvidersManagerTaskRuntime() + providers_manager._provider_dict["apache-airflow-providers-languages-java"] = ProviderInfo( + version="0.0.1", + data={ + "task-coordinators": [ + "airflow.providers.languages.java.task_coordinators.ZCoordinator", + "airflow.providers.languages.java.task_coordinators.ACoordinator", + "airflow.providers.languages.java.task_coordinators.ZCoordinator", + ] + }, + ) + + with patch.object(providers_manager, "initialize_providers_list"): + assert providers_manager.task_coordinators == [ + "airflow.providers.languages.java.task_coordinators.ACoordinator", + "airflow.providers.languages.java.task_coordinators.ZCoordinator", + ] + def test_initialize_provider_configs_can_reload_sdk_conf(self): from airflow.sdk.configuration import conf diff --git a/uv.lock b/uv.lock index bca09add09083..ca383e6f4e983 100644 --- a/uv.lock +++ b/uv.lock @@ -129,6 +129,7 @@ apache-airflow-providers-keycloak = false apache-airflow-providers-trino = false apache-airflow-providers-common-messaging = false apache-airflow-providers-standard = false +apache-airflow-providers-languages-java = false apache-airflow-providers-singularity = false apache-airflow-providers-common-compat = false apache-airflow-ctl-tests = false @@ -225,6 +226,7 @@ members = [ "apache-airflow-providers-jdbc", "apache-airflow-providers-jenkins", "apache-airflow-providers-keycloak", + "apache-airflow-providers-languages-java", "apache-airflow-providers-microsoft-azure", "apache-airflow-providers-microsoft-mssql", "apache-airflow-providers-microsoft-psrp", @@ -1040,6 +1042,7 @@ all = [ { name = "apache-airflow-providers-jdbc" }, { name = "apache-airflow-providers-jenkins" }, { name = "apache-airflow-providers-keycloak" }, + { name = "apache-airflow-providers-languages-java" }, { name = "apache-airflow-providers-microsoft-azure" }, { name = "apache-airflow-providers-microsoft-mssql" }, { name = "apache-airflow-providers-microsoft-psrp" }, @@ -1290,6 +1293,9 @@ kerberos = [ keycloak = [ { name = "apache-airflow-providers-keycloak" }, ] +languages-java = [ + { name = "apache-airflow-providers-languages-java" }, +] ldap = [ { name = "python-ldap" }, ] @@ -1655,6 +1661,8 @@ requires-dist = [ { name = "apache-airflow-providers-jenkins", marker = "extra == 'jenkins'", editable = "providers/jenkins" }, { name = "apache-airflow-providers-keycloak", marker = "extra == 'all'", editable = "providers/keycloak" }, { name = "apache-airflow-providers-keycloak", marker = "extra == 'keycloak'", editable = "providers/keycloak" }, + { name = "apache-airflow-providers-languages-java", marker = "extra == 'all'", editable = "providers/languages/java" }, + { name = "apache-airflow-providers-languages-java", marker = "extra == 'languages-java'", editable = "providers/languages/java" }, { name = "apache-airflow-providers-microsoft-azure", marker = "extra == 'all'", editable = "providers/microsoft/azure" }, { name = "apache-airflow-providers-microsoft-azure", marker = "extra == 'microsoft-azure'", editable = "providers/microsoft/azure" }, { name = "apache-airflow-providers-microsoft-mssql", marker = "extra == 'all'", editable = "providers/microsoft/mssql" }, @@ -5969,6 +5977,35 @@ dev = [ ] docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "devel-common" }] +[[package]] +name = "apache-airflow-providers-languages-java" +version = "0.1.0" +source = { editable = "providers/languages/java" } +dependencies = [ + { name = "apache-airflow" }, +] + +[package.dev-dependencies] +dev = [ + { name = "apache-airflow" }, + { name = "apache-airflow-devel-common" }, + { name = "apache-airflow-task-sdk" }, +] +docs = [ + { name = "apache-airflow-devel-common", extra = ["docs"] }, +] + +[package.metadata] +requires-dist = [{ name = "apache-airflow", editable = "." }] + +[package.metadata.requires-dev] +dev = [ + { name = "apache-airflow", editable = "." }, + { name = "apache-airflow-devel-common", editable = "devel-common" }, + { name = "apache-airflow-task-sdk", editable = "task-sdk" }, +] +docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "devel-common" }] + [[package]] name = "apache-airflow-providers-microsoft-azure" version = "13.3.0" From babc8f19f4c218c9e7f419faf3584bf2bdc548b7 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 10 Apr 2026 14:33:41 +0800 Subject: [PATCH 03/75] Add common selector loop utilities for socket I/O handling for subprocesses --- .../sdk/execution_time/selector_loop.py | 159 ++++++++++++++++++ .../airflow/sdk/execution_time/supervisor.py | 79 +-------- 2 files changed, 164 insertions(+), 74 deletions(-) create mode 100644 task-sdk/src/airflow/sdk/execution_time/selector_loop.py diff --git a/task-sdk/src/airflow/sdk/execution_time/selector_loop.py b/task-sdk/src/airflow/sdk/execution_time/selector_loop.py new file mode 100644 index 0000000000000..d67014ad1b418 --- /dev/null +++ b/task-sdk/src/airflow/sdk/execution_time/selector_loop.py @@ -0,0 +1,159 @@ +# +# 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. +""" +Selector-based I/O loop utilities shared across subprocess monitors. + +Both :class:`~airflow.sdk.execution_time.supervisor.WatchedSubprocess` +(supervisor-side) and provider-registered bridges such as the Locale DagFileProcessor (child-side) use these building blocks to multiplex +socket I/O without threads. + +The common contract for every callback registered with the selector: + +* The selector stores a ``(handler, on_close)`` tuple as ``key.data``. +* ``handler(fileobj) -> bool`` — read available data and return + ``True`` to keep listening, ``False`` on EOF / error. +* ``on_close(fileobj)`` — called when the handler returns ``False``; + must unregister the fileobj from the selector. +* :func:`service_selector` drives one iteration of this protocol. +""" + +from __future__ import annotations + +import selectors +from contextlib import suppress +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable, Generator + from socket import socket + + # (handler, on_close) — stored as ``selector.register(..., data=cb)`` + SelectorCallback = tuple[Callable[[socket], bool], Callable[[socket], None]] + + +# Sockets, even the `.makefile()` function don't correctly do line buffering on reading. If a chunk is read +# and it doesn't contain a new line character, `.readline()` will just return the chunk as is. +# +# This returns a callback suitable for attaching to a `selector` that reads in to a buffer, and yields lines +# to a (sync) generator +def make_buffered_socket_reader( + gen: Generator[None, bytes | bytearray, None], + on_close: Callable[[socket], None], + buffer_size: int = 4096, +) -> SelectorCallback: + """ + Create a selector callback that line-buffers socket data into a generator. + + Bytes are accumulated until a newline is found; each + complete line is sent to *gen* via ``gen.send(line)``. On EOF the + remainder of the buffer (if any) is flushed. + + Returns a ``(handler, on_close)`` tuple suitable for + ``selector.register(..., data=...)``. + """ + buffer = bytearray() # This will hold our accumulated binary data + read_buffer = bytearray(buffer_size) # Temporary buffer for each read + + # We need to start up the generator to get it to the point it's at waiting on the yield + next(gen) + + def cb(sock: socket): + nonlocal buffer, read_buffer + # Read up to `buffer_size` bytes of data from the socket + n_received = sock.recv_into(read_buffer) + + if not n_received: + # If no data is returned, the connection is closed. Return whatever is left in the buffer + if len(buffer): + with suppress(StopIteration): + gen.send(buffer) + return False + + buffer.extend(read_buffer[:n_received]) + + # We could have read multiple lines in one go, yield them all + while (newline_pos := buffer.find(b"\n")) != -1: + line = buffer[: newline_pos + 1] + try: + gen.send(line) + except StopIteration: + return False + buffer = buffer[newline_pos + 1 :] # Update the buffer with remaining data + + return True + + return cb, on_close + + +def make_raw_forwarder( + dest: socket, + on_close: Callable[[socket], None], +) -> SelectorCallback: + """ + Create a selector callback that forwards raw bytes to *dest*. + + Used for transparent protocol bridges where bytes must be shuttled + between two sockets without interpretation (e.g. length-prefixed + msgpack frames between a supervisor and a Java subprocess). + """ + + def cb(sock: socket) -> bool: + data = sock.recv(65536) + if not data: + return False + try: + dest.sendall(data) + except (BrokenPipeError, ConnectionResetError, OSError): + return False + return True + + return cb, on_close + + +def service_selector(selector: selectors.BaseSelector, timeout: float = 1.0) -> None: + """ + Process one round of selector events. + + For each ready socket whose handler returns ``False`` (EOF / error), + the socket's *on_close* callback is invoked and the socket is closed. + """ + # Ensure minimum timeout to prevent CPU spike with tight loop when timeout is 0 or negative + timeout = max(0.01, timeout) + events = selector.select(timeout=timeout) + for key, _ in events: + # Retrieve the handler responsible for processing this file object (e.g., stdout, stderr) + socket_handler, on_close = key.data + + # Example of handler behavior: + # If the subprocess writes "Hello, World!" to stdout: + # - `socket_handler` reads and processes the message. + # - If EOF is reached, the handler returns False to signal no more reads are expected. + # - BrokenPipeError should be caught and treated as if the handler returned false, similar + # to EOF case + try: + need_more = socket_handler(key.fileobj) + except (BrokenPipeError, ConnectionResetError): + need_more = False + + # If the handler signals that the file object is no longer needed (EOF, closed, etc.) + # unregister it from the selector to stop monitoring; `wait()` blocks until all selectors + # are removed. + if not need_more: + sock: socket = key.fileobj # type: ignore[assignment] + on_close(sock) + sock.close() diff --git a/task-sdk/src/airflow/sdk/execution_time/supervisor.py b/task-sdk/src/airflow/sdk/execution_time/supervisor.py index ac479dcebe69d..30bcad10bce00 100644 --- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py +++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py @@ -170,6 +170,7 @@ from airflow.executors.workloads import BundleInfo from airflow.sdk.bases.secrets_backend import BaseSecretsBackend from airflow.sdk.definitions.connection import Connection + from airflow.sdk.execution_time.selector_loop import SelectorCallback from airflow.sdk.types import RuntimeTaskInstanceProtocol as RuntimeTI __all__ = ["ActivitySubprocess", "WatchedSubprocess", "supervise", "supervise_task"] @@ -740,7 +741,7 @@ def _get_target_loggers(self) -> tuple[FilteringBoundLogger, ...]: target_loggers += (log,) return target_loggers - def _create_log_forwarder(self, loggers, name, log_level=logging.INFO) -> Callable[[socket], bool]: + def _create_log_forwarder(self, loggers, name, log_level=logging.INFO) -> SelectorCallback: """Create a socket handler that forwards logs to a logger.""" loggers = tuple( reconfigure_logger( @@ -972,41 +973,15 @@ def _service_subprocess( """ Service subprocess events by processing socket activity and checking for process exit. - This method: - - Waits for activity on the registered file objects (via `self.selector.select`). - - Processes any events triggered on these file objects. - - Checks if the subprocess has exited during the wait. + Delegates the selector event loop to :func:`service_selector` (shared + with provider-registered bridges), then checks the subprocess status. :param max_wait_time: Maximum time to block while waiting for events, in seconds. :param raise_on_timeout: If True, raise an exception if the subprocess does not exit within the timeout. :param expect_signal: Signal not to log if the task exits with this code. :returns: The process exit code, or None if it's still alive """ - # Ensure minimum timeout to prevent CPU spike with tight loop when timeout is 0 or negative - timeout = max(0.01, max_wait_time) - events = self.selector.select(timeout=timeout) - for key, _ in events: - # Retrieve the handler responsible for processing this file object (e.g., stdout, stderr) - socket_handler, on_close = key.data - - # Example of handler behavior: - # If the subprocess writes "Hello, World!" to stdout: - # - `socket_handler` reads and processes the message. - # - If EOF is reached, the handler returns False to signal no more reads are expected. - # - BrokenPipeError should be caught and treated as if the handler returned false, similar - # to EOF case - try: - need_more = socket_handler(key.fileobj) - except (BrokenPipeError, ConnectionResetError): - need_more = False - - # If the handler signals that the file object is no longer needed (EOF, closed, etc.) - # unregister it from the selector to stop monitoring; `wait()` blocks until all selectors - # are removed. - if not need_more: - sock: socket = key.fileobj # type: ignore[assignment] - on_close(sock) - sock.close() + service_selector(self.selector, timeout=max_wait_time) # Check if the subprocess has exited return self._check_subprocess_exit(raise_on_timeout=raise_on_timeout, expect_signal=expect_signal) @@ -2067,50 +2042,6 @@ def run_task_in_process(ti: TaskInstance, task) -> TaskRunResult: return InProcessTestSupervisor.start(what=ti, task=task) -# Sockets, even the `.makefile()` function don't correctly do line buffering on reading. If a chunk is read -# and it doesn't contain a new line character, `.readline()` will just return the chunk as is. -# -# This returns a callback suitable for attaching to a `selector` that reads in to a buffer, and yields lines -# to a (sync) generator -def make_buffered_socket_reader( - gen: Generator[None, bytes | bytearray, None], - on_close: Callable[[socket], None], - buffer_size: int = 4096, -): - buffer = bytearray() # This will hold our accumulated binary data - read_buffer = bytearray(buffer_size) # Temporary buffer for each read - - # We need to start up the generator to get it to the point it's at waiting on the yield - next(gen) - - def cb(sock: socket): - nonlocal buffer, read_buffer - # Read up to `buffer_size` bytes of data from the socket - n_received = sock.recv_into(read_buffer) - - if not n_received: - # If no data is returned, the connection is closed. Return whatever is left in the buffer - if len(buffer): - with suppress(StopIteration): - gen.send(buffer) - return False - - buffer.extend(read_buffer[:n_received]) - - # We could have read multiple lines in one go, yield them all - while (newline_pos := buffer.find(b"\n")) != -1: - line = buffer[: newline_pos + 1] - try: - gen.send(line) - except StopIteration: - return False - buffer = buffer[newline_pos + 1 :] # Update the buffer with remaining data - - return True - - return cb, on_close - - def length_prefixed_frame_reader( gen: Generator[None, _RequestFrame, None], on_close: Callable[[socket], None] ): From 851c99b66a2c906e89937a948efa002ac49fe31f Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 9 Apr 2026 16:12:37 +0800 Subject: [PATCH 04/75] Implement Java DAG file processor with TCP communication bridge --- .../src/airflow/dag_processing/processor.py | 112 +++++++++- .../languages/java/dag_file_processors.py | 191 +++++++++++++++++- 2 files changed, 298 insertions(+), 5 deletions(-) diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index 303b62d141111..5dff4eb58aeff 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -17,6 +17,7 @@ from __future__ import annotations import contextlib +import functools import importlib import logging import os @@ -180,6 +181,51 @@ class DagFileParsingResult(BaseModel): ] +class BaseDagFileProcessor: + """ + Base class for provider-contributed DAG file processors. + + Providers register subclasses in their ``provider.yaml`` under ``dag-file-processors``. + When :class:`DagFileProcessorProcess` starts, it checks all registered processors + via :meth:`can_handle`; the first match's :meth:`entrypoint` is used as the subprocess + target instead of the default ``_parse_file_entrypoint``. + + The ``entrypoint`` runs inside a forked child process. After the fork, + fd 0 is a bidirectional socket connected to the supervisor (the same channel + that ``_parse_file_entrypoint`` uses via :class:`CommsDecoder`). The entrypoint + can bridge this socket to an external process (e.g. a Java subprocess over TCP) + without needing ``CommsDecoder`` at all — just forward raw bytes between fd 0 + and the external process's socket. + + The supervisor will send a :class:`DagFileParseRequest` on fd 0 after the fork + and expects a :class:`DagFileParsingResult` back on the same channel. + """ + + def __init__( + self, + *, + target_bundle_name: str, + ): + # We will only store dag_bundle_name but not dag_bundle_path here because it's DagBundle's responsibility to manage the path + self.target_bundle_name = target_bundle_name + + def can_handle(self, bundle_name: str) -> bool: + """Return ``True`` if this processor should handle the given file.""" + # The Airflow Core DagFileProcessorProcess will pass the bundle_name to see + return self.target_bundle_name == bundle_name + + @staticmethod + def entrypoint(path: str, bundle_name: str, bundle_path: str) -> None: + """ + Entry point called in the forked child process. + + :param path: Absolute path to the file to process. + :param bundle_name: Name of the DAG bundle. + :param bundle_path: Root path of the DAG bundle. + """ + raise NotImplementedError + + def _pre_import_airflow_modules(file_path: str, log: FilteringBoundLogger) -> None: """ Pre-import Airflow modules found in the given file. @@ -572,7 +618,14 @@ def start( # type: ignore[override] ) -> Self: logger = kwargs["logger"] - _pre_import_airflow_modules(os.fspath(path), logger) + # Check if a provider-registered dag file processor should handle this file + logger.debug("Checking for provider-registered DAG file processor entrypoint for file", path=path) + resolved_target = cls._resolve_processor_target(path, bundle_name, bundle_path, logger) + if resolved_target is not None: + target = resolved_target + logger.debug("Resolved provider-registered DAG file processor entrypoint for file", path=path) + else: + _pre_import_airflow_modules(os.fspath(path), logger) proc: Self = super().start( target=target, @@ -585,6 +638,63 @@ def start( # type: ignore[override] proc._on_child_started(callbacks, path, bundle_path, bundle_name) return proc + @staticmethod + def _resolve_processor_target( + path: str | os.PathLike[str], + bundle_name: str, + bundle_path: Path, + log: FilteringBoundLogger, + ) -> Callable[[], None] | None: + """ + Return the entrypoint of the first provider dag file processor that can handle *path*. + + The returned callable is a ``functools.partial`` that binds *path*, *bundle_name* + and *bundle_path* so the supervisor can pass it as a no-arg ``target`` to + ``WatchedSubprocess.start``. + """ + from airflow._shared.module_loading import import_string + from airflow.providers_manager import ProvidersManager + + for processor_class_path in ProvidersManager().dag_file_processors: + try: + log.debug( + "Checking provider-registered DAG file processor %s for file %s", + processor_class_path, + path, + ) + processor_cls = import_string(processor_class_path) + processor_instance: BaseDagFileProcessor = processor_cls(target_bundle_name=bundle_name) + log.debug( + "Instantiated provider-registered DAG file processor %s for file %s", + processor_class_path, + path, + ) + if processor_instance.can_handle(bundle_name): + log.debug( + "Using provider-registered DAG file processor %s for file %s", + processor_class_path, + path, + ) + return functools.partial( + processor_instance.entrypoint, + path=os.fspath(path), + bundle_name=bundle_name, + bundle_path=os.fspath(bundle_path), + ) + log.debug( + "Provider-registered DAG file processor %s cannot handle file %s with bundle name %s", + processor_class_path, + path, + bundle_name, + ) + except Exception: + log.warning("Failed to load dag file processor %s", processor_class_path, exc_info=True) + + log.debug( + "No provider-registered DAG file processor found for file %s, using default processor", path + ) + return None + def _on_child_started( self, callbacks: list[CallbackRequest], diff --git a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py index b9e72fdffe7cb..87ef120c8b565 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py +++ b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py @@ -14,12 +14,195 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -"""Java DAG file processor implementations.""" +""" +Java DAG file processor implementations. + +The entrypoint runs inside a forked child process where fd 0 is a +bidirectional socket to the supervisor (set up by ``_fork_main``). +Instead of decoding messages with ``CommsDecoder``, we spawn a Java +subprocess, let it connect back over TCP, and bridge raw bytes between +fd 0 and the Java socket. The supervisor's existing ``_handle_request`` +handles the protocol on its side — the bridge is transparent. +""" from __future__ import annotations +import contextlib +import email +import os +import pathlib +import socket +import subprocess +import threading +import zipfile + +from airflow.dag_processing.processor import BaseDagFileProcessor + + +def _start_server() -> socket.socket: + """Create a TCP server socket bound to a random port on localhost.""" + server = socket.socket() + server.bind(("127.0.0.1", 0)) + server.setblocking(True) + server.listen(1) + return server + + +def _calculate_classpath(app_home: pathlib.Path) -> str: + """Build a classpath string from all JARs in *app_home*.""" + jars = (p.as_posix() for p in app_home.iterdir() if p.suffix == ".jar") + return os.pathsep.join(jars) + + +def _find_main_class(app_home: pathlib.Path) -> str: + """Read the Main-Class attribute from the first JAR manifest found in *app_home*.""" + for p in app_home.iterdir(): + if p.suffix != ".jar": + continue + with zipfile.ZipFile(p) as zf: + with zf.open("META-INF/MANIFEST.MF") as f: + if main_class := email.message_from_binary_file(f).get("Main-Class"): + return main_class + raise FileNotFoundError(f"Cannot find main class in {app_home.resolve()}") + + +class JavaDagFileProcessor(BaseDagFileProcessor): + """ + DAG file processor for Java JAR bundle workloads. + + Registered via ``dag-file-processors`` in the Java provider's ``provider.yaml``. + When the dag processor encounters a file that belongs to a Java bundle, + this processor's :meth:`entrypoint` is used as the subprocess target instead + of the default Python ``_parse_file_entrypoint``. + """ + + @staticmethod + def entrypoint(path: str, bundle_name: str, bundle_path: str) -> None: + """Bridge fd 0 (supervisor comms) to a Java subprocess over TCP.""" + parse_jar_bundles_entrypoint(path, bundle_name, bundle_path) + + +def parse_jar_bundles_entrypoint(path: str, bundle_name: str, bundle_path: str) -> None: + """ + Spawn a Java subprocess and bridge the supervisor-to-Java communication. + + After ``_fork_main``, fd 0 is a bidirectional socket to the supervisor. + This function: + + 1. Creates TCP servers for comm and logs channels. + 2. Spawns Java via ``subprocess.Popen``, passing both TCP addresses. + 3. Accepts connections from Java on both channels. + 4. Runs a threaded bridge that transparently forwards bytes between + fd 0 (supervisor) and the Java comm socket, and forwards Java's + structured log output to the child's stderr. + + No ``CommsDecoder`` is needed — the supervisor and Java both speak + the length-prefixed msgpack protocol; we just shuttle bytes. + """ + os.environ["_AIRFLOW_PROCESS_CONTEXT"] = "client" + + import structlog + + log = structlog.get_logger(logger_name="task") + + log.info("Starting Java DAG file processor", path=path, bundle_name=bundle_name, bundle_path=bundle_path) + jar_path = pathlib.Path(path) + + # TCP servers for Java to connect to (comm + logs). + comm_server = _start_server() + logs_server = _start_server() + comm_host, comm_port = comm_server.getsockname() + logs_host, logs_port = logs_server.getsockname() + + # Spawn the Java subprocess. + proc = subprocess.Popen( + [ + "java", + "-classpath", + _calculate_classpath(jar_path), + _find_main_class(jar_path), + f"--comm={comm_host}:{comm_port}", + f"--logs={logs_host}:{logs_port}", + ], + # Java stdout/stderr are inherited from the forked child + # (fd 1 and fd 2 already go to the supervisor's log readers). + ) + + # Wait for Java to connect to both servers. + java_comm, _ = comm_server.accept() + java_logs, _ = logs_server.accept() + comm_server.close() + logs_server.close() + + # fd 0 is the bidirectional comms socket to the supervisor. + supervisor_comm = socket.socket(fileno=os.dup(0)) + + # Bridge: forward raw bytes between the supervisor and Java. + _bridge(supervisor_comm, java_comm, java_logs, proc) + + +def _pipe(src: socket.socket, dest: socket.socket) -> None: + """Forward all bytes from *src* to *dest* until EOF or error.""" + try: + while True: + data = src.recv(65536) + if not data: + break + dest.sendall(data) + except (ConnectionResetError, BrokenPipeError, OSError): + pass + + +def _forward_logs(src: socket.socket) -> None: + """Forward Java's structured log lines to stderr for the supervisor to capture.""" + try: + while True: + data = src.recv(4096) + if not data: + break + os.write(2, data) + except (ConnectionResetError, BrokenPipeError, OSError): + pass + + +def _bridge( + supervisor_comm: socket.socket, + java_comm: socket.socket, + java_logs: socket.socket, + proc: subprocess.Popen, +) -> None: + """ + Forward bytes between the supervisor and Java until the Java process exits. + + Three threads run concurrently: + - supervisor → Java comm (forwards ``DagFileParseRequest`` and intermediate responses) + - Java comm → supervisor (forwards intermediate requests and ``DagFileParsingResult``) + - Java logs → stderr (structured log lines from the Java SDK) + """ + sup_to_java = threading.Thread(target=_pipe, args=(supervisor_comm, java_comm), daemon=True) + java_to_sup = threading.Thread(target=_pipe, args=(java_comm, supervisor_comm), daemon=True) + logs_fwd = threading.Thread(target=_forward_logs, args=(java_logs,), daemon=True) + + sup_to_java.start() + java_to_sup.start() + logs_fwd.start() + + # Wait for the Java process to complete. + proc.wait() + + # java_to_sup sees EOF when Java closes its comm socket; wait for it + # to finish forwarding all remaining data (including DagFileParsingResult). + java_to_sup.join(timeout=30.0) + logs_fwd.join(timeout=5.0) + + # Unblock the sup_to_java thread — the supervisor won't send more data + # now that Java has exited. + for sock in (supervisor_comm, java_comm, java_logs): + with contextlib.suppress(OSError): + sock.shutdown(socket.SHUT_RDWR) -class JavaDagFileProcessor: - """Placeholder DAG file processor entry point for Java workloads.""" + sup_to_java.join(timeout=5.0) - language = "java" + supervisor_comm.close() + java_comm.close() + java_logs.close() From a57e3f9dce840b10dd5026e0c6c4476dbbe02352 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 9 Apr 2026 17:55:06 +0800 Subject: [PATCH 05/75] Make JavaDagFileProcessor.can_handle aware of jar file content --- .../languages/java/dag_file_processors.py | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py index 87ef120c8b565..3c184fde35fd3 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py +++ b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py @@ -48,22 +48,13 @@ def _start_server() -> socket.socket: return server -def _calculate_classpath(app_home: pathlib.Path) -> str: - """Build a classpath string from all JARs in *app_home*.""" - jars = (p.as_posix() for p in app_home.iterdir() if p.suffix == ".jar") - return os.pathsep.join(jars) - - -def _find_main_class(app_home: pathlib.Path) -> str: - """Read the Main-Class attribute from the first JAR manifest found in *app_home*.""" - for p in app_home.iterdir(): - if p.suffix != ".jar": - continue - with zipfile.ZipFile(p) as zf: - with zf.open("META-INF/MANIFEST.MF") as f: - if main_class := email.message_from_binary_file(f).get("Main-Class"): - return main_class - raise FileNotFoundError(f"Cannot find main class in {app_home.resolve()}") +def _find_main_class(jar_path: pathlib.Path) -> str: + """Read the Main-Class attribute from the JAR manifest.""" + with zipfile.ZipFile(jar_path) as zf: + with zf.open("META-INF/MANIFEST.MF") as f: + if main_class := email.message_from_binary_file(f).get("Main-Class"): + return main_class + raise FileNotFoundError(f"No Main-Class in manifest of {jar_path}") class JavaDagFileProcessor(BaseDagFileProcessor): @@ -76,6 +67,21 @@ class JavaDagFileProcessor(BaseDagFileProcessor): of the default Python ``_parse_file_entrypoint``. """ + def can_handle(self, bundle_name: str, path: str | os.PathLike[str]) -> bool: + # The parent class will only validate against the bundle name + # If the configured bundle name doesn't match, we can skip the more expensive .jar content validation + if not super().can_handle(bundle_name): + return False + + # Then the dag_importer will validate based on the .jar content + + # TODO: If we decided to leverage AIP-85 `DagImporterRegistry` + # We should reuse `dag_importer.can_handle` + + with contextlib.suppress(FileNotFoundError): + return _find_main_class(pathlib.Path(path)) is not None + return False + @staticmethod def entrypoint(path: str, bundle_name: str, bundle_path: str) -> None: """Bridge fd 0 (supervisor comms) to a Java subprocess over TCP.""" @@ -119,7 +125,7 @@ def parse_jar_bundles_entrypoint(path: str, bundle_name: str, bundle_path: str) [ "java", "-classpath", - _calculate_classpath(jar_path), + jar_path.as_posix(), _find_main_class(jar_path), f"--comm={comm_host}:{comm_port}", f"--logs={logs_host}:{logs_port}", From 64f5f3d92ba8e43e5dbbd2fd92f7154f73451a91 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 9 Apr 2026 22:32:20 +0800 Subject: [PATCH 06/75] Fix java process startup issue --- .../src/airflow/dag_processing/processor.py | 4 ++-- .../providers/languages/java/dag_file_processors.py | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index 5dff4eb58aeff..cd2cbce874f9f 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -209,7 +209,7 @@ def __init__( # We will only store dag_bundle_name but not dag_bundle_path here because it's DagBundle's responsibility to manage the path self.target_bundle_name = target_bundle_name - def can_handle(self, bundle_name: str) -> bool: + def can_handle(self, bundle_name: str, path: str | os.PathLike[str]) -> bool: """Return ``True`` if this processor should handle the given file.""" # The Airflow Core DagFileProcessorProcess will pass the bundle_name to see return self.target_bundle_name == bundle_name @@ -669,7 +669,7 @@ def _resolve_processor_target( processor_class_path, path, ) - if processor_instance.can_handle(bundle_name): + if processor_instance.can_handle(bundle_name, path): log.debug( "Using provider-registered DAG file processor %s for file %s", processor_class_path, diff --git a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py index 3c184fde35fd3..bb8970c851826 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py +++ b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py @@ -70,7 +70,7 @@ class JavaDagFileProcessor(BaseDagFileProcessor): def can_handle(self, bundle_name: str, path: str | os.PathLike[str]) -> bool: # The parent class will only validate against the bundle name # If the configured bundle name doesn't match, we can skip the more expensive .jar content validation - if not super().can_handle(bundle_name): + if not super().can_handle(bundle_name, path): return False # Then the dag_importer will validate based on the .jar content @@ -120,12 +120,21 @@ def parse_jar_bundles_entrypoint(path: str, bundle_name: str, bundle_path: str) comm_host, comm_port = comm_server.getsockname() logs_host, logs_port = logs_server.getsockname() + # Build the classpath from all JARs in the bundle directory. + # Java bundles are typically thin JARs: the main JAR (e.g. example.jar) + # only contains the bundle's own classes while its dependencies (the + # Airflow Java SDK, logging libraries, etc.) are separate JARs that live + # alongside it. Using the ``/*`` wildcard lets the JVM load every + # JAR in the directory, matching the standard Gradle/Maven distribution + # layout produced by ``copyDependencies``-style tasks. + classpath = f"{bundle_path}/*" + # Spawn the Java subprocess. proc = subprocess.Popen( [ "java", "-classpath", - jar_path.as_posix(), + classpath, _find_main_class(jar_path), f"--comm={comm_host}:{comm_port}", f"--logs={logs_host}:{logs_port}", From 67e9bb4a04329dfc44810e2c59c9bf45c98d9582 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 9 Apr 2026 23:51:19 +0800 Subject: [PATCH 07/75] Fix sockets bidning --- .../languages/java/dag_file_processors.py | 90 +++++++++++++++---- 1 file changed, 73 insertions(+), 17 deletions(-) diff --git a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py index bb8970c851826..99cf16b1f2264 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py +++ b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py @@ -29,6 +29,7 @@ import contextlib import email +import json import os import pathlib import socket @@ -99,8 +100,8 @@ def parse_jar_bundles_entrypoint(path: str, bundle_name: str, bundle_path: str) 2. Spawns Java via ``subprocess.Popen``, passing both TCP addresses. 3. Accepts connections from Java on both channels. 4. Runs a threaded bridge that transparently forwards bytes between - fd 0 (supervisor) and the Java comm socket, and forwards Java's - structured log output to the child's stderr. + fd 0 (supervisor) and the Java comm socket, and re-emits Java's + log output through structlog (routed to ``log_fd``). No ``CommsDecoder`` is needed — the supervisor and Java both speak the length-prefixed msgpack protocol; we just shuttle bytes. @@ -130,6 +131,27 @@ def parse_jar_bundles_entrypoint(path: str, bundle_name: str, bundle_path: str) classpath = f"{bundle_path}/*" # Spawn the Java subprocess. + # + # fd layout in the forked child (set up by ``_reopen_std_io_handles`` + # before this entrypoint is called): + # + # fd 0 — bidirectional comms socket to the supervisor + # (``DagFileParseRequest`` <-> ``DagFileParsingResult``, + # length-prefixed msgpack frames) + # fd 1 — stdout socket to the supervisor + # fd 2 — stderr socket to the supervisor + # fd N — structured JSON log channel (``log_fd``, configured by + # ``_configure_logs_over_json_channel`` -> structlog) + # + # We redirect stdin to ``/dev/null`` so that the Java subprocess does + # not inherit fd 0 (the comms socket). Java communicates over the TCP + # sockets passed as ``--comm`` / ``--logs``; the bridge threads shuttle + # bytes between those TCP sockets and fd 0. + # + # stderr is captured via a pipe so that Java's SLF4J output can be + # re-emitted through structlog -> ``log_fd`` with the correct log level + # (instead of landing as ERROR-level ``task.stderr`` lines on the + # supervisor's raw stderr reader). proc = subprocess.Popen( [ "java", @@ -139,8 +161,8 @@ def parse_jar_bundles_entrypoint(path: str, bundle_name: str, bundle_path: str) f"--comm={comm_host}:{comm_port}", f"--logs={logs_host}:{logs_port}", ], - # Java stdout/stderr are inherited from the forked child - # (fd 1 and fd 2 already go to the supervisor's log readers). + stdin=subprocess.DEVNULL, + stderr=subprocess.PIPE, ) # Wait for Java to connect to both servers. @@ -153,7 +175,7 @@ def parse_jar_bundles_entrypoint(path: str, bundle_name: str, bundle_path: str) supervisor_comm = socket.socket(fileno=os.dup(0)) # Bridge: forward raw bytes between the supervisor and Java. - _bridge(supervisor_comm, java_comm, java_logs, proc) + _bridge(supervisor_comm, java_comm, java_logs, proc, log) def _pipe(src: socket.socket, dest: socket.socket) -> None: @@ -168,14 +190,40 @@ def _pipe(src: socket.socket, dest: socket.socket) -> None: pass -def _forward_logs(src: socket.socket) -> None: - """Forward Java's structured log lines to stderr for the supervisor to capture.""" +_JAVA_LEVEL_MAP = {"warn": "warning", "trace": "debug"} + + +def _forward_java_output(source, log) -> None: + """ + Parse JSON log lines from Java and re-emit through structlog. + + Routes Java's log output through the structured log channel + (``log_fd``, already configured by ``_configure_logs_over_json_channel``) + so the supervisor receives correct log levels instead of raw + ERROR-level ``task.stderr`` lines. + + *source* is any line-iterable (``proc.stderr`` pipe or + ``socket.makefile("rb")``). + """ try: - while True: - data = src.recv(4096) - if not data: - break - os.write(2, data) + for raw_line in source: + line = ( + raw_line.decode("utf-8", errors="replace").rstrip() + if isinstance(raw_line, bytes) + else raw_line.rstrip() + ) + if not line: + continue + try: + msg = json.loads(line) + level = msg.pop("level", "info") + event = msg.pop("event", "") + msg.pop("timestamp", None) + level_name = _JAVA_LEVEL_MAP.get(level, level) + log_fn = getattr(log, level_name, log.info) + log_fn(event, **msg) + except (json.JSONDecodeError, ValueError, TypeError): + log.info(line) except (ConnectionResetError, BrokenPipeError, OSError): pass @@ -185,22 +233,29 @@ def _bridge( java_comm: socket.socket, java_logs: socket.socket, proc: subprocess.Popen, + log, ) -> None: """ Forward bytes between the supervisor and Java until the Java process exits. - Three threads run concurrently: - - supervisor → Java comm (forwards ``DagFileParseRequest`` and intermediate responses) - - Java comm → supervisor (forwards intermediate requests and ``DagFileParsingResult``) - - Java logs → stderr (structured log lines from the Java SDK) + Four threads run concurrently: + + - supervisor -> Java comm (forwards ``DagFileParseRequest`` and intermediate responses) + - Java comm -> supervisor (forwards intermediate requests and ``DagFileParsingResult``) + - Java TCP logs -> structlog (structured log lines from the Java SDK's ``LogSender``) + - Java stderr -> structlog (SLF4J output from the Java process) """ sup_to_java = threading.Thread(target=_pipe, args=(supervisor_comm, java_comm), daemon=True) java_to_sup = threading.Thread(target=_pipe, args=(java_comm, supervisor_comm), daemon=True) - logs_fwd = threading.Thread(target=_forward_logs, args=(java_logs,), daemon=True) + logs_fwd = threading.Thread( + target=_forward_java_output, args=(java_logs.makefile("rb"), log), daemon=True + ) + stderr_fwd = threading.Thread(target=_forward_java_output, args=(proc.stderr, log), daemon=True) sup_to_java.start() java_to_sup.start() logs_fwd.start() + stderr_fwd.start() # Wait for the Java process to complete. proc.wait() @@ -209,6 +264,7 @@ def _bridge( # to finish forwarding all remaining data (including DagFileParsingResult). java_to_sup.join(timeout=30.0) logs_fwd.join(timeout=5.0) + stderr_fwd.join(timeout=5.0) # Unblock the sup_to_java thread — the supervisor won't send more data # now that Java has exited. From 909ccc36893a229aaa4db218609f1373fcdbd0dd Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 10 Apr 2026 14:38:33 +0800 Subject: [PATCH 08/75] Refactor Java DAG file processor to use selector-based I/O multiplexing for improved performance and reliability --- .../languages/java/dag_file_processors.py | 208 ++++++++++-------- 1 file changed, 111 insertions(+), 97 deletions(-) diff --git a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py index 99cf16b1f2264..042aff6b12337 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py +++ b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py @@ -1,3 +1,4 @@ +# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information @@ -22,7 +23,14 @@ Instead of decoding messages with ``CommsDecoder``, we spawn a Java subprocess, let it connect back over TCP, and bridge raw bytes between fd 0 and the Java socket. The supervisor's existing ``_handle_request`` -handles the protocol on its side — the bridge is transparent. +handles the protocol on its side -- the bridge is transparent. + +I/O multiplexing uses the same selector-based loop as +:class:`~airflow.sdk.execution_time.supervisor.WatchedSubprocess`: +sockets are registered with ``(handler, on_close)`` callback tuples +produced by :func:`~airflow.sdk.execution_time.selector_loop.make_buffered_socket_reader` +and :func:`~airflow.sdk.execution_time.selector_loop.make_raw_forwarder`, +then driven by :func:`~airflow.sdk.execution_time.selector_loop.service_selector`. """ from __future__ import annotations @@ -32,13 +40,20 @@ import json import os import pathlib +import selectors import socket import subprocess -import threading +import time import zipfile +from typing import TYPE_CHECKING from airflow.dag_processing.processor import BaseDagFileProcessor +if TYPE_CHECKING: + from collections.abc import Generator + + from structlog.typing import FilteringBoundLogger + def _start_server() -> socket.socket: """Create a TCP server socket bound to a random port on localhost.""" @@ -99,11 +114,11 @@ def parse_jar_bundles_entrypoint(path: str, bundle_name: str, bundle_path: str) 1. Creates TCP servers for comm and logs channels. 2. Spawns Java via ``subprocess.Popen``, passing both TCP addresses. 3. Accepts connections from Java on both channels. - 4. Runs a threaded bridge that transparently forwards bytes between - fd 0 (supervisor) and the Java comm socket, and re-emits Java's - log output through structlog (routed to ``log_fd``). + 4. Runs a selector-based bridge that transparently forwards bytes + between fd 0 (supervisor) and the Java comm socket, and re-emits + Java's log output through structlog (routed to ``log_fd``). - No ``CommsDecoder`` is needed — the supervisor and Java both speak + No ``CommsDecoder`` is needed -- the supervisor and Java both speak the length-prefixed msgpack protocol; we just shuttle bytes. """ os.environ["_AIRFLOW_PROCESS_CONTEXT"] = "client" @@ -135,23 +150,24 @@ def parse_jar_bundles_entrypoint(path: str, bundle_name: str, bundle_path: str) # fd layout in the forked child (set up by ``_reopen_std_io_handles`` # before this entrypoint is called): # - # fd 0 — bidirectional comms socket to the supervisor - # (``DagFileParseRequest`` <-> ``DagFileParsingResult``, - # length-prefixed msgpack frames) - # fd 1 — stdout socket to the supervisor - # fd 2 — stderr socket to the supervisor - # fd N — structured JSON log channel (``log_fd``, configured by - # ``_configure_logs_over_json_channel`` -> structlog) + # fd 0 -- bidirectional comms socket to the supervisor + # (``DagFileParseRequest`` <-> ``DagFileParsingResult``, + # length-prefixed msgpack frames) + # fd 1 -- stdout socket to the supervisor + # fd 2 -- stderr socket to the supervisor + # fd N -- structured JSON log channel (``log_fd``, configured by + # ``_configure_logs_over_json_channel`` -> structlog) # # We redirect stdin to ``/dev/null`` so that the Java subprocess does # not inherit fd 0 (the comms socket). Java communicates over the TCP - # sockets passed as ``--comm`` / ``--logs``; the bridge threads shuttle - # bytes between those TCP sockets and fd 0. + # sockets passed as ``--comm`` / ``--logs``; the bridge shuttles bytes + # between those TCP sockets and fd 0. # - # stderr is captured via a pipe so that Java's SLF4J output can be - # re-emitted through structlog -> ``log_fd`` with the correct log level - # (instead of landing as ERROR-level ``task.stderr`` lines on the - # supervisor's raw stderr reader). + # stderr uses a socketpair (instead of ``subprocess.PIPE``) so it is a + # real socket compatible with ``make_buffered_socket_reader``'s + # ``recv_into``. + child_stderr, read_stderr = socket.socketpair() + proc = subprocess.Popen( [ "java", @@ -162,8 +178,9 @@ def parse_jar_bundles_entrypoint(path: str, bundle_name: str, bundle_path: str) f"--logs={logs_host}:{logs_port}", ], stdin=subprocess.DEVNULL, - stderr=subprocess.PIPE, + stderr=child_stderr.fileno(), ) + child_stderr.close() # Close the child's end in the parent. # Wait for Java to connect to both servers. java_comm, _ = comm_server.accept() @@ -174,106 +191,103 @@ def parse_jar_bundles_entrypoint(path: str, bundle_name: str, bundle_path: str) # fd 0 is the bidirectional comms socket to the supervisor. supervisor_comm = socket.socket(fileno=os.dup(0)) - # Bridge: forward raw bytes between the supervisor and Java. - _bridge(supervisor_comm, java_comm, java_logs, proc, log) - - -def _pipe(src: socket.socket, dest: socket.socket) -> None: - """Forward all bytes from *src* to *dest* until EOF or error.""" - try: - while True: - data = src.recv(65536) - if not data: - break - dest.sendall(data) - except (ConnectionResetError, BrokenPipeError, OSError): - pass + # Bridge: multiplex I/O between the supervisor and Java. + _bridge(supervisor_comm, java_comm, java_logs, read_stderr, proc, log) _JAVA_LEVEL_MAP = {"warn": "warning", "trace": "debug"} -def _forward_java_output(source, log) -> None: +def _java_log_forwarder(log: FilteringBoundLogger) -> Generator[None, bytes | bytearray, None]: """ - Parse JSON log lines from Java and re-emit through structlog. - - Routes Java's log output through the structured log channel - (``log_fd``, already configured by ``_configure_logs_over_json_channel``) - so the supervisor receives correct log levels instead of raw - ERROR-level ``task.stderr`` lines. + Receive line-buffered bytes from Java and re-emit via structlog. - *source* is any line-iterable (``proc.stderr`` pipe or - ``socket.makefile("rb")``). + Follows the same generator protocol as + :func:`~airflow.sdk.execution_time.supervisor.forward_to_log` and + :func:`~airflow.sdk.execution_time.supervisor.process_log_messages_from_subprocess`: + primed with ``next(gen)``, then fed lines via ``gen.send(line)``. """ - try: - for raw_line in source: - line = ( - raw_line.decode("utf-8", errors="replace").rstrip() - if isinstance(raw_line, bytes) - else raw_line.rstrip() - ) - if not line: - continue - try: - msg = json.loads(line) - level = msg.pop("level", "info") - event = msg.pop("event", "") - msg.pop("timestamp", None) - level_name = _JAVA_LEVEL_MAP.get(level, level) - log_fn = getattr(log, level_name, log.info) - log_fn(event, **msg) - except (json.JSONDecodeError, ValueError, TypeError): - log.info(line) - except (ConnectionResetError, BrokenPipeError, OSError): - pass + while True: + raw_line = yield + line = raw_line.decode("utf-8", errors="replace").rstrip() + if not line: + continue + try: + msg = json.loads(line) + level = msg.pop("level", "info") + event = msg.pop("event", "") + msg.pop("timestamp", None) + level_name = _JAVA_LEVEL_MAP.get(level, level) + log_fn = getattr(log, level_name, log.info) + log_fn(event, **msg) + except (json.JSONDecodeError, ValueError, TypeError): + log.info(line) def _bridge( supervisor_comm: socket.socket, java_comm: socket.socket, java_logs: socket.socket, + java_stderr: socket.socket, proc: subprocess.Popen, - log, + log: FilteringBoundLogger, ) -> None: """ - Forward bytes between the supervisor and Java until the Java process exits. + Multiplex I/O between the supervisor and Java using a selector loop. - Four threads run concurrently: + Four channels are registered with the selector: - - supervisor -> Java comm (forwards ``DagFileParseRequest`` and intermediate responses) - - Java comm -> supervisor (forwards intermediate requests and ``DagFileParsingResult``) - - Java TCP logs -> structlog (structured log lines from the Java SDK's ``LogSender``) - - Java stderr -> structlog (SLF4J output from the Java process) + - ``supervisor_comm`` -> ``java_comm`` (raw: ``DagFileParseRequest`` and + intermediate responses) + - ``java_comm`` -> ``supervisor_comm`` (raw: intermediate requests and + ``DagFileParsingResult``) + - ``java_logs`` -> structlog (line-buffered JSON from the Java SDK's + ``LogSender``) + - ``java_stderr`` -> structlog (line-buffered SLF4J output) + + The same ``(handler, on_close)`` callback contract used by + :class:`~airflow.sdk.execution_time.supervisor.WatchedSubprocess` + applies here, driven by :func:`service_selector`. """ - sup_to_java = threading.Thread(target=_pipe, args=(supervisor_comm, java_comm), daemon=True) - java_to_sup = threading.Thread(target=_pipe, args=(java_comm, supervisor_comm), daemon=True) - logs_fwd = threading.Thread( - target=_forward_java_output, args=(java_logs.makefile("rb"), log), daemon=True + from airflow.sdk.execution_time.selector_loop import ( + make_buffered_socket_reader, + make_raw_forwarder, + service_selector, ) - stderr_fwd = threading.Thread(target=_forward_java_output, args=(proc.stderr, log), daemon=True) - - sup_to_java.start() - java_to_sup.start() - logs_fwd.start() - stderr_fwd.start() - # Wait for the Java process to complete. - proc.wait() + sel = selectors.DefaultSelector() - # java_to_sup sees EOF when Java closes its comm socket; wait for it - # to finish forwarding all remaining data (including DagFileParsingResult). - java_to_sup.join(timeout=30.0) - logs_fwd.join(timeout=5.0) - stderr_fwd.join(timeout=5.0) + def on_close(sock: socket.socket) -> None: + with contextlib.suppress(KeyError): + sel.unregister(sock) - # Unblock the sup_to_java thread — the supervisor won't send more data - # now that Java has exited. - for sock in (supervisor_comm, java_comm, java_logs): - with contextlib.suppress(OSError): - sock.shutdown(socket.SHUT_RDWR) + # Comm: bidirectional raw byte forwarding. + sel.register(supervisor_comm, selectors.EVENT_READ, make_raw_forwarder(java_comm, on_close)) + sel.register(java_comm, selectors.EVENT_READ, make_raw_forwarder(supervisor_comm, on_close)) - sup_to_java.join(timeout=5.0) + # Logs: line-buffered JSON -> structlog. + sel.register( + java_logs, + selectors.EVENT_READ, + make_buffered_socket_reader(_java_log_forwarder(log), on_close), + ) + sel.register( + java_stderr, + selectors.EVENT_READ, + make_buffered_socket_reader(_java_log_forwarder(log), on_close), + ) - supervisor_comm.close() - java_comm.close() - java_logs.close() + # Event loop -- runs until Java exits and all sockets are drained. + while sel.get_map(): + service_selector(sel, timeout=1.0) + if proc.poll() is not None: + # Java has exited -- drain remaining data with a short deadline. + deadline = time.monotonic() + 5.0 + while sel.get_map() and time.monotonic() < deadline: + service_selector(sel, timeout=0.5) + break + + sel.close() + for sock in (supervisor_comm, java_comm, java_logs, java_stderr): + with contextlib.suppress(OSError): + sock.close() From bc2b4c9112a7519dc2d4f6f1c83017b63af14d52 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 10 Apr 2026 17:08:00 +0800 Subject: [PATCH 09/75] Add BaseLocaleCoordinator for non-Python DAG file processing and task execution --- .../airflow/sdk/execution_time/coordinator.py | 325 ++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 task-sdk/src/airflow/sdk/execution_time/coordinator.py diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py new file mode 100644 index 0000000000000..07da05db234c1 --- /dev/null +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -0,0 +1,325 @@ +# +# 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. +""" +Locale coordinator for non-Python DAG file processing and task execution. + +Provides :class:`BaseLocaleCoordinator`, the base class for +language-specific coordinators that bridge subprocess I/O between the +Airflow supervisor and an external-language runtime (Java, Go, Rust, etc.). + +The coordinator's :meth:`~BaseLocaleCoordinator.run_dag_parsing` method +handles the full lifecycle: + +1. Creates TCP servers for comm and logs channels. +2. Calls :meth:`~BaseLocaleCoordinator.dag_parsing_locale_cmd` (provided + by the subclass) to obtain the subprocess command. +3. Spawns the subprocess and accepts TCP connections from it. +4. Runs a selector-based bridge that transparently forwards bytes + between fd 0 (supervisor) and the subprocess comm socket, and + re-emits the subprocess's log output through structlog. + +I/O multiplexing uses the same selector-based loop as +:class:`~airflow.sdk.execution_time.supervisor.WatchedSubprocess`, +driven by :func:`~airflow.sdk.execution_time.selector_loop.service_selector`. +""" + +from __future__ import annotations + +import contextlib +import os +import selectors +import socket +import subprocess +import time +from typing import TYPE_CHECKING, NamedTuple + +if TYPE_CHECKING: + from structlog.typing import FilteringBoundLogger + + from airflow.sdk.api.datamodels._generated import BundleInfo, TaskInstance + + +def _start_server() -> socket.socket: + """Create a TCP server socket bound to a random port on localhost.""" + server = socket.socket() + server.bind(("127.0.0.1", 0)) + server.setblocking(True) + server.listen(1) + return server + + +def _bridge( + supervisor_comm: socket.socket, + locale_comm: socket.socket, + locale_logs: socket.socket, + locale_stderr: socket.socket, + proc: subprocess.Popen, + log: FilteringBoundLogger, +) -> None: + """ + Multiplex I/O between the supervisor and a locale subprocess. + + Four channels are registered with the selector: + + - ``supervisor_comm`` -> ``locale_comm`` (raw byte forwarding) + - ``locale_comm`` -> ``supervisor_comm`` (raw byte forwarding) + - ``locale_logs`` -> structlog (line-buffered JSON logs) + - ``locale_stderr`` -> structlog (line-buffered stderr output) + + Uses the same ``(handler, on_close)`` callback contract as + :class:`~airflow.sdk.execution_time.supervisor.WatchedSubprocess`, + driven by :func:`~airflow.sdk.execution_time.selector_loop.service_selector`. + """ + from airflow.sdk.execution_time.selector_loop import ( + make_buffered_socket_reader, + make_raw_forwarder, + service_selector, + ) + from airflow.sdk.execution_time.supervisor import process_log_messages_from_subprocess + + sel = selectors.DefaultSelector() + + def on_close(sock: socket.socket) -> None: + with contextlib.suppress(KeyError): + sel.unregister(sock) + + target_loggers = (log,) + + # Comm: bidirectional raw byte forwarding. + sel.register(supervisor_comm, selectors.EVENT_READ, make_raw_forwarder(locale_comm, on_close)) + sel.register(locale_comm, selectors.EVENT_READ, make_raw_forwarder(supervisor_comm, on_close)) + + # Logs: line-buffered JSON -> structlog, using the same log processor + # as WatchedSubprocess (handles level mapping, timestamp parsing, and + # exception extraction). + sel.register( + locale_logs, + selectors.EVENT_READ, + make_buffered_socket_reader(process_log_messages_from_subprocess(target_loggers), on_close), + ) + sel.register( + locale_stderr, + selectors.EVENT_READ, + make_buffered_socket_reader(process_log_messages_from_subprocess(target_loggers), on_close), + ) + + # Event loop -- runs until the subprocess exits and all sockets are drained. + while sel.get_map(): + service_selector(sel, timeout=1.0) + if proc.poll() is not None: + # Subprocess has exited -- drain remaining data with a short deadline. + deadline = time.monotonic() + 5.0 + while sel.get_map() and time.monotonic() < deadline: + service_selector(sel, timeout=0.5) + break + + sel.close() + for sock in (supervisor_comm, locale_comm, locale_logs, locale_stderr): + with contextlib.suppress(OSError): + sock.close() + + +class BaseLocaleCoordinator: + """ + Base coordinator for locale-specific DAG file processing and task execution. + + Subclasses represent a specific language runtime (Java, Go, etc.) and + only need to implement :meth:`dag_parsing_locale_cmd` and + :meth:`task_execution_locale_cmd` to return the subprocess command. + The base class owns the entire bridge lifecycle: TCP servers, + subprocess management, selector-based I/O loop, and cleanup. + """ + + locale_name: str + + class DagParsingInfo(NamedTuple): + """Information needed for locale Dag parsing.""" + + dag_file_path: str + bundle_name: str + bundle_path: str + mode: str = "dag-parsing" + + class TaskExecutionInfo(NamedTuple): + """Information needed for locale task execution.""" + + what: TaskInstance + dag_rel_path: str | os.PathLike[str] + bundle_info: BundleInfo + mode: str = "task-execution" + + @classmethod + def dag_parsing_locale_cmd( + cls, + *, + dag_file_path: str, + bundle_name: str, + bundle_path: str, + comm_addr: str, + logs_addr: str, + ) -> list[str]: + """ + Return the subprocess command for DAG file parsing. + + :param dag_file_path: Absolute path to the DAG file to parse. + :param bundle_name: Name of the DAG bundle. + :param bundle_path: Root path of the DAG bundle. + :param comm_addr: ``host:port`` the subprocess must connect to + for the bidirectional msgpack comm channel. + :param logs_addr: ``host:port`` the subprocess must connect to + for the structured JSON log channel. + :returns: Full command list (e.g. ``["java", "-cp", "...", ...]`` based on each locale). + """ + raise NotImplementedError + + @classmethod + def task_execution_locale_cmd( + cls, + *, + what: TaskInstance, + dag_rel_path: str | os.PathLike[str], + bundle_info: BundleInfo, + comm_addr: str, + logs_addr: str, + ) -> list[str]: + """ + Return the subprocess command for task execution. + + :param what: The task instance to execute. + :param dag_rel_path: Relative path to the DAG file within the bundle. + :param bundle_info: Bundle metadata. + :param comm_addr: ``host:port`` the subprocess must connect to + for the bidirectional msgpack comm channel. + :param logs_addr: ``host:port`` the subprocess must connect to + for the structured JSON log channel. + :returns: Full command list. + """ + raise NotImplementedError + + @classmethod + def run_dag_parsing(cls, *, path: str, bundle_name: str, bundle_path: str) -> None: + """Entry point for running locale-specific Dag File Processing.""" + cls._locale_subprocess_entrypoint( + cls.DagParsingInfo( + dag_file_path=path, + bundle_name=bundle_name, + bundle_path=bundle_path, + ) + ) + + @classmethod + def run_task_execution( + cls, *, what: TaskInstance, dag_rel_path: str | os.PathLike[str], bundle_info: BundleInfo + ) -> None: + cls._locale_subprocess_entrypoint( + cls.TaskExecutionInfo( + what=what, + dag_rel_path=dag_rel_path, + bundle_info=bundle_info, + ) + ) + + @classmethod + def _locale_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskExecutionInfo) -> None: + """ + Spawn the locale subprocess and bridge I/O with the supervisor. + + This is called inside the forked child process where fd 0 is the + bidirectional comms socket to the supervisor. The method: + + 1. Creates TCP servers for comm and logs. + 2. Calls :meth:`dag_parsing_locale_cmd` or :meth:`task_execution_locale_cmd` to get the command. + 3. Spawns the subprocess with ``stdin=/dev/null`` and stderr + captured via a socketpair. + 4. Runs the selector-based bridge until the subprocess exits. + + fd layout (set up by ``_reopen_std_io_handles`` before this runs): + + - fd 0 -- bidirectional comms socket to the supervisor + (``DagFileParseRequest`` <-> ``DagFileParsingResult``, + length-prefixed msgpack frames) + - fd 1 -- stdout socket to the supervisor + - fd 2 -- stderr socket to the supervisor + - fd N -- structured JSON log channel (``log_fd``, configured by + ``_configure_logs_over_json_channel`` -> structlog) + """ + os.environ["_AIRFLOW_PROCESS_CONTEXT"] = "client" + + import structlog + + log = structlog.get_logger(logger_name="task") + log.info( + "Starting locale subprocess", + locale=cls.locale_name, + mode=entrypoint_info.mode, + ) + + # TCP servers for the locale subprocess to connect to. + comm_server = _start_server() + logs_server = _start_server() + comm_host, comm_port = comm_server.getsockname() + logs_host, logs_port = logs_server.getsockname() + + comm_addr = f"{comm_host}:{comm_port}" + logs_addr = f"{logs_host}:{logs_port}" + + # stderr uses a socketpair (instead of ``subprocess.PIPE``) so it + # is a real socket compatible with ``make_buffered_socket_reader``. + child_stderr, read_stderr = socket.socketpair() + + if isinstance(entrypoint_info, cls.DagParsingInfo): + cmd = cls.dag_parsing_locale_cmd( + dag_file_path=entrypoint_info.dag_file_path, + bundle_name=entrypoint_info.bundle_name, + bundle_path=entrypoint_info.bundle_path, + comm_addr=comm_addr, + logs_addr=logs_addr, + ) + elif isinstance(entrypoint_info, cls.TaskExecutionInfo): + cmd = cls.task_execution_locale_cmd( + what=entrypoint_info.what, + dag_rel_path=entrypoint_info.dag_rel_path, + bundle_info=entrypoint_info.bundle_info, + comm_addr=comm_addr, + logs_addr=logs_addr, + ) + else: + raise ValueError(f"Unknown entrypoint_info type: {type(entrypoint_info)}") + + # stdin redirected to /dev/null so the subprocess does not inherit + # fd 0 (the comms socket). + proc = subprocess.Popen( + cmd, + stdin=subprocess.DEVNULL, + stderr=child_stderr.fileno(), + ) + child_stderr.close() + + # Wait for the subprocess to connect to both servers. + locale_comm, _ = comm_server.accept() + locale_logs, _ = logs_server.accept() + comm_server.close() + logs_server.close() + + # fd 0 is the bidirectional comms socket to the supervisor. + supervisor_comm = socket.socket(fileno=os.dup(0)) + + _bridge(supervisor_comm, locale_comm, locale_logs, read_stderr, proc, log) + + +__all__ = ["BaseLocaleCoordinator"] From 19d1b605f83099812ea83087dd7c0ccb22b2432e Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 10 Apr 2026 17:09:39 +0800 Subject: [PATCH 10/75] Implement JavaLocaleCoordinator --- .../providers/languages/java/coordinator.py | 67 +++++ .../languages/java/dag_file_processors.py | 254 +----------------- 2 files changed, 74 insertions(+), 247 deletions(-) create mode 100644 providers/languages/java/src/airflow/providers/languages/java/coordinator.py diff --git a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py new file mode 100644 index 0000000000000..2c2ceba2e05d0 --- /dev/null +++ b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py @@ -0,0 +1,67 @@ +# +# 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. +"""Java locale coordinator that launches a JVM subprocess for Dag file processing and task execution.""" + +from __future__ import annotations + +import email +import zipfile +from pathlib import Path + +from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator + + +def find_main_class(jar_path: Path) -> str: + """Read the Main-Class attribute from the JAR manifest.""" + with zipfile.ZipFile(jar_path) as zf: + with zf.open("META-INF/MANIFEST.MF") as f: + if main_class := email.message_from_binary_file(f).get("Main-Class"): + return main_class + raise FileNotFoundError(f"No Main-Class in manifest of {jar_path}") + + +class JavaLocaleCoordinator(BaseLocaleCoordinator): + """Coordinator that launches a JVM subprocess for DAG parsing.""" + + locale_name = "java" + + @classmethod + def dag_parsing_locale_cmd( + cls, + *, + dag_file_path: str, + bundle_name: str, + bundle_path: str, + comm_addr: str, + logs_addr: str, + ) -> list[str]: + """Build the ``java`` command for parsing a JAR bundle.""" + jar_path = Path(dag_file_path) + # Java bundles are typically thin JARs: the main JAR only contains + # the bundle's own classes while its dependencies (the Airflow Java + # SDK, logging libraries, etc.) are separate JARs that live alongside + # it. Using ``/*`` lets the JVM load every JAR in the directory. + classpath = f"{bundle_path}/*" + return [ + "java", + "-classpath", + classpath, + find_main_class(jar_path), + f"--comm={comm_addr}", + f"--logs={logs_addr}", + ] diff --git a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py index 042aff6b12337..cccdb6a1ee918 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py +++ b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py @@ -15,62 +15,16 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -""" -Java DAG file processor implementations. - -The entrypoint runs inside a forked child process where fd 0 is a -bidirectional socket to the supervisor (set up by ``_fork_main``). -Instead of decoding messages with ``CommsDecoder``, we spawn a Java -subprocess, let it connect back over TCP, and bridge raw bytes between -fd 0 and the Java socket. The supervisor's existing ``_handle_request`` -handles the protocol on its side -- the bridge is transparent. - -I/O multiplexing uses the same selector-based loop as -:class:`~airflow.sdk.execution_time.supervisor.WatchedSubprocess`: -sockets are registered with ``(handler, on_close)`` callback tuples -produced by :func:`~airflow.sdk.execution_time.selector_loop.make_buffered_socket_reader` -and :func:`~airflow.sdk.execution_time.selector_loop.make_raw_forwarder`, -then driven by :func:`~airflow.sdk.execution_time.selector_loop.service_selector`. -""" +"""Java DAG file processor and locale coordinator.""" from __future__ import annotations import contextlib -import email -import json import os -import pathlib -import selectors -import socket -import subprocess -import time -import zipfile -from typing import TYPE_CHECKING +from pathlib import Path from airflow.dag_processing.processor import BaseDagFileProcessor - -if TYPE_CHECKING: - from collections.abc import Generator - - from structlog.typing import FilteringBoundLogger - - -def _start_server() -> socket.socket: - """Create a TCP server socket bound to a random port on localhost.""" - server = socket.socket() - server.bind(("127.0.0.1", 0)) - server.setblocking(True) - server.listen(1) - return server - - -def _find_main_class(jar_path: pathlib.Path) -> str: - """Read the Main-Class attribute from the JAR manifest.""" - with zipfile.ZipFile(jar_path) as zf: - with zf.open("META-INF/MANIFEST.MF") as f: - if main_class := email.message_from_binary_file(f).get("Main-Class"): - return main_class - raise FileNotFoundError(f"No Main-Class in manifest of {jar_path}") +from airflow.providers.languages.java.coordinator import JavaLocaleCoordinator, find_main_class class JavaDagFileProcessor(BaseDagFileProcessor): @@ -83,211 +37,17 @@ class JavaDagFileProcessor(BaseDagFileProcessor): of the default Python ``_parse_file_entrypoint``. """ + language = "java" + def can_handle(self, bundle_name: str, path: str | os.PathLike[str]) -> bool: - # The parent class will only validate against the bundle name - # If the configured bundle name doesn't match, we can skip the more expensive .jar content validation if not super().can_handle(bundle_name, path): return False - # Then the dag_importer will validate based on the .jar content - - # TODO: If we decided to leverage AIP-85 `DagImporterRegistry` - # We should reuse `dag_importer.can_handle` - with contextlib.suppress(FileNotFoundError): - return _find_main_class(pathlib.Path(path)) is not None + return find_main_class(Path(path)) is not None return False @staticmethod def entrypoint(path: str, bundle_name: str, bundle_path: str) -> None: """Bridge fd 0 (supervisor comms) to a Java subprocess over TCP.""" - parse_jar_bundles_entrypoint(path, bundle_name, bundle_path) - - -def parse_jar_bundles_entrypoint(path: str, bundle_name: str, bundle_path: str) -> None: - """ - Spawn a Java subprocess and bridge the supervisor-to-Java communication. - - After ``_fork_main``, fd 0 is a bidirectional socket to the supervisor. - This function: - - 1. Creates TCP servers for comm and logs channels. - 2. Spawns Java via ``subprocess.Popen``, passing both TCP addresses. - 3. Accepts connections from Java on both channels. - 4. Runs a selector-based bridge that transparently forwards bytes - between fd 0 (supervisor) and the Java comm socket, and re-emits - Java's log output through structlog (routed to ``log_fd``). - - No ``CommsDecoder`` is needed -- the supervisor and Java both speak - the length-prefixed msgpack protocol; we just shuttle bytes. - """ - os.environ["_AIRFLOW_PROCESS_CONTEXT"] = "client" - - import structlog - - log = structlog.get_logger(logger_name="task") - - log.info("Starting Java DAG file processor", path=path, bundle_name=bundle_name, bundle_path=bundle_path) - jar_path = pathlib.Path(path) - - # TCP servers for Java to connect to (comm + logs). - comm_server = _start_server() - logs_server = _start_server() - comm_host, comm_port = comm_server.getsockname() - logs_host, logs_port = logs_server.getsockname() - - # Build the classpath from all JARs in the bundle directory. - # Java bundles are typically thin JARs: the main JAR (e.g. example.jar) - # only contains the bundle's own classes while its dependencies (the - # Airflow Java SDK, logging libraries, etc.) are separate JARs that live - # alongside it. Using the ``/*`` wildcard lets the JVM load every - # JAR in the directory, matching the standard Gradle/Maven distribution - # layout produced by ``copyDependencies``-style tasks. - classpath = f"{bundle_path}/*" - - # Spawn the Java subprocess. - # - # fd layout in the forked child (set up by ``_reopen_std_io_handles`` - # before this entrypoint is called): - # - # fd 0 -- bidirectional comms socket to the supervisor - # (``DagFileParseRequest`` <-> ``DagFileParsingResult``, - # length-prefixed msgpack frames) - # fd 1 -- stdout socket to the supervisor - # fd 2 -- stderr socket to the supervisor - # fd N -- structured JSON log channel (``log_fd``, configured by - # ``_configure_logs_over_json_channel`` -> structlog) - # - # We redirect stdin to ``/dev/null`` so that the Java subprocess does - # not inherit fd 0 (the comms socket). Java communicates over the TCP - # sockets passed as ``--comm`` / ``--logs``; the bridge shuttles bytes - # between those TCP sockets and fd 0. - # - # stderr uses a socketpair (instead of ``subprocess.PIPE``) so it is a - # real socket compatible with ``make_buffered_socket_reader``'s - # ``recv_into``. - child_stderr, read_stderr = socket.socketpair() - - proc = subprocess.Popen( - [ - "java", - "-classpath", - classpath, - _find_main_class(jar_path), - f"--comm={comm_host}:{comm_port}", - f"--logs={logs_host}:{logs_port}", - ], - stdin=subprocess.DEVNULL, - stderr=child_stderr.fileno(), - ) - child_stderr.close() # Close the child's end in the parent. - - # Wait for Java to connect to both servers. - java_comm, _ = comm_server.accept() - java_logs, _ = logs_server.accept() - comm_server.close() - logs_server.close() - - # fd 0 is the bidirectional comms socket to the supervisor. - supervisor_comm = socket.socket(fileno=os.dup(0)) - - # Bridge: multiplex I/O between the supervisor and Java. - _bridge(supervisor_comm, java_comm, java_logs, read_stderr, proc, log) - - -_JAVA_LEVEL_MAP = {"warn": "warning", "trace": "debug"} - - -def _java_log_forwarder(log: FilteringBoundLogger) -> Generator[None, bytes | bytearray, None]: - """ - Receive line-buffered bytes from Java and re-emit via structlog. - - Follows the same generator protocol as - :func:`~airflow.sdk.execution_time.supervisor.forward_to_log` and - :func:`~airflow.sdk.execution_time.supervisor.process_log_messages_from_subprocess`: - primed with ``next(gen)``, then fed lines via ``gen.send(line)``. - """ - while True: - raw_line = yield - line = raw_line.decode("utf-8", errors="replace").rstrip() - if not line: - continue - try: - msg = json.loads(line) - level = msg.pop("level", "info") - event = msg.pop("event", "") - msg.pop("timestamp", None) - level_name = _JAVA_LEVEL_MAP.get(level, level) - log_fn = getattr(log, level_name, log.info) - log_fn(event, **msg) - except (json.JSONDecodeError, ValueError, TypeError): - log.info(line) - - -def _bridge( - supervisor_comm: socket.socket, - java_comm: socket.socket, - java_logs: socket.socket, - java_stderr: socket.socket, - proc: subprocess.Popen, - log: FilteringBoundLogger, -) -> None: - """ - Multiplex I/O between the supervisor and Java using a selector loop. - - Four channels are registered with the selector: - - - ``supervisor_comm`` -> ``java_comm`` (raw: ``DagFileParseRequest`` and - intermediate responses) - - ``java_comm`` -> ``supervisor_comm`` (raw: intermediate requests and - ``DagFileParsingResult``) - - ``java_logs`` -> structlog (line-buffered JSON from the Java SDK's - ``LogSender``) - - ``java_stderr`` -> structlog (line-buffered SLF4J output) - - The same ``(handler, on_close)`` callback contract used by - :class:`~airflow.sdk.execution_time.supervisor.WatchedSubprocess` - applies here, driven by :func:`service_selector`. - """ - from airflow.sdk.execution_time.selector_loop import ( - make_buffered_socket_reader, - make_raw_forwarder, - service_selector, - ) - - sel = selectors.DefaultSelector() - - def on_close(sock: socket.socket) -> None: - with contextlib.suppress(KeyError): - sel.unregister(sock) - - # Comm: bidirectional raw byte forwarding. - sel.register(supervisor_comm, selectors.EVENT_READ, make_raw_forwarder(java_comm, on_close)) - sel.register(java_comm, selectors.EVENT_READ, make_raw_forwarder(supervisor_comm, on_close)) - - # Logs: line-buffered JSON -> structlog. - sel.register( - java_logs, - selectors.EVENT_READ, - make_buffered_socket_reader(_java_log_forwarder(log), on_close), - ) - sel.register( - java_stderr, - selectors.EVENT_READ, - make_buffered_socket_reader(_java_log_forwarder(log), on_close), - ) - - # Event loop -- runs until Java exits and all sockets are drained. - while sel.get_map(): - service_selector(sel, timeout=1.0) - if proc.poll() is not None: - # Java has exited -- drain remaining data with a short deadline. - deadline = time.monotonic() + 5.0 - while sel.get_map() and time.monotonic() < deadline: - service_selector(sel, timeout=0.5) - break - - sel.close() - for sock in (supervisor_comm, java_comm, java_logs, java_stderr): - with contextlib.suppress(OSError): - sock.close() + JavaLocaleCoordinator.run_dag_parsing(path=path, bundle_name=bundle_name, bundle_path=bundle_path) From 0039418032c3c2c40ae6761d33f06b6b11d5d753 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 13 Apr 2026 09:58:29 +0800 Subject: [PATCH 11/75] Add Java task coordinator and entrypoint for locale-specific execution --- .../providers/languages/java/coordinator.py | 31 ++++++++++ .../languages/java/task_coordinators.py | 21 +++++++ .../airflow/sdk/execution_time/coordinator.py | 57 +++++++++++++++++-- .../airflow/sdk/execution_time/task_runner.py | 45 +++++++++++++++ 4 files changed, 148 insertions(+), 6 deletions(-) diff --git a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py index 2c2ceba2e05d0..9bfcf2b86e240 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py +++ b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py @@ -20,11 +20,16 @@ from __future__ import annotations import email +import os import zipfile from pathlib import Path +from typing import TYPE_CHECKING from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator +if TYPE_CHECKING: + from airflow.sdk.api.datamodels._generated import BundleInfo, TaskInstance + def find_main_class(jar_path: Path) -> str: """Read the Main-Class attribute from the JAR manifest.""" @@ -65,3 +70,29 @@ def dag_parsing_locale_cmd( f"--comm={comm_addr}", f"--logs={logs_addr}", ] + + @classmethod + def task_execution_locale_cmd( + cls, + *, + what: TaskInstance, + dag_rel_path: str | os.PathLike[str], + bundle_info: BundleInfo, + comm_addr: str, + logs_addr: str, + ) -> list[str]: + """Build the ``java`` command for executing a task in a JAR bundle.""" + jar_path = Path(dag_rel_path) + # Java bundles are typically thin JARs: the main JAR only contains + # the bundle's own classes while its dependencies (the Airflow Java + # SDK, logging libraries, etc.) are separate JARs that live alongside + # it. Using ``/*`` lets the JVM load every JAR in the directory. + classpath = f"{jar_path.parent}/*" + return [ + "java", + "-classpath", + classpath, + find_main_class(jar_path), + f"--comm={comm_addr}", + f"--logs={logs_addr}", + ] diff --git a/providers/languages/java/src/airflow/providers/languages/java/task_coordinators.py b/providers/languages/java/src/airflow/providers/languages/java/task_coordinators.py index c5b0437140326..d673da37e95f7 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/task_coordinators.py +++ b/providers/languages/java/src/airflow/providers/languages/java/task_coordinators.py @@ -18,8 +18,29 @@ from __future__ import annotations +import os +from typing import TYPE_CHECKING + +from airflow.providers.languages.java.coordinator import JavaLocaleCoordinator + +if TYPE_CHECKING: + from airflow.sdk.api.datamodels._generated import BundleInfo, TaskInstance + from airflow.sdk.execution_time.comms import StartupDetails + class JavaTaskCoordinator: """Placeholder task coordinator entry point for Java workloads.""" language = "java" + + @staticmethod + def entrypoint( + what: TaskInstance, + dag_rel_path: str | os.PathLike[str], + bundle_info: BundleInfo, + startup_details: StartupDetails, + ) -> None: + """Bridge fd 0 (supervisor comms) to a Java subprocess over TCP.""" + JavaLocaleCoordinator.run_task_execution( + what=what, dag_rel_path=dag_rel_path, bundle_info=bundle_info, startup_details=startup_details + ) diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 07da05db234c1..6ff5a475001d1 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -52,6 +52,7 @@ from structlog.typing import FilteringBoundLogger from airflow.sdk.api.datamodels._generated import BundleInfo, TaskInstance + from airflow.sdk.execution_time.comms import StartupDetails def _start_server() -> socket.socket: @@ -63,6 +64,26 @@ def _start_server() -> socket.socket: return server +def _send_startup_details(locale_comm: socket.socket, startup_details: StartupDetails) -> None: + """ + Re-encode and send the ``StartupDetails`` frame to the locale subprocess. + + In the task execution flow, ``task_runner.main()`` consumes the + ``StartupDetails`` message from fd 0 (to determine routing) before + delegating to the locale coordinator. This function re-serializes + the message and writes it to the locale subprocess's comm socket so + the subprocess receives it as if it came directly from the supervisor. + """ + from airflow.sdk.execution_time.comms import _ResponseFrame + + # Use mode="json" so that datetime, UUID, and other complex Python + # types are serialized as plain strings/numbers in msgpack — avoiding + # msgpack extension types (e.g. Timestamp) that non-Python decoders + # may not support. + frame = _ResponseFrame(id=0, body=startup_details.model_dump(mode="json")) + locale_comm.sendall(frame.as_bytes()) + + def _bridge( supervisor_comm: socket.socket, locale_comm: socket.socket, @@ -90,7 +111,10 @@ def _bridge( make_raw_forwarder, service_selector, ) - from airflow.sdk.execution_time.supervisor import process_log_messages_from_subprocess + from airflow.sdk.execution_time.supervisor import ( + forward_to_log, + process_log_messages_from_subprocess, + ) sel = selectors.DefaultSelector() @@ -104,18 +128,25 @@ def on_close(sock: socket.socket) -> None: sel.register(supervisor_comm, selectors.EVENT_READ, make_raw_forwarder(locale_comm, on_close)) sel.register(locale_comm, selectors.EVENT_READ, make_raw_forwarder(supervisor_comm, on_close)) - # Logs: line-buffered JSON -> structlog, using the same log processor - # as WatchedSubprocess (handles level mapping, timestamp parsing, and - # exception extraction). + # TCP logs channel: line-buffered JSON from the locale SDK's LogSender, + # processed with the same handler as WatchedSubprocess (level mapping, + # timestamp parsing, exception extraction). sel.register( locale_logs, selectors.EVENT_READ, make_buffered_socket_reader(process_log_messages_from_subprocess(target_loggers), on_close), ) + # stderr: plain-text output from the locale process's logging framework + # (e.g. SLF4J simple logger). Use forward_to_log which handles raw + # text lines, not process_log_messages_from_subprocess which expects JSON. + import logging + sel.register( locale_stderr, selectors.EVENT_READ, - make_buffered_socket_reader(process_log_messages_from_subprocess(target_loggers), on_close), + make_buffered_socket_reader( + forward_to_log(target_loggers, logger="task.stderr", level=logging.ERROR), on_close + ), ) # Event loop -- runs until the subprocess exits and all sockets are drained. @@ -161,6 +192,7 @@ class TaskExecutionInfo(NamedTuple): what: TaskInstance dag_rel_path: str | os.PathLike[str] bundle_info: BundleInfo + startup_details: StartupDetails mode: str = "task-execution" @classmethod @@ -224,13 +256,19 @@ def run_dag_parsing(cls, *, path: str, bundle_name: str, bundle_path: str) -> No @classmethod def run_task_execution( - cls, *, what: TaskInstance, dag_rel_path: str | os.PathLike[str], bundle_info: BundleInfo + cls, + *, + what: TaskInstance, + dag_rel_path: str | os.PathLike[str], + bundle_info: BundleInfo, + startup_details: StartupDetails, ) -> None: cls._locale_subprocess_entrypoint( cls.TaskExecutionInfo( what=what, dag_rel_path=dag_rel_path, bundle_info=bundle_info, + startup_details=startup_details, ) ) @@ -316,6 +354,13 @@ def _locale_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskExe comm_server.close() logs_server.close() + # For task execution the supervisor already sent ``StartupDetails`` + # on fd 0 and ``task_runner.main()`` consumed it before delegating + # here. Re-encode and forward it to the locale subprocess so it + # knows which task to execute. + if isinstance(entrypoint_info, cls.TaskExecutionInfo): + _send_startup_details(locale_comm, entrypoint_info.startup_details) + # fd 0 is the bidirectional comms socket to the supervisor. supervisor_comm = socket.socket(fileno=os.dup(0)) diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 313309fa40100..7e9b13fbb54a2 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -2080,6 +2080,51 @@ def flush_spans(): provider.force_flush(timeout_millis=timeout_millis) +def _resolve_locale_entrypoint(startup_details: StartupDetails, log: Logger) -> Callable[[], None] | None: + """ + Check provider-registered task coordinators for a locale-specific entrypoint. + + If a coordinator claims this task (e.g. a Java coordinator for JVM-based + tasks), return a no-arg callable that bridges fd 0 to the locale + subprocess. Otherwise return ``None`` to fall through to the standard + Python execution path. + """ + import functools + + from airflow._shared.module_loading import import_string + from airflow.sdk.providers_manager_runtime import ProvidersManagerTaskRuntime + + # TODO: Route based on a ``language`` field on the TaskInstance model + # once it is exposed via the Execution API. For now, we iterate over + # all registered coordinators and let each decide via its own matching + # logic (e.g. checking the bundle type or task metadata). + for coordinator_path in ProvidersManagerTaskRuntime().task_coordinators: + try: + coordinator_cls = import_string(coordinator_path) + except Exception: + log.exception("Failed to import task coordinator", path=coordinator_path) + continue + + if not hasattr(coordinator_cls, "entrypoint"): + continue + + log.debug( + "Resolved locale-specific entrypoint for task", + coordinator=coordinator_path, + task_id=startup_details.ti.task_id, + ) + return functools.partial( + coordinator_cls.entrypoint, + what=startup_details.ti, + # dag_rel_path=startup_details.dag_rel_path, #TODO: Not sure why we get `.` for dag_rel_path, mock as expected path for now + dag_rel_path="/files/java-bundle/lib/example.jar", + bundle_info=startup_details.bundle_info, + startup_details=startup_details, + ) + + return None + + @flush_spans() def main(): log = structlog.get_logger(logger_name="task") From 79c1f5aa9d54e4da0a2c8395cbec23b3b81178f5 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 13 Apr 2026 11:30:24 +0800 Subject: [PATCH 12/75] Refactor Java provider to with generic process coordinators and update related components --- .../src/airflow/dag_processing/processor.py | 85 ++++--------------- .../src/airflow/provider.yaml.schema.json | 11 +-- .../src/airflow/provider_info.schema.json | 11 +-- airflow-core/src/airflow/providers_manager.py | 32 +++---- .../unit/always/test_providers_manager.py | 16 ++-- providers/languages/java/docs/index.rst | 2 +- providers/languages/java/provider.yaml | 12 +-- .../providers/languages/java/coordinator.py | 10 ++- .../languages/java/dag_file_processors.py | 53 ------------ .../languages/java/get_provider_info.py | 11 +-- .../languages/java/task_coordinators.py | 46 ---------- .../unit/languages/java/test_java_provider.py | 18 ++-- .../airflow/sdk/execution_time/coordinator.py | 22 ++++- .../airflow/sdk/execution_time/task_runner.py | 10 +-- .../airflow/sdk/providers_manager_runtime.py | 30 +++---- .../test_providers_manager_runtime.py | 16 ++-- 16 files changed, 116 insertions(+), 269 deletions(-) delete mode 100644 providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py delete mode 100644 providers/languages/java/src/airflow/providers/languages/java/task_coordinators.py diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index cd2cbce874f9f..ea7b620f04416 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -181,51 +181,6 @@ class DagFileParsingResult(BaseModel): ] -class BaseDagFileProcessor: - """ - Base class for provider-contributed DAG file processors. - - Providers register subclasses in their ``provider.yaml`` under ``dag-file-processors``. - When :class:`DagFileProcessorProcess` starts, it checks all registered processors - via :meth:`can_handle`; the first match's :meth:`entrypoint` is used as the subprocess - target instead of the default ``_parse_file_entrypoint``. - - The ``entrypoint`` runs inside a forked child process. After the fork, - fd 0 is a bidirectional socket connected to the supervisor (the same channel - that ``_parse_file_entrypoint`` uses via :class:`CommsDecoder`). The entrypoint - can bridge this socket to an external process (e.g. a Java subprocess over TCP) - without needing ``CommsDecoder`` at all — just forward raw bytes between fd 0 - and the external process's socket. - - The supervisor will send a :class:`DagFileParseRequest` on fd 0 after the fork - and expects a :class:`DagFileParsingResult` back on the same channel. - """ - - def __init__( - self, - *, - target_bundle_name: str, - ): - # We will only store dag_bundle_name but not dag_bundle_path here because it's DagBundle's responsibility to manage the path - self.target_bundle_name = target_bundle_name - - def can_handle(self, bundle_name: str, path: str | os.PathLike[str]) -> bool: - """Return ``True`` if this processor should handle the given file.""" - # The Airflow Core DagFileProcessorProcess will pass the bundle_name to see - return self.target_bundle_name == bundle_name - - @staticmethod - def entrypoint(path: str, bundle_name: str, bundle_path: str) -> None: - """ - Entry point called in the forked child process. - - :param path: Absolute path to the file to process. - :param bundle_name: Name of the DAG bundle. - :param bundle_path: Root path of the DAG bundle. - """ - raise NotImplementedError - - def _pre_import_airflow_modules(file_path: str, log: FilteringBoundLogger) -> None: """ Pre-import Airflow modules found in the given file. @@ -618,12 +573,12 @@ def start( # type: ignore[override] ) -> Self: logger = kwargs["logger"] - # Check if a provider-registered dag file processor should handle this file - logger.debug("Checking for provider-registered DAG file processor entrypoint for file", path=path) + # Check if a provider-registered process coordinator should handle this file + logger.debug("Checking for provider-registered process coordinator entrypoint for file", path=path) resolved_target = cls._resolve_processor_target(path, bundle_name, bundle_path, logger) if resolved_target is not None: target = resolved_target - logger.debug("Resolved provider-registered DAG file processor entrypoint for file", path=path) + logger.debug("Resolved provider-registered process coordinator entrypoint for file", path=path) else: _pre_import_airflow_modules(os.fspath(path), logger) @@ -646,7 +601,7 @@ def _resolve_processor_target( log: FilteringBoundLogger, ) -> Callable[[], None] | None: """ - Return the entrypoint of the first provider dag file processor that can handle *path*. + Return the entrypoint of the first provider process coordinator that can handle *path*. The returned callable is a ``functools.partial`` that binds *path*, *bundle_name* and *bundle_path* so the supervisor can pass it as a no-arg ``target`` to @@ -655,44 +610,36 @@ def _resolve_processor_target( from airflow._shared.module_loading import import_string from airflow.providers_manager import ProvidersManager - for processor_class_path in ProvidersManager().dag_file_processors: + for coordinator_class_path in ProvidersManager().process_coordinators: try: log.debug( - "Checking provider-registered DAG file processor %s for file %s", - processor_class_path, + "Checking process coordinator %s for file %s", + coordinator_class_path, path, ) - processor_cls = import_string(processor_class_path) - processor_instance: BaseDagFileProcessor = processor_cls(target_bundle_name=bundle_name) - log.debug( - "Instantiated provider-registered DAG file processor %s for file %s", - processor_class_path, - path, - ) - if processor_instance.can_handle(bundle_name, path): + coordinator_cls = import_string(coordinator_class_path) + if coordinator_cls.can_handle_dag_file(bundle_name, path): log.debug( - "Using provider-registered DAG file processor %s for file %s", - processor_class_path, + "Using process coordinator %s for file %s", + coordinator_class_path, path, ) return functools.partial( - processor_instance.entrypoint, + coordinator_cls.run_dag_parsing, path=os.fspath(path), bundle_name=bundle_name, bundle_path=os.fspath(bundle_path), ) log.debug( - "Provider-registered DAG file processor %s cannot handle file %s with bundle name %s", - processor_class_path, + "Process coordinator %s cannot handle file %s with bundle name %s", + coordinator_class_path, path, bundle_name, ) except Exception: - log.warning("Failed to load dag file processor %s", processor_class_path, exc_info=True) + log.warning("Failed to load process coordinator %s", coordinator_class_path, exc_info=True) - log.debug( - "No provider-registered DAG file processor found for file %s, using default processor", path - ) + log.debug("No process coordinator found for file %s, using default processor", path) return None def _on_child_started( diff --git a/airflow-core/src/airflow/provider.yaml.schema.json b/airflow-core/src/airflow/provider.yaml.schema.json index 2412347a56c30..4f0143d65cda6 100644 --- a/airflow-core/src/airflow/provider.yaml.schema.json +++ b/airflow-core/src/airflow/provider.yaml.schema.json @@ -624,16 +624,9 @@ } } }, - "task-coordinators": { + "process-coordinators": { "type": "array", - "description": "Task Coordinator class names", - "items": { - "type": "string" - } - }, - "dag-file-processors": { - "type": "array", - "description": "Dag File Processor class names", + "description": "Process Coordinator class names (BaseLocaleCoordinator subclasses)", "items": { "type": "string" } diff --git a/airflow-core/src/airflow/provider_info.schema.json b/airflow-core/src/airflow/provider_info.schema.json index 1f9ea31bf1274..26cccdc5efa87 100644 --- a/airflow-core/src/airflow/provider_info.schema.json +++ b/airflow-core/src/airflow/provider_info.schema.json @@ -447,16 +447,9 @@ } } }, - "task-coordinators": { + "process-coordinators": { "type": "array", - "description": "Task Coordinator class names", - "items": { - "type": "string" - } - }, - "dag-file-processors": { - "type": "array", - "description": "Dag File Processor class names", + "description": "Process Coordinator class names (BaseLocaleCoordinator subclasses)", "items": { "type": "string" } diff --git a/airflow-core/src/airflow/providers_manager.py b/airflow-core/src/airflow/providers_manager.py index c74a8aa97a4a7..6f08c06474ecb 100644 --- a/airflow-core/src/airflow/providers_manager.py +++ b/airflow-core/src/airflow/providers_manager.py @@ -448,7 +448,7 @@ def __init__(self): ) # Set of plugins contained in providers self._plugins_set: set[PluginInfo] = set() - self._dag_file_processors: list[str] = [] + self._process_coordinators: list[str] = [] self._init_airflow_core_hooks() self._runtime_manager = None @@ -626,11 +626,11 @@ def initialize_providers_configuration(self): self.initialize_providers_list() self._discover_config() - @provider_info_cache("dag_file_processors") - def initialize_providers_dag_file_processors(self): - """Lazy initialization of providers dag file processors.""" + @provider_info_cache("process_coordinators") + def initialize_providers_process_coordinators(self): + """Lazy initialization of providers process coordinators.""" self.initialize_providers_list() - self._discover_dag_file_processors() + self._discover_process_coordinators() @provider_info_cache("plugins") def initialize_providers_plugins(self): @@ -1287,13 +1287,13 @@ def _discover_config(self) -> None: if provider.data.get("config"): self._provider_configs[provider_package] = provider.data.get("config") # type: ignore[assignment] - def _discover_dag_file_processors(self) -> None: - """Retrieve all dag file processors defined in the providers.""" + def _discover_process_coordinators(self) -> None: + """Retrieve all process coordinators defined in the providers.""" for provider_package, provider in self._provider_dict.items(): - for dag_file_processor_class_path in provider.data.get("dag-file-processors", []): - if _correctness_check(provider_package, dag_file_processor_class_path, provider): - self._dag_file_processors.append(dag_file_processor_class_path) - self._dag_file_processors = sorted(set(self._dag_file_processors)) + for coordinator_class_path in provider.data.get("process-coordinators", []): + if _correctness_check(provider_package, coordinator_class_path, provider): + self._process_coordinators.append(coordinator_class_path) + self._process_coordinators = sorted(set(self._process_coordinators)) def _discover_plugins(self) -> None: """Retrieve all plugins defined in the providers.""" @@ -1493,10 +1493,10 @@ def db_managers(self) -> list[str]: return sorted(self._db_manager_class_name_set) @property - def dag_file_processors(self) -> list[str]: - """Returns dag file processor class paths available in providers.""" - self.initialize_providers_dag_file_processors() - return self._dag_file_processors + def process_coordinators(self) -> list[str]: + """Returns process coordinator class paths available in providers.""" + self.initialize_providers_process_coordinators() + return self._process_coordinators @property def filesystem_module_names(self) -> list[str]: @@ -1569,7 +1569,7 @@ def _cleanup(self): self._trigger_info_set.clear() self._notification_info_set.clear() self._plugins_set.clear() - self._dag_file_processors.clear() + self._process_coordinators.clear() self._cli_command_functions_set.clear() self._cli_command_provider_name_set.clear() diff --git a/airflow-core/tests/unit/always/test_providers_manager.py b/airflow-core/tests/unit/always/test_providers_manager.py index bce1125e98a3d..8db5a1de0e227 100644 --- a/airflow-core/tests/unit/always/test_providers_manager.py +++ b/airflow-core/tests/unit/always/test_providers_manager.py @@ -259,25 +259,25 @@ def test_dialects(self): assert dialect_class_names == ["default", "mssql", "postgresql"] @patch("airflow.providers_manager.import_string") - def test_dag_file_processors(self, mock_import_string): + def test_process_coordinators(self, mock_import_string): mock_import_string.return_value = object() providers_manager = ProvidersManager() providers_manager._provider_dict = LazyDictWithCache() providers_manager._provider_dict["apache-airflow-providers-languages-java"] = ProviderInfo( version="0.0.1", data={ - "dag-file-processors": [ - "airflow.providers.languages.java.dag_file_processors.ZProcessor", - "airflow.providers.languages.java.dag_file_processors.AProcessor", - "airflow.providers.languages.java.dag_file_processors.ZProcessor", + "process-coordinators": [ + "airflow.providers.languages.java.coordinator.ZCoordinator", + "airflow.providers.languages.java.coordinator.ACoordinator", + "airflow.providers.languages.java.coordinator.ZCoordinator", ] }, ) with patch.object(providers_manager, "initialize_providers_list"): - assert providers_manager.dag_file_processors == [ - "airflow.providers.languages.java.dag_file_processors.AProcessor", - "airflow.providers.languages.java.dag_file_processors.ZProcessor", + assert providers_manager.process_coordinators == [ + "airflow.providers.languages.java.coordinator.ACoordinator", + "airflow.providers.languages.java.coordinator.ZCoordinator", ] diff --git a/providers/languages/java/docs/index.rst b/providers/languages/java/docs/index.rst index 0c161234ff871..a737ab8ee92d5 100644 --- a/providers/languages/java/docs/index.rst +++ b/providers/languages/java/docs/index.rst @@ -58,7 +58,7 @@ The Languages: Java provider registers Java-specific task coordinator and DAG fi apache-airflow-providers-languages-java package ------------------------------------------------------ -Java language support for Apache Airflow task coordinators and DAG file processors. +Java language support for Apache Airflow process coordinators. Release: 0.1.0 diff --git a/providers/languages/java/provider.yaml b/providers/languages/java/provider.yaml index f7602b4bdee19..5e0c9d70ba33f 100644 --- a/providers/languages/java/provider.yaml +++ b/providers/languages/java/provider.yaml @@ -19,7 +19,7 @@ package-name: apache-airflow-providers-languages-java name: "Languages: Java" description: | - Java language support for Apache Airflow task coordinators and DAG file processors. + Java language support for Apache Airflow process coordinators. state: ready lifecycle: incubation @@ -35,11 +35,7 @@ integrations: - integration-name: Java external-doc-url: https://openjdk.org/ tags: - - language - - java + - software -task-coordinators: - - airflow.providers.languages.java.task_coordinators.JavaTaskCoordinator - -dag-file-processors: - - airflow.providers.languages.java.dag_file_processors.JavaDagFileProcessor +process-coordinators: + - airflow.providers.languages.java.coordinator.JavaLocaleCoordinator diff --git a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py index 9bfcf2b86e240..ecccbcf3fff0d 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py +++ b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py @@ -19,6 +19,7 @@ from __future__ import annotations +import contextlib import email import os import zipfile @@ -41,10 +42,17 @@ def find_main_class(jar_path: Path) -> str: class JavaLocaleCoordinator(BaseLocaleCoordinator): - """Coordinator that launches a JVM subprocess for DAG parsing.""" + """Coordinator that launches a JVM subprocess for DAG parsing and task execution.""" locale_name = "java" + @classmethod + def can_handle_dag_file(cls, bundle_name: str, path: str | os.PathLike[str]) -> bool: + """Return ``True`` when *path* is a JAR with a ``Main-Class`` manifest entry.""" + with contextlib.suppress(FileNotFoundError): + return find_main_class(Path(path)) is not None + return False + @classmethod def dag_parsing_locale_cmd( cls, diff --git a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py b/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py deleted file mode 100644 index cccdb6a1ee918..0000000000000 --- a/providers/languages/java/src/airflow/providers/languages/java/dag_file_processors.py +++ /dev/null @@ -1,53 +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. -"""Java DAG file processor and locale coordinator.""" - -from __future__ import annotations - -import contextlib -import os -from pathlib import Path - -from airflow.dag_processing.processor import BaseDagFileProcessor -from airflow.providers.languages.java.coordinator import JavaLocaleCoordinator, find_main_class - - -class JavaDagFileProcessor(BaseDagFileProcessor): - """ - DAG file processor for Java JAR bundle workloads. - - Registered via ``dag-file-processors`` in the Java provider's ``provider.yaml``. - When the dag processor encounters a file that belongs to a Java bundle, - this processor's :meth:`entrypoint` is used as the subprocess target instead - of the default Python ``_parse_file_entrypoint``. - """ - - language = "java" - - def can_handle(self, bundle_name: str, path: str | os.PathLike[str]) -> bool: - if not super().can_handle(bundle_name, path): - return False - - with contextlib.suppress(FileNotFoundError): - return find_main_class(Path(path)) is not None - return False - - @staticmethod - def entrypoint(path: str, bundle_name: str, bundle_path: str) -> None: - """Bridge fd 0 (supervisor comms) to a Java subprocess over TCP.""" - JavaLocaleCoordinator.run_dag_parsing(path=path, bundle_name=bundle_name, bundle_path=bundle_path) diff --git a/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py b/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py index b7772a9241715..887aef84fbb96 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py +++ b/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py @@ -25,14 +25,9 @@ def get_provider_info(): return { "package-name": "apache-airflow-providers-languages-java", "name": "Languages: Java", - "description": "Java language support for Apache Airflow task coordinators and DAG file processors.\n", + "description": "Java language support for Apache Airflow process coordinators.\n", "integrations": [ - { - "integration-name": "Java", - "external-doc-url": "https://openjdk.org/", - "tags": ["language", "java"], - } + {"integration-name": "Java", "external-doc-url": "https://openjdk.org/", "tags": ["software"]} ], - "task-coordinators": ["airflow.providers.languages.java.task_coordinators.JavaTaskCoordinator"], - "dag-file-processors": ["airflow.providers.languages.java.dag_file_processors.JavaDagFileProcessor"], + "process-coordinators": ["airflow.providers.languages.java.coordinator.JavaLocaleCoordinator"], } diff --git a/providers/languages/java/src/airflow/providers/languages/java/task_coordinators.py b/providers/languages/java/src/airflow/providers/languages/java/task_coordinators.py deleted file mode 100644 index d673da37e95f7..0000000000000 --- a/providers/languages/java/src/airflow/providers/languages/java/task_coordinators.py +++ /dev/null @@ -1,46 +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. -"""Java task coordinator implementations.""" - -from __future__ import annotations - -import os -from typing import TYPE_CHECKING - -from airflow.providers.languages.java.coordinator import JavaLocaleCoordinator - -if TYPE_CHECKING: - from airflow.sdk.api.datamodels._generated import BundleInfo, TaskInstance - from airflow.sdk.execution_time.comms import StartupDetails - - -class JavaTaskCoordinator: - """Placeholder task coordinator entry point for Java workloads.""" - - language = "java" - - @staticmethod - def entrypoint( - what: TaskInstance, - dag_rel_path: str | os.PathLike[str], - bundle_info: BundleInfo, - startup_details: StartupDetails, - ) -> None: - """Bridge fd 0 (supervisor comms) to a Java subprocess over TCP.""" - JavaLocaleCoordinator.run_task_execution( - what=what, dag_rel_path=dag_rel_path, bundle_info=bundle_info, startup_details=startup_details - ) diff --git a/providers/languages/java/tests/unit/languages/java/test_java_provider.py b/providers/languages/java/tests/unit/languages/java/test_java_provider.py index 2403573b58cf8..859fa073a9ff4 100644 --- a/providers/languages/java/tests/unit/languages/java/test_java_provider.py +++ b/providers/languages/java/tests/unit/languages/java/test_java_provider.py @@ -1,3 +1,4 @@ +# # 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 @@ -16,32 +17,27 @@ # under the License. from __future__ import annotations -from airflow.providers.languages.java.dag_file_processors import JavaDagFileProcessor +from airflow.providers.languages.java.coordinator import JavaLocaleCoordinator from airflow.providers.languages.java.get_provider_info import get_provider_info -from airflow.providers.languages.java.task_coordinators import JavaTaskCoordinator def test_get_provider_info_exposes_java_runtime_components(): assert get_provider_info() == { "package-name": "apache-airflow-providers-languages-java", "name": "Languages: Java", - "description": "Java language support for Apache Airflow task coordinators and DAG file processors.\n", + "description": "Java language support for Apache Airflow process coordinators.\n", "integrations": [ { "integration-name": "Java", "external-doc-url": "https://openjdk.org/", - "tags": ["language", "java"], + "tags": ["software"], } ], - "task-coordinators": [ - "airflow.providers.languages.java.task_coordinators.JavaTaskCoordinator", - ], - "dag-file-processors": [ - "airflow.providers.languages.java.dag_file_processors.JavaDagFileProcessor", + "process-coordinators": [ + "airflow.providers.languages.java.coordinator.JavaLocaleCoordinator", ], } def test_java_provider_entrypoints_are_importable(): - assert JavaTaskCoordinator.language == "java" - assert JavaDagFileProcessor.language == "java" + assert JavaLocaleCoordinator.locale_name == "java" diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 6ff5a475001d1..009c342c79e94 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -169,9 +169,14 @@ class BaseLocaleCoordinator: """ Base coordinator for locale-specific DAG file processing and task execution. + Providers register subclasses in their ``provider.yaml`` under + ``process-coordinators``. Both :class:`ProvidersManager` (airflow-core) + and :class:`ProvidersManagerTaskRuntime` (task-sdk) discover registered + coordinators through this single extension point. + Subclasses represent a specific language runtime (Java, Go, etc.) and - only need to implement :meth:`dag_parsing_locale_cmd` and - :meth:`task_execution_locale_cmd` to return the subprocess command. + only need to implement :meth:`can_handle_dag_file`, + :meth:`dag_parsing_locale_cmd` and :meth:`task_execution_locale_cmd`. The base class owns the entire bridge lifecycle: TCP servers, subprocess management, selector-based I/O loop, and cleanup. """ @@ -195,6 +200,19 @@ class TaskExecutionInfo(NamedTuple): startup_details: StartupDetails mode: str = "task-execution" + @classmethod + def can_handle_dag_file(cls, bundle_name: str, path: str | os.PathLike[str]) -> bool: + """ + Return ``True`` if this coordinator should handle DAG-file parsing for *path*. + + Called by :meth:`DagFileProcessorProcess._resolve_processor_target` to + decide whether to delegate parsing to this coordinator's + :meth:`run_dag_parsing` instead of the default Python entrypoint. + + The default implementation returns ``False``; subclasses must override. + """ + return False + @classmethod def dag_parsing_locale_cmd( cls, diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 7e9b13fbb54a2..227c31bd03b77 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -2082,7 +2082,7 @@ def flush_spans(): def _resolve_locale_entrypoint(startup_details: StartupDetails, log: Logger) -> Callable[[], None] | None: """ - Check provider-registered task coordinators for a locale-specific entrypoint. + Check provider-registered process coordinators for a locale-specific entrypoint. If a coordinator claims this task (e.g. a Java coordinator for JVM-based tasks), return a no-arg callable that bridges fd 0 to the locale @@ -2098,14 +2098,14 @@ def _resolve_locale_entrypoint(startup_details: StartupDetails, log: Logger) -> # once it is exposed via the Execution API. For now, we iterate over # all registered coordinators and let each decide via its own matching # logic (e.g. checking the bundle type or task metadata). - for coordinator_path in ProvidersManagerTaskRuntime().task_coordinators: + for coordinator_path in ProvidersManagerTaskRuntime().process_coordinators: try: coordinator_cls = import_string(coordinator_path) except Exception: - log.exception("Failed to import task coordinator", path=coordinator_path) + log.exception("Failed to import process coordinator", path=coordinator_path) continue - if not hasattr(coordinator_cls, "entrypoint"): + if not hasattr(coordinator_cls, "run_task_execution"): continue log.debug( @@ -2114,7 +2114,7 @@ def _resolve_locale_entrypoint(startup_details: StartupDetails, log: Logger) -> task_id=startup_details.ti.task_id, ) return functools.partial( - coordinator_cls.entrypoint, + coordinator_cls.run_task_execution, what=startup_details.ti, # dag_rel_path=startup_details.dag_rel_path, #TODO: Not sure why we get `.` for dag_rel_path, mock as expected path for now dag_rel_path="/files/java-bundle/lib/example.jar", diff --git a/task-sdk/src/airflow/sdk/providers_manager_runtime.py b/task-sdk/src/airflow/sdk/providers_manager_runtime.py index 716c69f7cf575..182eb06b5e36e 100644 --- a/task-sdk/src/airflow/sdk/providers_manager_runtime.py +++ b/task-sdk/src/airflow/sdk/providers_manager_runtime.py @@ -150,7 +150,7 @@ def __init__(self): # Keeps dict of hooks keyed by connection type. They are lazy evaluated at access time self._hooks_lazy_dict: LazyDictWithCache[str, HookInfo | Callable] = LazyDictWithCache() self._plugins_set: set[PluginInfo] = set() - self._task_coordinators: list[str] = [] + self._process_coordinators: list[str] = [] self._provider_schema_validator = _create_provider_info_schema_validator() self._init_airflow_core_hooks() # Populated by initialize_provider_configs(); holds provider-contributed config sections. @@ -221,11 +221,11 @@ def initialize_providers_taskflow_decorator(self): self.initialize_providers_list() self._discover_taskflow_decorators() - @provider_info_cache("task_coordinators") - def initialize_providers_task_coordinators(self): - """Lazy initialization of providers workload coordinators.""" + @provider_info_cache("process_coordinators") + def initialize_providers_process_coordinators(self): + """Lazy initialization of providers process coordinators.""" self.initialize_providers_list() - self._discover_task_coordinators() + self._discover_process_coordinators() @provider_info_cache("provider_configs") def initialize_provider_configs(self): @@ -471,13 +471,13 @@ def _import_hook( connection_testable=hasattr(hook_class, "test_connection"), ) - def _discover_task_coordinators(self) -> None: - """Retrieve all workload coordinators defined in the providers.""" + def _discover_process_coordinators(self) -> None: + """Retrieve all process coordinators defined in the providers.""" for provider_package, provider in self._provider_dict.items(): - for coordinator_class_path in provider.data.get("task-coordinators", []): + for coordinator_class_path in provider.data.get("process-coordinators", []): if _correctness_check(provider_package, coordinator_class_path, provider): - self._task_coordinators.append(coordinator_class_path) - self._task_coordinators = sorted(set(self._task_coordinators)) + self._process_coordinators.append(coordinator_class_path) + self._process_coordinators = sorted(set(self._process_coordinators)) def _discover_filesystems(self) -> None: """Retrieve all filesystems defined in the providers.""" @@ -627,10 +627,10 @@ def plugins(self) -> list[PluginInfo]: return sorted(self._plugins_set, key=lambda x: x.plugin_class) @property - def task_coordinators(self) -> list[str]: - """Returns workload coordinator class paths available in providers.""" - self.initialize_providers_task_coordinators() - return self._task_coordinators + def process_coordinators(self) -> list[str]: + """Returns process coordinator class paths available in providers.""" + self.initialize_providers_process_coordinators() + return self._process_coordinators @property def provider_configs(self) -> list[tuple[str, dict[str, Any]]]: @@ -664,7 +664,7 @@ def _cleanup(self): self._asset_uri_handlers.clear() self._asset_factories.clear() self._asset_to_openlineage_converters.clear() - self._task_coordinators.clear() + self._process_coordinators.clear() self._provider_configs.clear() # Imported lazily to preserve SDK conf lazy initialization and avoid a configuration/runtime cycle. diff --git a/task-sdk/tests/task_sdk/test_providers_manager_runtime.py b/task-sdk/tests/task_sdk/test_providers_manager_runtime.py index d7cdf0abe068c..f73d723174946 100644 --- a/task-sdk/tests/task_sdk/test_providers_manager_runtime.py +++ b/task-sdk/tests/task_sdk/test_providers_manager_runtime.py @@ -244,24 +244,24 @@ def test_already_initialized_provider_configs_emits_deprecation_warning(self): pm.already_initialized_provider_configs @patch("airflow.sdk.providers_manager_runtime.import_string") - def test_task_coordinators(self, mock_import_string): + def test_process_coordinators(self, mock_import_string): mock_import_string.return_value = object() providers_manager = ProvidersManagerTaskRuntime() providers_manager._provider_dict["apache-airflow-providers-languages-java"] = ProviderInfo( version="0.0.1", data={ - "task-coordinators": [ - "airflow.providers.languages.java.task_coordinators.ZCoordinator", - "airflow.providers.languages.java.task_coordinators.ACoordinator", - "airflow.providers.languages.java.task_coordinators.ZCoordinator", + "process-coordinators": [ + "airflow.providers.languages.java.coordinator.ZCoordinator", + "airflow.providers.languages.java.coordinator.ACoordinator", + "airflow.providers.languages.java.coordinator.ZCoordinator", ] }, ) with patch.object(providers_manager, "initialize_providers_list"): - assert providers_manager.task_coordinators == [ - "airflow.providers.languages.java.task_coordinators.ACoordinator", - "airflow.providers.languages.java.task_coordinators.ZCoordinator", + assert providers_manager.process_coordinators == [ + "airflow.providers.languages.java.coordinator.ACoordinator", + "airflow.providers.languages.java.coordinator.ZCoordinator", ] def test_initialize_provider_configs_can_reload_sdk_conf(self): From 8e312575517640c4e7c03854c5ca040bc20a2bb7 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 14 Apr 2026 15:26:56 +0800 Subject: [PATCH 13/75] Fix Coordinator by getting the correct dag bundle and dag path --- .../providers/languages/java/coordinator.py | 7 +- .../airflow/sdk/execution_time/coordinator.py | 75 ++++++++++++------- .../airflow/sdk/execution_time/task_runner.py | 27 +++++-- 3 files changed, 73 insertions(+), 36 deletions(-) diff --git a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py index ecccbcf3fff0d..b20037f4cfd22 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py +++ b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py @@ -84,18 +84,19 @@ def task_execution_locale_cmd( cls, *, what: TaskInstance, - dag_rel_path: str | os.PathLike[str], + dag_file_path: str, + bundle_path: str, bundle_info: BundleInfo, comm_addr: str, logs_addr: str, ) -> list[str]: """Build the ``java`` command for executing a task in a JAR bundle.""" - jar_path = Path(dag_rel_path) + jar_path = Path(dag_file_path) # Java bundles are typically thin JARs: the main JAR only contains # the bundle's own classes while its dependencies (the Airflow Java # SDK, logging libraries, etc.) are separate JARs that live alongside # it. Using ``/*`` lets the JVM load every JAR in the directory. - classpath = f"{jar_path.parent}/*" + classpath = f"{bundle_path}/*" return [ "java", "-classpath", diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 009c342c79e94..34f8ff7631800 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -242,7 +242,8 @@ def task_execution_locale_cmd( cls, *, what: TaskInstance, - dag_rel_path: str | os.PathLike[str], + dag_file_path: str, + bundle_path: str, bundle_info: BundleInfo, comm_addr: str, logs_addr: str, @@ -251,7 +252,8 @@ def task_execution_locale_cmd( Return the subprocess command for task execution. :param what: The task instance to execute. - :param dag_rel_path: Relative path to the DAG file within the bundle. + :param dag_file_path: Absolute path to the DAG file. + :param bundle_path: Root path of the DAG bundle. :param bundle_info: Bundle metadata. :param comm_addr: ``host:port`` the subprocess must connect to for the bidirectional msgpack comm channel. @@ -338,6 +340,11 @@ def _locale_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskExe # is a real socket compatible with ``make_buffered_socket_reader``. child_stderr, read_stderr = socket.socketpair() + # For task execution, hold a BundleVersionLock for the entire + # subprocess lifetime to prevent the bundle version from being + # garbage-collected while the locale process is still running. + bundle_version_lock: contextlib.AbstractContextManager = contextlib.nullcontext() + if isinstance(entrypoint_info, cls.DagParsingInfo): cmd = cls.dag_parsing_locale_cmd( dag_file_path=entrypoint_info.dag_file_path, @@ -347,42 +354,60 @@ def _locale_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskExe logs_addr=logs_addr, ) elif isinstance(entrypoint_info, cls.TaskExecutionInfo): + from pathlib import Path + + # import from core now will raise static check error from `check-core-imports` check + # We should support ignore label for the above static check + # directly commit for now + from airflow.dag_processing.bundles.base import BundleVersionLock + from airflow.sdk.execution_time.task_runner import resolve_bundle + + bundle_instance = resolve_bundle(entrypoint_info.bundle_info, log) + resolved_bundle_path = str(bundle_instance.path) + resolved_dag_file_path = os.fspath(Path(bundle_instance.path, entrypoint_info.dag_rel_path)) + cmd = cls.task_execution_locale_cmd( what=entrypoint_info.what, - dag_rel_path=entrypoint_info.dag_rel_path, + dag_file_path=resolved_dag_file_path, + bundle_path=resolved_bundle_path, bundle_info=entrypoint_info.bundle_info, comm_addr=comm_addr, logs_addr=logs_addr, ) + bundle_version_lock = BundleVersionLock( + bundle_name=entrypoint_info.bundle_info.name, + bundle_version=entrypoint_info.bundle_info.version, + ) else: raise ValueError(f"Unknown entrypoint_info type: {type(entrypoint_info)}") - # stdin redirected to /dev/null so the subprocess does not inherit - # fd 0 (the comms socket). - proc = subprocess.Popen( - cmd, - stdin=subprocess.DEVNULL, - stderr=child_stderr.fileno(), - ) - child_stderr.close() + with bundle_version_lock: + # stdin redirected to /dev/null so the subprocess does not inherit + # fd 0 (the comms socket). + proc = subprocess.Popen( + cmd, + stdin=subprocess.DEVNULL, + stderr=child_stderr.fileno(), + ) + child_stderr.close() - # Wait for the subprocess to connect to both servers. - locale_comm, _ = comm_server.accept() - locale_logs, _ = logs_server.accept() - comm_server.close() - logs_server.close() + # Wait for the subprocess to connect to both servers. + locale_comm, _ = comm_server.accept() + locale_logs, _ = logs_server.accept() + comm_server.close() + logs_server.close() - # For task execution the supervisor already sent ``StartupDetails`` - # on fd 0 and ``task_runner.main()`` consumed it before delegating - # here. Re-encode and forward it to the locale subprocess so it - # knows which task to execute. - if isinstance(entrypoint_info, cls.TaskExecutionInfo): - _send_startup_details(locale_comm, entrypoint_info.startup_details) + # For task execution the supervisor already sent ``StartupDetails`` + # on fd 0 and ``task_runner.main()`` consumed it before delegating + # here. Re-encode and forward it to the locale subprocess so it + # knows which task to execute. + if isinstance(entrypoint_info, cls.TaskExecutionInfo): + _send_startup_details(locale_comm, entrypoint_info.startup_details) - # fd 0 is the bidirectional comms socket to the supervisor. - supervisor_comm = socket.socket(fileno=os.dup(0)) + # fd 0 is the bidirectional comms socket to the supervisor. + supervisor_comm = socket.socket(fileno=os.dup(0)) - _bridge(supervisor_comm, locale_comm, locale_logs, read_stderr, proc, log) + _bridge(supervisor_comm, locale_comm, locale_logs, read_stderr, proc, log) __all__ = ["BaseLocaleCoordinator"] diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 227c31bd03b77..7265683d37dba 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -50,6 +50,7 @@ from airflow.sdk.api.client import get_hostname, getuser from airflow.sdk.api.datamodels._generated import ( AssetProfile, + BundleInfo, DagRun, PreviousTIResponse, TaskInstance, @@ -841,12 +842,7 @@ def parse(what: StartupDetails, log: Logger) -> RuntimeTaskInstance: bundle_info = what.bundle_info bundle_prepare_start = time.monotonic() - bundle_instance = DagBundlesManager().get_bundle( - name=bundle_info.name, - version=bundle_info.version, - ) - bundle_instance.initialize() - _verify_bundle_access(bundle_instance, log) + bundle_instance = resolve_bundle(bundle_info, log) bundle_prepare_ms = int((time.monotonic() - bundle_prepare_start) * 1000) dag_absolute_path = os.fspath(Path(bundle_instance.path, what.dag_rel_path)) @@ -973,6 +969,22 @@ def _verify_bundle_access(bundle_instance: BaseDagBundle, log: Logger) -> None: ) +def resolve_bundle(bundle_info: BundleInfo, log: Logger) -> BaseDagBundle: + """ + Resolve, initialize, and verify access to a DAG bundle. + + Used by both the standard Python task execution path and locale + coordinators (Java, Go, etc.) to obtain a ready-to-use bundle instance. + """ + bundle_instance = DagBundlesManager().get_bundle( + name=bundle_info.name, + version=bundle_info.version, + ) + bundle_instance.initialize() + _verify_bundle_access(bundle_instance, log) + return bundle_instance + + def get_startup_details() -> StartupDetails: # The parent sends us a StartupDetails message un-prompted. After this, every single message is only sent # in response to us sending a request. @@ -2116,8 +2128,7 @@ def _resolve_locale_entrypoint(startup_details: StartupDetails, log: Logger) -> return functools.partial( coordinator_cls.run_task_execution, what=startup_details.ti, - # dag_rel_path=startup_details.dag_rel_path, #TODO: Not sure why we get `.` for dag_rel_path, mock as expected path for now - dag_rel_path="/files/java-bundle/lib/example.jar", + dag_rel_path=startup_details.dag_rel_path, bundle_info=startup_details.bundle_info, startup_details=startup_details, ) From deff3318e609c6660169dc6348967314f4056083 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 15 Apr 2026 14:12:17 +0800 Subject: [PATCH 14/75] Make @task.stub(language=java) works --- .../execution_api/datamodels/taskinstance.py | 1 + .../execution_api/versions/__init__.py | 2 + .../execution_api/versions/v2026_07_01.py | 30 +++++++++++ .../src/airflow/executors/workloads/task.py | 2 + ...3_0_add_language_field_to_task_instance.py | 50 +++++++++++++++++++ .../src/airflow/models/taskinstance.py | 3 ++ .../serialization/definitions/baseoperator.py | 1 + .../providers/standard/decorators/stub.py | 5 ++ .../airflow/sdk/api/datamodels/_generated.py | 1 + task-sdk/src/airflow/sdk/bases/operator.py | 3 ++ .../airflow/sdk/definitions/mappedoperator.py | 4 ++ .../airflow/sdk/execution_time/task_runner.py | 25 +++++++--- uv.lock | 37 -------------- 13 files changed, 119 insertions(+), 45 deletions(-) create mode 100644 airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_07_01.py create mode 100644 airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_language_field_to_task_instance.py diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py index a0d9739080118..8cf413d5819f3 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py @@ -256,6 +256,7 @@ class TaskInstance(BaseModel): map_index: int = -1 hostname: str | None = None context_carrier: dict | None = None + language: str | None = None class AssetReferenceAssetEventDagRun(StrictBaseModel): diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py b/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py index ab995da52d062..98685a1e0b25d 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py @@ -47,10 +47,12 @@ AddTeamNameField, ) from airflow.api_fastapi.execution_api.versions.v2026_06_30 import AddVariableKeysEndpoint +from airflow.api_fastapi.execution_api.versions.v2026_07_01 import AddLanguageField bundle = VersionBundle( HeadVersion(), Version("2026-06-30", AddVariableKeysEndpoint), + Version("2026-06-16", AddRetryPolicyFields, AddLanguageField), Version( "2026-06-16", AddRetryPolicyFields, diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_07_01.py b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_07_01.py new file mode 100644 index 0000000000000..7882a51787944 --- /dev/null +++ b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_07_01.py @@ -0,0 +1,30 @@ +# 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 cadwyn import VersionChange, schema + +from airflow.api_fastapi.execution_api.datamodels.taskinstance import TaskInstance + + +class AddLanguageField(VersionChange): + """Add the `language` field to the TaskInstance model.""" + + description = __doc__ + + instructions_to_migrate_to_previous_version = (schema(TaskInstance).field("language").didnt_exist,) diff --git a/airflow-core/src/airflow/executors/workloads/task.py b/airflow-core/src/airflow/executors/workloads/task.py index 9af3f33c10efd..f0f56ed575e6c 100644 --- a/airflow-core/src/airflow/executors/workloads/task.py +++ b/airflow-core/src/airflow/executors/workloads/task.py @@ -55,6 +55,8 @@ class BaseTaskInstanceDTO(BaseModel): priority_weight: int executor_config: dict | None = Field(default=None, exclude=True) + language: str | None = None + parent_context_carrier: dict | None = None context_carrier: dict | None = None diff --git a/airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_language_field_to_task_instance.py b/airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_language_field_to_task_instance.py new file mode 100644 index 0000000000000..fd0e230251899 --- /dev/null +++ b/airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_language_field_to_task_instance.py @@ -0,0 +1,50 @@ +# +# 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. + +""" +Add language field to task instance. + +Revision ID: 7d3c6395b7f6 +Revises: 9fabad868fdb +Create Date: 2026-04-15 05:57:22.353951 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "7d3c6395b7f6" +down_revision = "9fabad868fdb" +branch_labels = None +depends_on = None +airflow_version = "3.3.0" + + +def upgrade(): + """Apply add language field to task instance.""" + with op.batch_alter_table("task_instance", schema=None) as batch_op: + batch_op.add_column(sa.Column("language", sa.String(length=64), nullable=True)) + + +def downgrade(): + """Unapply add language field to task instance.""" + with op.batch_alter_table("task_instance", schema=None) as batch_op: + batch_op.drop_column("language") diff --git a/airflow-core/src/airflow/models/taskinstance.py b/airflow-core/src/airflow/models/taskinstance.py index 9cc8d6a627140..dc74003ea6ea9 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -578,6 +578,7 @@ class TaskInstance(Base, LoggingMixin, BaseWorkload): priority_weight: Mapped[int | None] = mapped_column(Integer, nullable=True) operator: Mapped[str | None] = mapped_column(String(1000), nullable=True) custom_operator_name: Mapped[str | None] = mapped_column(String(1000), nullable=True) + language: Mapped[str | None] = mapped_column(String(64), nullable=True) queued_dttm: Mapped[datetime | None] = mapped_column(UtcDateTime, nullable=True) scheduled_dttm: Mapped[datetime | None] = mapped_column(UtcDateTime, nullable=True) queued_by_job_id: Mapped[int | None] = mapped_column(Integer, nullable=True) @@ -765,6 +766,7 @@ def insert_mapping( "executor_config": task.executor_config, "operator": task.task_type, "custom_operator_name": getattr(task, "operator_name", None), + "language": getattr(task, "language", None), "map_index": map_index, "_task_display_property_value": task.task_display_name, "dag_version_id": dag_version_id, @@ -943,6 +945,7 @@ def refresh_from_task(self, task: Operator, pool_override: str | None = None) -> self.executor = task.executor self.executor_config = task.executor_config self.operator = task.task_type + self.language = getattr(task, "language", None) op_name = getattr(task, "operator_name", None) self.custom_operator_name = op_name if isinstance(op_name, str) else "" # Re-apply cluster policy here so that task default do not overload previous data diff --git a/airflow-core/src/airflow/serialization/definitions/baseoperator.py b/airflow-core/src/airflow/serialization/definitions/baseoperator.py index 6bafc5891235a..b1b91ff8f7ecb 100644 --- a/airflow-core/src/airflow/serialization/definitions/baseoperator.py +++ b/airflow-core/src/airflow/serialization/definitions/baseoperator.py @@ -195,6 +195,7 @@ def get_serialized_fields(cls): "ignore_first_depends_on_past", "inlets", "is_setup", + "language", "is_teardown", "map_index_template", "max_active_tis_per_dag", diff --git a/providers/standard/src/airflow/providers/standard/decorators/stub.py b/providers/standard/src/airflow/providers/standard/decorators/stub.py index f29d123c740c1..ac2061700e67c 100644 --- a/providers/standard/src/airflow/providers/standard/decorators/stub.py +++ b/providers/standard/src/airflow/providers/standard/decorators/stub.py @@ -78,6 +78,7 @@ def stub( python_callable: Callable | None = None, queue: str | None = None, executor: str | None = None, + language: str | None = None, **kwargs, ) -> TaskDecorator: """ @@ -86,11 +87,15 @@ def stub( Stub tasks exist in the Dag graph only, but the execution must happen in an external environment via the Task Execution Interface. + :param language: The language runtime that should execute this task (e.g. ``"java"``). + When set, the worker routes the task to the matching locale coordinator + instead of executing it as Python. """ return task_decorator_factory( decorated_operator_class=_StubOperator, python_callable=python_callable, queue=queue, executor=executor, + language=language, **kwargs, ) diff --git a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py index 2d999111eb01b..0f3e78739c653 100644 --- a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py +++ b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py @@ -512,6 +512,7 @@ class TaskInstance(BaseModel): map_index: Annotated[int | None, Field(title="Map Index")] = -1 hostname: Annotated[str | None, Field(title="Hostname")] = None context_carrier: Annotated[dict[str, Any] | None, Field(title="Context Carrier")] = None + language: Annotated[str | None, Field(title="Language")] = None class BundleInfo(BaseModel): diff --git a/task-sdk/src/airflow/sdk/bases/operator.py b/task-sdk/src/airflow/sdk/bases/operator.py index 8d6de54eb6d3d..c0f028f7b0fd7 100644 --- a/task-sdk/src/airflow/sdk/bases/operator.py +++ b/task-sdk/src/airflow/sdk/bases/operator.py @@ -905,6 +905,7 @@ def say_hello_world(**context): max_active_tis_per_dagrun: int | None = None executor: str | None = None executor_config: dict | None = None + language: str | None = None do_xcom_push: bool = True multiple_outputs: bool = False inlets: list[Any] = field(default_factory=list) @@ -1063,6 +1064,7 @@ def __init__( max_active_tis_per_dagrun: int | None = None, executor: str | None = None, executor_config: dict | None = None, + language: str | None = None, do_xcom_push: bool = True, multiple_outputs: bool = False, inlets: Any | None = None, @@ -1142,6 +1144,7 @@ def __init__( self.end_date = timezone.convert_to_utc(end_date) self.executor = executor self.executor_config = executor_config or {} + self.language = language self.run_as_user = run_as_user # TODO: # self.retries = parse_retries(retries) diff --git a/task-sdk/src/airflow/sdk/definitions/mappedoperator.py b/task-sdk/src/airflow/sdk/definitions/mappedoperator.py index 0faa2ab6f1850..ff7ed9321b283 100644 --- a/task-sdk/src/airflow/sdk/definitions/mappedoperator.py +++ b/task-sdk/src/airflow/sdk/definitions/mappedoperator.py @@ -673,6 +673,10 @@ def executor(self) -> str | None: def executor_config(self) -> dict: return self.partial_kwargs.get("executor_config", {}) + @property + def language(self) -> str | None: + return self.partial_kwargs.get("language") + @property def inlets(self) -> list[Any]: return self.partial_kwargs.get("inlets", []) diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 7265683d37dba..8cb983b8242a1 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -2096,20 +2096,20 @@ def _resolve_locale_entrypoint(startup_details: StartupDetails, log: Logger) -> """ Check provider-registered process coordinators for a locale-specific entrypoint. - If a coordinator claims this task (e.g. a Java coordinator for JVM-based - tasks), return a no-arg callable that bridges fd 0 to the locale - subprocess. Otherwise return ``None`` to fall through to the standard - Python execution path. + If the task's ``language`` field matches a coordinator's ``locale_name``, + return a no-arg callable that bridges fd 0 to the locale subprocess. + Otherwise return ``None`` to fall through to the standard Python + execution path. """ + language = startup_details.ti.language + if language is None: + return None + import functools from airflow._shared.module_loading import import_string from airflow.sdk.providers_manager_runtime import ProvidersManagerTaskRuntime - # TODO: Route based on a ``language`` field on the TaskInstance model - # once it is exposed via the Execution API. For now, we iterate over - # all registered coordinators and let each decide via its own matching - # logic (e.g. checking the bundle type or task metadata). for coordinator_path in ProvidersManagerTaskRuntime().process_coordinators: try: coordinator_cls = import_string(coordinator_path) @@ -2120,9 +2120,13 @@ def _resolve_locale_entrypoint(startup_details: StartupDetails, log: Logger) -> if not hasattr(coordinator_cls, "run_task_execution"): continue + if getattr(coordinator_cls, "locale_name", None) != language: + continue + log.debug( "Resolved locale-specific entrypoint for task", coordinator=coordinator_path, + language=language, task_id=startup_details.ti.task_id, ) return functools.partial( @@ -2133,6 +2137,11 @@ def _resolve_locale_entrypoint(startup_details: StartupDetails, log: Logger) -> startup_details=startup_details, ) + log.warning( + "No process coordinator found for language", + language=language, + task_id=startup_details.ti.task_id, + ) return None diff --git a/uv.lock b/uv.lock index ca383e6f4e983..bca09add09083 100644 --- a/uv.lock +++ b/uv.lock @@ -129,7 +129,6 @@ apache-airflow-providers-keycloak = false apache-airflow-providers-trino = false apache-airflow-providers-common-messaging = false apache-airflow-providers-standard = false -apache-airflow-providers-languages-java = false apache-airflow-providers-singularity = false apache-airflow-providers-common-compat = false apache-airflow-ctl-tests = false @@ -226,7 +225,6 @@ members = [ "apache-airflow-providers-jdbc", "apache-airflow-providers-jenkins", "apache-airflow-providers-keycloak", - "apache-airflow-providers-languages-java", "apache-airflow-providers-microsoft-azure", "apache-airflow-providers-microsoft-mssql", "apache-airflow-providers-microsoft-psrp", @@ -1042,7 +1040,6 @@ all = [ { name = "apache-airflow-providers-jdbc" }, { name = "apache-airflow-providers-jenkins" }, { name = "apache-airflow-providers-keycloak" }, - { name = "apache-airflow-providers-languages-java" }, { name = "apache-airflow-providers-microsoft-azure" }, { name = "apache-airflow-providers-microsoft-mssql" }, { name = "apache-airflow-providers-microsoft-psrp" }, @@ -1293,9 +1290,6 @@ kerberos = [ keycloak = [ { name = "apache-airflow-providers-keycloak" }, ] -languages-java = [ - { name = "apache-airflow-providers-languages-java" }, -] ldap = [ { name = "python-ldap" }, ] @@ -1661,8 +1655,6 @@ requires-dist = [ { name = "apache-airflow-providers-jenkins", marker = "extra == 'jenkins'", editable = "providers/jenkins" }, { name = "apache-airflow-providers-keycloak", marker = "extra == 'all'", editable = "providers/keycloak" }, { name = "apache-airflow-providers-keycloak", marker = "extra == 'keycloak'", editable = "providers/keycloak" }, - { name = "apache-airflow-providers-languages-java", marker = "extra == 'all'", editable = "providers/languages/java" }, - { name = "apache-airflow-providers-languages-java", marker = "extra == 'languages-java'", editable = "providers/languages/java" }, { name = "apache-airflow-providers-microsoft-azure", marker = "extra == 'all'", editable = "providers/microsoft/azure" }, { name = "apache-airflow-providers-microsoft-azure", marker = "extra == 'microsoft-azure'", editable = "providers/microsoft/azure" }, { name = "apache-airflow-providers-microsoft-mssql", marker = "extra == 'all'", editable = "providers/microsoft/mssql" }, @@ -5977,35 +5969,6 @@ dev = [ ] docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "devel-common" }] -[[package]] -name = "apache-airflow-providers-languages-java" -version = "0.1.0" -source = { editable = "providers/languages/java" } -dependencies = [ - { name = "apache-airflow" }, -] - -[package.dev-dependencies] -dev = [ - { name = "apache-airflow" }, - { name = "apache-airflow-devel-common" }, - { name = "apache-airflow-task-sdk" }, -] -docs = [ - { name = "apache-airflow-devel-common", extra = ["docs"] }, -] - -[package.metadata] -requires-dist = [{ name = "apache-airflow", editable = "." }] - -[package.metadata.requires-dev] -dev = [ - { name = "apache-airflow", editable = "." }, - { name = "apache-airflow-devel-common", editable = "devel-common" }, - { name = "apache-airflow-task-sdk", editable = "task-sdk" }, -] -docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "devel-common" }] - [[package]] name = "apache-airflow-providers-microsoft-azure" version = "13.3.0" From eb7ba13aff74d368614a98366eab03623e38872a Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 16 Apr 2026 10:14:19 +0800 Subject: [PATCH 15/75] Make coordinator respect Jar bundle based on TI workload type --- providers/languages/java/docs/index.rst | 28 +++ providers/languages/java/provider.yaml | 17 ++ providers/languages/java/pyproject.toml | 8 + .../languages/java/bundle_scanner.py | 190 ++++++++++++++++++ .../providers/languages/java/coordinator.py | 56 ++++-- .../languages/java/get_provider_info.py | 14 ++ .../airflow/sdk/execution_time/task_runner.py | 5 +- 7 files changed, 295 insertions(+), 23 deletions(-) create mode 100644 providers/languages/java/src/airflow/providers/languages/java/bundle_scanner.py diff --git a/providers/languages/java/docs/index.rst b/providers/languages/java/docs/index.rst index a737ab8ee92d5..482210c2706c8 100644 --- a/providers/languages/java/docs/index.rst +++ b/providers/languages/java/docs/index.rst @@ -86,3 +86,31 @@ PIP package Version required ================== ================== ``apache-airflow`` ``>=3.0.0`` ================== ================== + +Cross provider package dependencies +----------------------------------- + +Those are dependencies that might be needed in order to use all the features of the package. +You need to install the specified provider distributions in order to use them. + +You can install such cross-provider dependencies when installing from PyPI. For example: + +.. code-block:: bash + + pip install apache-airflow-providers-languages-java[common.compat] + + +================================================================================================================== ================= +Dependent package Extra +================================================================================================================== ================= +`apache-airflow-providers-common-compat `_ ``common.compat`` +================================================================================================================== ================= + +Downloading official packages +----------------------------- + +You can download officially released packages and verify their checksums and signatures from the +`Official Apache Download site `_ + +* `The apache-airflow-providers-languages-java 0.1.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-languages-java 0.1.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/languages/java/provider.yaml b/providers/languages/java/provider.yaml index 5e0c9d70ba33f..b9020b0d38af7 100644 --- a/providers/languages/java/provider.yaml +++ b/providers/languages/java/provider.yaml @@ -37,5 +37,22 @@ integrations: tags: - software +config: + java: + description: "Options for the Java language provider." + options: + bundles_folder: + description: | + Path to the directory containing Java DAG bundle JARs. + When using Python stub DAGs that delegate task execution to Java, + the coordinator scans this directory to find the JAR bundle matching + the target dag_id. Each immediate subdirectory is treated as a + separate bundle home, and the directory itself is also checked + (flat layout). + type: string + version_added: ~ + example: ~/airflow/java-bundles + default: "" + process-coordinators: - airflow.providers.languages.java.coordinator.JavaLocaleCoordinator diff --git a/providers/languages/java/pyproject.toml b/providers/languages/java/pyproject.toml index f5a62794a077d..493c07a5c10aa 100644 --- a/providers/languages/java/pyproject.toml +++ b/providers/languages/java/pyproject.toml @@ -62,11 +62,19 @@ dependencies = [ "apache-airflow>=3.0.0", ] +# The optional dependencies should be modified in place in the generated file +# Any change in the dependencies is preserved when the file is regenerated +[project.optional-dependencies] +"common.compat" = [ + "apache-airflow-providers-common-compat" +] + [dependency-groups] dev = [ "apache-airflow", "apache-airflow-task-sdk", "apache-airflow-devel-common", + "apache-airflow-providers-common-compat", # Additional devel dependencies (do not remove this line and add extra development dependencies) ] diff --git a/providers/languages/java/src/airflow/providers/languages/java/bundle_scanner.py b/providers/languages/java/src/airflow/providers/languages/java/bundle_scanner.py new file mode 100644 index 0000000000000..a53e6d99db578 --- /dev/null +++ b/providers/languages/java/src/airflow/providers/languages/java/bundle_scanner.py @@ -0,0 +1,190 @@ +# +# 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. +""" +Scan directories for Airflow Java SDK bundle JARs. + +Mirrors the Java SDK's ``BundleScanner`` — checks each JAR's manifest for +``Airflow-Java-SDK-Metadata``, reads the embedded metadata YAML, and +resolves the main class and classpath needed to launch the bundle process. +""" + +from __future__ import annotations + +import email +import os +import zipfile +from pathlib import Path +from typing import NamedTuple + +import yaml + +_MANIFEST_PATH = "META-INF/MANIFEST.MF" +METADATA_MANIFEST_KEY = "Airflow-Java-SDK-Metadata" +SDK_VERSION_MANIFEST_KEY = "Airflow-Java-SDK-Version" +MAIN_CLASS_MANIFEST_KEY = "Main-Class" + + +class ResolvedJarBundle(NamedTuple): + """A resolved Java DAG bundle: everything needed to start the bundle process.""" + + main_class: str + classpath: str + + +class BundleScanner: + """ + Locate Airflow Java SDK bundles inside a directory tree. + + Supports two directory layouts: + + - **Nested** - each immediate subdirectory of *bundles_dir* is a bundle home. + - **Flat** — *bundles_dir* itself contains the bundle JARs. + + Within a bundle home the JVM convention of a ``lib/`` subdirectory for + dependency JARs is respected automatically. + """ + + def __init__(self, bundles_dir: Path) -> None: + self._bundles_dir = bundles_dir + + def resolve(self, dag_id: str) -> ResolvedJarBundle: + """ + Find the bundle whose metadata YAML lists *dag_id*. + + :raises FileNotFoundError: if no matching bundle is found. + """ + for bundle_home in self._candidate_homes(): + jars = _jar_files(bundle_home) + if not jars: + continue + + for jar_path in jars: + result = _read_bundle_jar(jar_path) + if result is None: + continue + main_class, dag_ids = result + if dag_id in dag_ids: + classpath = os.pathsep.join(str(j.resolve()) for j in jars) + return ResolvedJarBundle(main_class=main_class, classpath=classpath) + + raise FileNotFoundError(f"No JAR bundle containing dag_id={dag_id!r} found in {self._bundles_dir}") + + @staticmethod + def resolve_jar(jar_path: Path) -> str: + """ + Read ``Main-Class`` from a single bundle JAR, validating SDK attributes. + + :raises FileNotFoundError: if the JAR is not a valid Airflow Java SDK bundle. + """ + result = _read_bundle_jar(jar_path) + if result is None: + raise FileNotFoundError( + f"Not a valid Airflow Java SDK bundle: {jar_path} " + f"(requires {METADATA_MANIFEST_KEY} and {MAIN_CLASS_MANIFEST_KEY})" + ) + return result[0] + + def _candidate_homes(self) -> list[Path]: + """Return normalised bundle-home directories to inspect.""" + candidates: list[Path] = [] + + # Each subdirectory is a potential bundle home (nested layout). + if self._bundles_dir.is_dir(): + for child in sorted(self._bundles_dir.iterdir()): + if child.is_dir(): + candidates.append(_normalize_bundle_home(child)) + + # The directory itself (flat layout). + candidates.append(_normalize_bundle_home(self._bundles_dir)) + return candidates + + +def _jar_files(directory: Path) -> list[Path]: + """List all ``.jar`` files in *directory*, sorted by name.""" + if not directory.is_dir(): + return [] + return sorted(p for p in directory.iterdir() if p.is_file() and p.suffix == ".jar") + + +def _normalize_bundle_home(path: Path) -> Path: + """ + Normalize a bundle path to the directory containing JARs. + + Handles the common JVM distribution layout where dependency JARs + live in a ``lib/`` subdirectory (Gradle ``application`` plugin, + Maven Assembly, sbt-native-packager, etc.). + + - If *path* points to a JAR file, use its parent directory. + - If the directory has a ``lib/`` subdirectory containing JARs, use that. + - Otherwise, return the directory as-is. + """ + normalized = path.resolve() + if normalized.is_file() and normalized.suffix == ".jar": + return normalized.parent + lib = normalized / "lib" + if lib.is_dir() and any(p.suffix == ".jar" for p in lib.iterdir()): + return lib + return normalized + + +def _read_bundle_jar(jar_path: Path) -> tuple[str, set[str]] | None: + """ + Read ``Main-Class`` and dag IDs from a JAR's manifest and embedded metadata. + + Returns ``(main_class, dag_ids)`` when the JAR carries valid + ``Airflow-Java-SDK-Metadata`` and ``Main-Class`` manifest attributes + and the referenced metadata YAML contains at least one dag ID. + Returns ``None`` otherwise. + """ + try: + with zipfile.ZipFile(jar_path) as zf: + try: + with zf.open(_MANIFEST_PATH) as f: + manifest = email.message_from_binary_file(f) + except KeyError: + return None + + metadata_file = manifest.get(METADATA_MANIFEST_KEY) + if not metadata_file: + return None + + main_class = manifest.get(MAIN_CLASS_MANIFEST_KEY) + if not main_class: + return None + + try: + with zf.open(metadata_file) as f: + content = f.read().decode() + except KeyError: + return None + except zipfile.BadZipFile: + return None + + dag_ids = _parse_dag_ids_from_metadata(content) + if not dag_ids: + return None + + return main_class, dag_ids + + +def _parse_dag_ids_from_metadata(yaml_content: str) -> set[str]: + """Parse dag IDs from an ``airflow-metadata.yaml`` content string.""" + data = yaml.safe_load(yaml_content) + if not isinstance(data, dict) or "dags" not in data: + return set() + return set(data["dags"].keys()) diff --git a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py index b20037f4cfd22..7af19d3273c36 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py +++ b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py @@ -20,27 +20,18 @@ from __future__ import annotations import contextlib -import email import os import zipfile from pathlib import Path from typing import TYPE_CHECKING +from airflow.providers.languages.java.bundle_scanner import BundleScanner from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator if TYPE_CHECKING: from airflow.sdk.api.datamodels._generated import BundleInfo, TaskInstance -def find_main_class(jar_path: Path) -> str: - """Read the Main-Class attribute from the JAR manifest.""" - with zipfile.ZipFile(jar_path) as zf: - with zf.open("META-INF/MANIFEST.MF") as f: - if main_class := email.message_from_binary_file(f).get("Main-Class"): - return main_class - raise FileNotFoundError(f"No Main-Class in manifest of {jar_path}") - - class JavaLocaleCoordinator(BaseLocaleCoordinator): """Coordinator that launches a JVM subprocess for DAG parsing and task execution.""" @@ -48,9 +39,9 @@ class JavaLocaleCoordinator(BaseLocaleCoordinator): @classmethod def can_handle_dag_file(cls, bundle_name: str, path: str | os.PathLike[str]) -> bool: - """Return ``True`` when *path* is a JAR with a ``Main-Class`` manifest entry.""" - with contextlib.suppress(FileNotFoundError): - return find_main_class(Path(path)) is not None + """Return ``True`` when *path* is a JAR with valid Airflow Java SDK manifest attributes.""" + with contextlib.suppress(FileNotFoundError, zipfile.BadZipFile, KeyError): + return BundleScanner.resolve_jar(Path(path)) is not None return False @classmethod @@ -74,7 +65,7 @@ def dag_parsing_locale_cmd( "java", "-classpath", classpath, - find_main_class(jar_path), + BundleScanner.resolve_jar(jar_path), f"--comm={comm_addr}", f"--logs={logs_addr}", ] @@ -91,17 +82,38 @@ def task_execution_locale_cmd( logs_addr: str, ) -> list[str]: """Build the ``java`` command for executing a task in a JAR bundle.""" - jar_path = Path(dag_file_path) - # Java bundles are typically thin JARs: the main JAR only contains - # the bundle's own classes while its dependencies (the Airflow Java - # SDK, logging libraries, etc.) are separate JARs that live alongside - # it. Using ``/*`` lets the JVM load every JAR in the directory. - classpath = f"{bundle_path}/*" + if what.language is None: + # Case 1: Pure Java Dag — the dag_file_path points directly to a + # bundle JAR inside the Airflow Core Dag Bundle. + jar_path = Path(dag_file_path) + classpath = f"{bundle_path}/*" + return [ + "java", + "-classpath", + classpath, + BundleScanner.resolve_jar(jar_path), + f"--comm={comm_addr}", + f"--logs={logs_addr}", + ] + + # Case 2: Python Stub Dag — the task's ``language`` field is set + # (e.g. "java"). The actual JAR bundle lives in the provider's + # configured ``[java] bundles_folder``, not in the Dag bundle path. + from airflow.providers.common.compat.sdk import conf + + bundles_folder = conf.get("java", "bundles_folder", fallback=None) + if not bundles_folder: + raise ValueError( + "The [java] bundles_folder config must be set for Python stub DAGs " + "that delegate to Java task execution." + ) + + resolved = BundleScanner(Path(bundles_folder)).resolve(dag_id=what.dag_id) return [ "java", "-classpath", - classpath, - find_main_class(jar_path), + resolved.classpath, + resolved.main_class, f"--comm={comm_addr}", f"--logs={logs_addr}", ] diff --git a/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py b/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py index 887aef84fbb96..3546750084338 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py +++ b/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py @@ -29,5 +29,19 @@ def get_provider_info(): "integrations": [ {"integration-name": "Java", "external-doc-url": "https://openjdk.org/", "tags": ["software"]} ], + "config": { + "java": { + "description": "Options for the Java language provider.", + "options": { + "bundles_folder": { + "description": "Path to the directory containing Java DAG bundle JARs.\nWhen using Python stub DAGs that delegate task execution to Java,\nthe coordinator scans this directory to find the JAR bundle matching\nthe target dag_id. Each immediate subdirectory is treated as a\nseparate bundle home, and the directory itself is also checked\n(flat layout).\n", + "type": "string", + "version_added": None, + "example": "~/airflow/java-bundles", + "default": "", + } + }, + } + }, "process-coordinators": ["airflow.providers.languages.java.coordinator.JavaLocaleCoordinator"], } diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 8cb983b8242a1..214273e79a89f 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -2101,6 +2101,9 @@ def _resolve_locale_entrypoint(startup_details: StartupDetails, log: Logger) -> Otherwise return ``None`` to fall through to the standard Python execution path. """ + if TYPE_CHECKING: + from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator + language = startup_details.ti.language if language is None: return None @@ -2112,7 +2115,7 @@ def _resolve_locale_entrypoint(startup_details: StartupDetails, log: Logger) -> for coordinator_path in ProvidersManagerTaskRuntime().process_coordinators: try: - coordinator_cls = import_string(coordinator_path) + coordinator_cls: type[BaseLocaleCoordinator] = import_string(coordinator_path) except Exception: log.exception("Failed to import process coordinator", path=coordinator_path) continue From 8aad6b0d64354fa6b734fe6e5c122ea63b176260 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 16 Apr 2026 14:57:37 +0800 Subject: [PATCH 16/75] Add get_code_from_file interface for BaseLocaleCoordinator --- .../src/airflow/dag_processing/processor.py | 5 +++- airflow-core/src/airflow/models/dagcode.py | 20 +++++++++++++ .../languages/java/bundle_scanner.py | 30 +++++++++++++++++++ .../providers/languages/java/coordinator.py | 18 +++++++---- .../airflow/sdk/execution_time/coordinator.py | 17 +++++++++++ 5 files changed, 84 insertions(+), 6 deletions(-) diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index ea7b620f04416..c0627a6ccd5dc 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -610,6 +610,9 @@ def _resolve_processor_target( from airflow._shared.module_loading import import_string from airflow.providers_manager import ProvidersManager + if TYPE_CHECKING: + from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator + for coordinator_class_path in ProvidersManager().process_coordinators: try: log.debug( @@ -617,7 +620,7 @@ def _resolve_processor_target( coordinator_class_path, path, ) - coordinator_cls = import_string(coordinator_class_path) + coordinator_cls: type[BaseLocaleCoordinator] = import_string(coordinator_class_path) if coordinator_cls.can_handle_dag_file(bundle_name, path): log.debug( "Using process coordinator %s for file %s", diff --git a/airflow-core/src/airflow/models/dagcode.py b/airflow-core/src/airflow/models/dagcode.py index 60ee91c8b59b5..1898ce84fa44b 100644 --- a/airflow-core/src/airflow/models/dagcode.py +++ b/airflow-core/src/airflow/models/dagcode.py @@ -119,6 +119,26 @@ def code(cls, dag_id, session: Session = NEW_SESSION) -> str: @staticmethod def get_code_from_file(fileloc): + # Try from locale coordinator first + # TODO: define another field in ProvidersManager for pre-loaded coordinator + # So that we don't need to import every time in the runtime and could be reused + from airflow._shared.module_loading import import_string + from airflow.providers_manager import ProvidersManager + + if TYPE_CHECKING: + from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator + + if process_coordinator_paths := ProvidersManager().process_coordinators: + # short circuit for `process_coordinators` field + + for coordinator_class_path in process_coordinator_paths: + coordinator_cls: type[BaseLocaleCoordinator] = import_string(coordinator_class_path) + # TODO: Perhaps the `can_handle_dag_file` interface should just accept `path` only? + # Or maybe we can have different granularity for this. that 1 with bundle + path, another with just path + if coordinator_cls.can_handle_dag_file("", fileloc): + return coordinator_cls.get_code_from_file(fileloc) + + # Then fallback to python native try: with open_maybe_zipped(fileloc, "r") as f: code = f.read() diff --git a/providers/languages/java/src/airflow/providers/languages/java/bundle_scanner.py b/providers/languages/java/src/airflow/providers/languages/java/bundle_scanner.py index a53e6d99db578..16712ec524152 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/bundle_scanner.py +++ b/providers/languages/java/src/airflow/providers/languages/java/bundle_scanner.py @@ -36,6 +36,7 @@ _MANIFEST_PATH = "META-INF/MANIFEST.MF" METADATA_MANIFEST_KEY = "Airflow-Java-SDK-Metadata" SDK_VERSION_MANIFEST_KEY = "Airflow-Java-SDK-Version" +DAG_CODE_MANIFEST_KEY = "Airflow-Java-SDK-Dag-Code" MAIN_CLASS_MANIFEST_KEY = "Main-Class" @@ -182,6 +183,35 @@ def _read_bundle_jar(jar_path: Path) -> tuple[str, set[str]] | None: return main_class, dag_ids +def read_dag_code(jar_path: Path) -> str | None: + """ + Read the DAG source code embedded in a JAR bundle. + + Returns the source code string when the JAR carries a valid + ``Airflow-Java-SDK-Dag-Code`` manifest attribute pointing to an + embedded source file. Returns ``None`` otherwise. + """ + try: + with zipfile.ZipFile(jar_path) as zf: + try: + with zf.open(_MANIFEST_PATH) as f: + manifest = email.message_from_binary_file(f) + except KeyError: + return None + + dag_code_path = manifest.get(DAG_CODE_MANIFEST_KEY) + if not dag_code_path: + return None + + try: + with zf.open(dag_code_path) as f: + return f.read().decode() + except KeyError: + return None + except zipfile.BadZipFile: + return None + + def _parse_dag_ids_from_metadata(yaml_content: str) -> set[str]: """Parse dag IDs from an ``airflow-metadata.yaml`` content string.""" data = yaml.safe_load(yaml_content) diff --git a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py index 7af19d3273c36..7e5adde48e1f7 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py +++ b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py @@ -25,7 +25,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from airflow.providers.languages.java.bundle_scanner import BundleScanner +from airflow.providers.languages.java.bundle_scanner import BundleScanner, read_dag_code from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator if TYPE_CHECKING: @@ -44,6 +44,14 @@ def can_handle_dag_file(cls, bundle_name: str, path: str | os.PathLike[str]) -> return BundleScanner.resolve_jar(Path(path)) is not None return False + @classmethod + def get_code_from_file(cls, fileloc: str) -> str: + """Read embedded DAG source code from a JAR bundle.""" + code = read_dag_code(Path(fileloc)) + if code is None: + raise FileNotFoundError(f"No DAG source code found in JAR: {fileloc}") + return code + @classmethod def dag_parsing_locale_cmd( cls, @@ -82,7 +90,7 @@ def task_execution_locale_cmd( logs_addr: str, ) -> list[str]: """Build the ``java`` command for executing a task in a JAR bundle.""" - if what.language is None: + if dag_file_path.endswith(".jar"): # Case 1: Pure Java Dag — the dag_file_path points directly to a # bundle JAR inside the Airflow Core Dag Bundle. jar_path = Path(dag_file_path) @@ -96,9 +104,9 @@ def task_execution_locale_cmd( f"--logs={logs_addr}", ] - # Case 2: Python Stub Dag — the task's ``language`` field is set - # (e.g. "java"). The actual JAR bundle lives in the provider's - # configured ``[java] bundles_folder``, not in the Dag bundle path. + # Case 2: Python Stub Dag — the dag_file_path is a Python file but + # the task delegates to a Java runtime. The actual JAR bundle lives + # in the provider's configured ``[java] bundles_folder``. from airflow.providers.common.compat.sdk import conf bundles_folder = conf.get("java", "bundles_folder", fallback=None) diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 34f8ff7631800..5bac2b67619e7 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -213,6 +213,23 @@ def can_handle_dag_file(cls, bundle_name: str, path: str | os.PathLike[str]) -> """ return False + @classmethod + def get_code_from_file(cls, fileloc: str) -> str: + """ + Return the human-readable source code for a DAG file managed by this coordinator. + + Called by :class:`~airflow.models.dagcode.DagCode` when persisting DAG + source to the metadata database. The default Python path reads ``.py`` + files directly; locale coordinators must override this to extract source + from their native packaging format (e.g. reading an embedded ``.java`` + file from a JAR bundle). + + :param fileloc: Absolute path to the DAG file (e.g. a ``/path/to/example.jar``). + :return: The source code as a string. + :raises FileNotFoundError: If source code cannot be retrieved from *fileloc*. + """ + raise NotImplementedError + @classmethod def dag_parsing_locale_cmd( cls, From 487365c6948364407a6443ac168c829b72fd90ae Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 17 Apr 2026 13:10:53 +0800 Subject: [PATCH 17/75] Fix the 'Pure Java Dag' disappear in metadata DB issue --- .../src/airflow/dag_processing/manager.py | 30 ++++++++++++++++++- .../providers/languages/java/coordinator.py | 1 + .../airflow/sdk/execution_time/coordinator.py | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/dag_processing/manager.py b/airflow-core/src/airflow/dag_processing/manager.py index ed5c61c604c79..3f062055154c0 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -276,6 +276,9 @@ class DagFileProcessorManager(LoggingMixin): factory=_config_get_factory("dag_processor", "file_parsing_sort_mode") ) + _locale_file_extensions: tuple[str, ...] | None = attrs.field(default=None, init=False) + """File extensions registered by locale coordinators (e.g. ".jar"). Lazily populated.""" + _api_server: InProcessExecutionAPI = attrs.field(init=False, factory=InProcessExecutionAPI) """API server to interact with Metadata DB""" @@ -865,6 +868,27 @@ def _find_files_in_bundle(self, bundle: BaseDagBundle) -> list[Path]: return rel_paths + def _get_locale_file_extensions(self) -> tuple[str, ...]: + """Collect file extensions from registered locale coordinators (cached after first call).""" + if self._locale_file_extensions is not None: + return self._locale_file_extensions + + from airflow._shared.module_loading import import_string + from airflow.providers_manager import ProvidersManager + + if TYPE_CHECKING: + from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator + + extensions: list[str] = [] + for coordinator_class_path in ProvidersManager().process_coordinators: + try: + coordinator_cls: type[BaseLocaleCoordinator] = import_string(coordinator_class_path) + extensions.append(coordinator_cls.file_extension) + except Exception: + self.log.exception("Failed to load coordinator %s", coordinator_class_path) + self._locale_file_extensions = tuple(extensions) + return self._locale_file_extensions + def _get_observed_filelocs(self, present: set[DagFileInfo]) -> set[str]: """ Return observed DAG source paths for bundle entries. @@ -872,7 +896,11 @@ def _get_observed_filelocs(self, present: set[DagFileInfo]) -> set[str]: For regular files this includes the relative file path. For ZIP archives this includes DAG-like inner paths such as ``archive.zip/dag.py``. + + Locale coordinator file extensions (e.g. ``.jar``) are treated as + opaque files rather than ZIP archives. """ + locale_extensions = self._get_locale_file_extensions() def find_zipped_dags(abs_path: os.PathLike) -> Iterator[str]: """Yield absolute paths for DAG-like files inside a ZIP archive.""" @@ -887,7 +915,7 @@ def find_zipped_dags(abs_path: os.PathLike) -> Iterator[str]: observed_filelocs: set[str] = set() for info in present: abs_path = str(info.absolute_path) - if abs_path.endswith(".py") or not zipfile.is_zipfile(abs_path): + if abs_path.endswith((".py", *locale_extensions)) or not zipfile.is_zipfile(abs_path): observed_filelocs.add(str(info.rel_path)) else: if TYPE_CHECKING: diff --git a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py index 7e5adde48e1f7..1e2f83dd94426 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py +++ b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py @@ -36,6 +36,7 @@ class JavaLocaleCoordinator(BaseLocaleCoordinator): """Coordinator that launches a JVM subprocess for DAG parsing and task execution.""" locale_name = "java" + file_extension = ".jar" @classmethod def can_handle_dag_file(cls, bundle_name: str, path: str | os.PathLike[str]) -> bool: diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 5bac2b67619e7..799f62e2fc5e6 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -182,6 +182,7 @@ class BaseLocaleCoordinator: """ locale_name: str + file_extension: str class DagParsingInfo(NamedTuple): """Information needed for locale Dag parsing.""" From b07bb8290a752f5e4023d1f8f4a0e40dabc87576 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 17 Apr 2026 15:31:09 +0800 Subject: [PATCH 18/75] Refactor process coordinators to runtime coordinators - Renamed all instances of "process coordinators" to "runtime coordinators" in the codebase. - Updated the ProvidersManager and ProvidersManagerTaskRuntime classes to handle runtime coordinators. - Modified the DagFileProcessorManager to collect file extensions from runtime coordinators. - Adjusted the Java provider to implement the new runtime coordinator structure. - Updated tests to reflect changes from process to runtime coordinators. --- .../src/airflow/dag_processing/manager.py | 34 +++--- .../src/airflow/dag_processing/processor.py | 31 +++-- airflow-core/src/airflow/models/dagcode.py | 22 +--- .../src/airflow/provider.yaml.schema.json | 4 +- .../src/airflow/provider_info.schema.json | 4 +- airflow-core/src/airflow/providers_manager.py | 38 ++++--- .../unit/always/test_providers_manager.py | 20 ++-- providers/languages/java/docs/index.rst | 2 +- providers/languages/java/provider.yaml | 6 +- .../providers/languages/java/coordinator.py | 12 +- .../languages/java/get_provider_info.py | 4 +- .../unit/languages/java/test_java_provider.py | 10 +- .../airflow/sdk/execution_time/coordinator.py | 106 +++++++++--------- .../sdk/execution_time/task_coordinator.py | 20 ---- .../airflow/sdk/execution_time/task_runner.py | 28 ++--- .../airflow/sdk/providers_manager_runtime.py | 38 ++++--- .../test_providers_manager_runtime.py | 20 ++-- 17 files changed, 185 insertions(+), 214 deletions(-) delete mode 100644 task-sdk/src/airflow/sdk/execution_time/task_coordinator.py diff --git a/airflow-core/src/airflow/dag_processing/manager.py b/airflow-core/src/airflow/dag_processing/manager.py index 3f062055154c0..5234ad910a8d5 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -276,8 +276,8 @@ class DagFileProcessorManager(LoggingMixin): factory=_config_get_factory("dag_processor", "file_parsing_sort_mode") ) - _locale_file_extensions: tuple[str, ...] | None = attrs.field(default=None, init=False) - """File extensions registered by locale coordinators (e.g. ".jar"). Lazily populated.""" + _runtime_file_extensions: tuple[str, ...] | None = attrs.field(default=None, init=False) + """File extensions registered by runtime coordinators (e.g. ".jar"). Lazily populated.""" _api_server: InProcessExecutionAPI = attrs.field(init=False, factory=InProcessExecutionAPI) """API server to interact with Metadata DB""" @@ -868,26 +868,18 @@ def _find_files_in_bundle(self, bundle: BaseDagBundle) -> list[Path]: return rel_paths - def _get_locale_file_extensions(self) -> tuple[str, ...]: - """Collect file extensions from registered locale coordinators (cached after first call).""" - if self._locale_file_extensions is not None: - return self._locale_file_extensions + def _get_runtime_file_extensions(self) -> tuple[str, ...]: + """Collect file extensions from registered runtime coordinators (cached after first call).""" + if self._runtime_file_extensions is not None: + return self._runtime_file_extensions - from airflow._shared.module_loading import import_string from airflow.providers_manager import ProvidersManager - if TYPE_CHECKING: - from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator - extensions: list[str] = [] - for coordinator_class_path in ProvidersManager().process_coordinators: - try: - coordinator_cls: type[BaseLocaleCoordinator] = import_string(coordinator_class_path) - extensions.append(coordinator_cls.file_extension) - except Exception: - self.log.exception("Failed to load coordinator %s", coordinator_class_path) - self._locale_file_extensions = tuple(extensions) - return self._locale_file_extensions + for coordinator_cls in ProvidersManager().runtime_coordinators: + extensions.append(coordinator_cls.file_extension) + self._runtime_file_extensions = tuple(extensions) + return self._runtime_file_extensions def _get_observed_filelocs(self, present: set[DagFileInfo]) -> set[str]: """ @@ -897,10 +889,10 @@ def _get_observed_filelocs(self, present: set[DagFileInfo]) -> set[str]: For ZIP archives this includes DAG-like inner paths such as ``archive.zip/dag.py``. - Locale coordinator file extensions (e.g. ``.jar``) are treated as + Runtime coordinator file extensions (e.g. ``.jar``) are treated as opaque files rather than ZIP archives. """ - locale_extensions = self._get_locale_file_extensions() + runtime_extensions = self._get_runtime_file_extensions() def find_zipped_dags(abs_path: os.PathLike) -> Iterator[str]: """Yield absolute paths for DAG-like files inside a ZIP archive.""" @@ -915,7 +907,7 @@ def find_zipped_dags(abs_path: os.PathLike) -> Iterator[str]: observed_filelocs: set[str] = set() for info in present: abs_path = str(info.absolute_path) - if abs_path.endswith((".py", *locale_extensions)) or not zipfile.is_zipfile(abs_path): + if abs_path.endswith((".py", *runtime_extensions)) or not zipfile.is_zipfile(abs_path): observed_filelocs.add(str(info.rel_path)) else: if TYPE_CHECKING: diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index c0627a6ccd5dc..90f1caf5f5cb7 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -573,12 +573,12 @@ def start( # type: ignore[override] ) -> Self: logger = kwargs["logger"] - # Check if a provider-registered process coordinator should handle this file - logger.debug("Checking for provider-registered process coordinator entrypoint for file", path=path) + # Check if a provider-registered runtime coordinator should handle this file + logger.debug("Checking for provider-registered runtime coordinator entrypoint for file", path=path) resolved_target = cls._resolve_processor_target(path, bundle_name, bundle_path, logger) if resolved_target is not None: target = resolved_target - logger.debug("Resolved provider-registered process coordinator entrypoint for file", path=path) + logger.debug("Resolved provider-registered runtime coordinator entrypoint for file", path=path) else: _pre_import_airflow_modules(os.fspath(path), logger) @@ -601,30 +601,25 @@ def _resolve_processor_target( log: FilteringBoundLogger, ) -> Callable[[], None] | None: """ - Return the entrypoint of the first provider process coordinator that can handle *path*. + Return the entrypoint of the first provider runtime coordinator that can handle *path*. The returned callable is a ``functools.partial`` that binds *path*, *bundle_name* and *bundle_path* so the supervisor can pass it as a no-arg ``target`` to ``WatchedSubprocess.start``. """ - from airflow._shared.module_loading import import_string from airflow.providers_manager import ProvidersManager - if TYPE_CHECKING: - from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator - - for coordinator_class_path in ProvidersManager().process_coordinators: + for coordinator_cls in ProvidersManager().runtime_coordinators: try: log.debug( - "Checking process coordinator %s for file %s", - coordinator_class_path, + "Checking runtime coordinator %s for file %s", + coordinator_cls, path, ) - coordinator_cls: type[BaseLocaleCoordinator] = import_string(coordinator_class_path) if coordinator_cls.can_handle_dag_file(bundle_name, path): log.debug( - "Using process coordinator %s for file %s", - coordinator_class_path, + "Using runtime coordinator %s for file %s", + coordinator_cls, path, ) return functools.partial( @@ -634,15 +629,15 @@ def _resolve_processor_target( bundle_path=os.fspath(bundle_path), ) log.debug( - "Process coordinator %s cannot handle file %s with bundle name %s", - coordinator_class_path, + "Runtime coordinator %s cannot handle file %s with bundle name %s", + coordinator_cls, path, bundle_name, ) except Exception: - log.warning("Failed to load process coordinator %s", coordinator_class_path, exc_info=True) + log.warning("Failed to check runtime coordinator %s", coordinator_cls, exc_info=True) - log.debug("No process coordinator found for file %s, using default processor", path) + log.debug("No runtime coordinator found for file %s, using default processor", path) return None def _on_child_started( diff --git a/airflow-core/src/airflow/models/dagcode.py b/airflow-core/src/airflow/models/dagcode.py index 1898ce84fa44b..90b84a98404fb 100644 --- a/airflow-core/src/airflow/models/dagcode.py +++ b/airflow-core/src/airflow/models/dagcode.py @@ -119,24 +119,14 @@ def code(cls, dag_id, session: Session = NEW_SESSION) -> str: @staticmethod def get_code_from_file(fileloc): - # Try from locale coordinator first - # TODO: define another field in ProvidersManager for pre-loaded coordinator - # So that we don't need to import every time in the runtime and could be reused - from airflow._shared.module_loading import import_string + # Try from runtime coordinator first (classes are pre-loaded by ProvidersManager) from airflow.providers_manager import ProvidersManager - if TYPE_CHECKING: - from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator - - if process_coordinator_paths := ProvidersManager().process_coordinators: - # short circuit for `process_coordinators` field - - for coordinator_class_path in process_coordinator_paths: - coordinator_cls: type[BaseLocaleCoordinator] = import_string(coordinator_class_path) - # TODO: Perhaps the `can_handle_dag_file` interface should just accept `path` only? - # Or maybe we can have different granularity for this. that 1 with bundle + path, another with just path - if coordinator_cls.can_handle_dag_file("", fileloc): - return coordinator_cls.get_code_from_file(fileloc) + for coordinator_cls in ProvidersManager().runtime_coordinators: + # TODO: Perhaps the `can_handle_dag_file` interface should just accept `path` only? + # Or maybe we can have different granularity for this. that 1 with bundle + path, another with just path + if coordinator_cls.can_handle_dag_file("", fileloc): + return coordinator_cls.get_code_from_file(fileloc) # Then fallback to python native try: diff --git a/airflow-core/src/airflow/provider.yaml.schema.json b/airflow-core/src/airflow/provider.yaml.schema.json index 4f0143d65cda6..50336f05cb945 100644 --- a/airflow-core/src/airflow/provider.yaml.schema.json +++ b/airflow-core/src/airflow/provider.yaml.schema.json @@ -624,9 +624,9 @@ } } }, - "process-coordinators": { + "runtime-coordinators": { "type": "array", - "description": "Process Coordinator class names (BaseLocaleCoordinator subclasses)", + "description": "Runtime Coordinator class names (BaseRuntimeCoordinator subclasses)", "items": { "type": "string" } diff --git a/airflow-core/src/airflow/provider_info.schema.json b/airflow-core/src/airflow/provider_info.schema.json index 26cccdc5efa87..45cc92cba276f 100644 --- a/airflow-core/src/airflow/provider_info.schema.json +++ b/airflow-core/src/airflow/provider_info.schema.json @@ -447,9 +447,9 @@ } } }, - "process-coordinators": { + "runtime-coordinators": { "type": "array", - "description": "Process Coordinator class names (BaseLocaleCoordinator subclasses)", + "description": "Runtime Coordinator class names (BaseRuntimeCoordinator subclasses)", "items": { "type": "string" } diff --git a/airflow-core/src/airflow/providers_manager.py b/airflow-core/src/airflow/providers_manager.py index 6f08c06474ecb..123fe502273fc 100644 --- a/airflow-core/src/airflow/providers_manager.py +++ b/airflow-core/src/airflow/providers_manager.py @@ -41,6 +41,7 @@ if TYPE_CHECKING: from airflow.cli.cli_config import CLICommand + from airflow.sdk.execution_time.coordinator import BaseRuntimeCoordinator log = logging.getLogger(__name__) @@ -448,7 +449,7 @@ def __init__(self): ) # Set of plugins contained in providers self._plugins_set: set[PluginInfo] = set() - self._process_coordinators: list[str] = [] + self._runtime_coordinators: list[type[BaseRuntimeCoordinator]] = [] self._init_airflow_core_hooks() self._runtime_manager = None @@ -626,11 +627,11 @@ def initialize_providers_configuration(self): self.initialize_providers_list() self._discover_config() - @provider_info_cache("process_coordinators") - def initialize_providers_process_coordinators(self): - """Lazy initialization of providers process coordinators.""" + @provider_info_cache("runtime_coordinators") + def initialize_providers_runtime_coordinators(self): + """Lazy initialization of providers runtime coordinators.""" self.initialize_providers_list() - self._discover_process_coordinators() + self._discover_runtime_coordinators() @provider_info_cache("plugins") def initialize_providers_plugins(self): @@ -1287,13 +1288,18 @@ def _discover_config(self) -> None: if provider.data.get("config"): self._provider_configs[provider_package] = provider.data.get("config") # type: ignore[assignment] - def _discover_process_coordinators(self) -> None: - """Retrieve all process coordinators defined in the providers.""" + def _discover_runtime_coordinators(self) -> None: + """Retrieve and pre-load all runtime coordinators defined in the providers.""" + seen: set[str] = set() for provider_package, provider in self._provider_dict.items(): - for coordinator_class_path in provider.data.get("process-coordinators", []): - if _correctness_check(provider_package, coordinator_class_path, provider): - self._process_coordinators.append(coordinator_class_path) - self._process_coordinators = sorted(set(self._process_coordinators)) + for coordinator_class_path in provider.data.get("runtime-coordinators", []): + if coordinator_class_path in seen: + continue + coordinator_cls = _correctness_check(provider_package, coordinator_class_path, provider) + if coordinator_cls: + seen.add(coordinator_class_path) + self._runtime_coordinators.append(coordinator_cls) + self._runtime_coordinators = sorted(self._runtime_coordinators, key=lambda c: c.__qualname__) def _discover_plugins(self) -> None: """Retrieve all plugins defined in the providers.""" @@ -1493,10 +1499,10 @@ def db_managers(self) -> list[str]: return sorted(self._db_manager_class_name_set) @property - def process_coordinators(self) -> list[str]: - """Returns process coordinator class paths available in providers.""" - self.initialize_providers_process_coordinators() - return self._process_coordinators + def runtime_coordinators(self) -> list[type[BaseRuntimeCoordinator]]: + """Returns pre-loaded runtime coordinator classes available in providers.""" + self.initialize_providers_runtime_coordinators() + return self._runtime_coordinators @property def filesystem_module_names(self) -> list[str]: @@ -1569,7 +1575,7 @@ def _cleanup(self): self._trigger_info_set.clear() self._notification_info_set.clear() self._plugins_set.clear() - self._process_coordinators.clear() + self._runtime_coordinators.clear() self._cli_command_functions_set.clear() self._cli_command_provider_name_set.clear() diff --git a/airflow-core/tests/unit/always/test_providers_manager.py b/airflow-core/tests/unit/always/test_providers_manager.py index 8db5a1de0e227..2bc51c993e0fe 100644 --- a/airflow-core/tests/unit/always/test_providers_manager.py +++ b/airflow-core/tests/unit/always/test_providers_manager.py @@ -259,14 +259,23 @@ def test_dialects(self): assert dialect_class_names == ["default", "mssql", "postgresql"] @patch("airflow.providers_manager.import_string") - def test_process_coordinators(self, mock_import_string): - mock_import_string.return_value = object() + def test_runtime_coordinators(self, mock_import_string): + class ACoordinator: + pass + + class ZCoordinator: + pass + + mock_import_string.side_effect = lambda path: { + "airflow.providers.languages.java.coordinator.ACoordinator": ACoordinator, + "airflow.providers.languages.java.coordinator.ZCoordinator": ZCoordinator, + }[path] providers_manager = ProvidersManager() providers_manager._provider_dict = LazyDictWithCache() providers_manager._provider_dict["apache-airflow-providers-languages-java"] = ProviderInfo( version="0.0.1", data={ - "process-coordinators": [ + "runtime-coordinators": [ "airflow.providers.languages.java.coordinator.ZCoordinator", "airflow.providers.languages.java.coordinator.ACoordinator", "airflow.providers.languages.java.coordinator.ZCoordinator", @@ -275,10 +284,7 @@ def test_process_coordinators(self, mock_import_string): ) with patch.object(providers_manager, "initialize_providers_list"): - assert providers_manager.process_coordinators == [ - "airflow.providers.languages.java.coordinator.ACoordinator", - "airflow.providers.languages.java.coordinator.ZCoordinator", - ] + assert providers_manager.runtime_coordinators == [ACoordinator, ZCoordinator] class TestWithoutCheckProviderManager: diff --git a/providers/languages/java/docs/index.rst b/providers/languages/java/docs/index.rst index 482210c2706c8..cccaf3c8d3f2d 100644 --- a/providers/languages/java/docs/index.rst +++ b/providers/languages/java/docs/index.rst @@ -58,7 +58,7 @@ The Languages: Java provider registers Java-specific task coordinator and DAG fi apache-airflow-providers-languages-java package ------------------------------------------------------ -Java language support for Apache Airflow process coordinators. +Java language support for Apache Airflow runtime coordinators. Release: 0.1.0 diff --git a/providers/languages/java/provider.yaml b/providers/languages/java/provider.yaml index b9020b0d38af7..5153042da02eb 100644 --- a/providers/languages/java/provider.yaml +++ b/providers/languages/java/provider.yaml @@ -19,7 +19,7 @@ package-name: apache-airflow-providers-languages-java name: "Languages: Java" description: | - Java language support for Apache Airflow process coordinators. + Java language support for Apache Airflow runtime coordinators. state: ready lifecycle: incubation @@ -54,5 +54,5 @@ config: example: ~/airflow/java-bundles default: "" -process-coordinators: - - airflow.providers.languages.java.coordinator.JavaLocaleCoordinator +runtime-coordinators: + - airflow.providers.languages.java.coordinator.JavaRuntimeCoordinator diff --git a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py index 1e2f83dd94426..3a5b80875c7e8 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py +++ b/providers/languages/java/src/airflow/providers/languages/java/coordinator.py @@ -15,7 +15,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -"""Java locale coordinator that launches a JVM subprocess for Dag file processing and task execution.""" +"""Java runtime coordinator that launches a JVM subprocess for Dag file processing and task execution.""" from __future__ import annotations @@ -26,16 +26,16 @@ from typing import TYPE_CHECKING from airflow.providers.languages.java.bundle_scanner import BundleScanner, read_dag_code -from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator +from airflow.sdk.execution_time.coordinator import BaseRuntimeCoordinator if TYPE_CHECKING: from airflow.sdk.api.datamodels._generated import BundleInfo, TaskInstance -class JavaLocaleCoordinator(BaseLocaleCoordinator): +class JavaRuntimeCoordinator(BaseRuntimeCoordinator): """Coordinator that launches a JVM subprocess for DAG parsing and task execution.""" - locale_name = "java" + runtime_name = "java" file_extension = ".jar" @classmethod @@ -54,7 +54,7 @@ def get_code_from_file(cls, fileloc: str) -> str: return code @classmethod - def dag_parsing_locale_cmd( + def dag_parsing_runtime_cmd( cls, *, dag_file_path: str, @@ -80,7 +80,7 @@ def dag_parsing_locale_cmd( ] @classmethod - def task_execution_locale_cmd( + def task_execution_runtime_cmd( cls, *, what: TaskInstance, diff --git a/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py b/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py index 3546750084338..dc0cb5ed67c41 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py +++ b/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py @@ -25,7 +25,7 @@ def get_provider_info(): return { "package-name": "apache-airflow-providers-languages-java", "name": "Languages: Java", - "description": "Java language support for Apache Airflow process coordinators.\n", + "description": "Java language support for Apache Airflow runtime coordinators.\n", "integrations": [ {"integration-name": "Java", "external-doc-url": "https://openjdk.org/", "tags": ["software"]} ], @@ -43,5 +43,5 @@ def get_provider_info(): }, } }, - "process-coordinators": ["airflow.providers.languages.java.coordinator.JavaLocaleCoordinator"], + "runtime-coordinators": ["airflow.providers.languages.java.coordinator.JavaRuntimeCoordinator"], } diff --git a/providers/languages/java/tests/unit/languages/java/test_java_provider.py b/providers/languages/java/tests/unit/languages/java/test_java_provider.py index 859fa073a9ff4..782373158ce92 100644 --- a/providers/languages/java/tests/unit/languages/java/test_java_provider.py +++ b/providers/languages/java/tests/unit/languages/java/test_java_provider.py @@ -17,7 +17,7 @@ # under the License. from __future__ import annotations -from airflow.providers.languages.java.coordinator import JavaLocaleCoordinator +from airflow.providers.languages.java.coordinator import JavaRuntimeCoordinator from airflow.providers.languages.java.get_provider_info import get_provider_info @@ -25,7 +25,7 @@ def test_get_provider_info_exposes_java_runtime_components(): assert get_provider_info() == { "package-name": "apache-airflow-providers-languages-java", "name": "Languages: Java", - "description": "Java language support for Apache Airflow process coordinators.\n", + "description": "Java language support for Apache Airflow runtime coordinators.\n", "integrations": [ { "integration-name": "Java", @@ -33,11 +33,11 @@ def test_get_provider_info_exposes_java_runtime_components(): "tags": ["software"], } ], - "process-coordinators": [ - "airflow.providers.languages.java.coordinator.JavaLocaleCoordinator", + "runtime-coordinators": [ + "airflow.providers.languages.java.coordinator.JavaRuntimeCoordinator", ], } def test_java_provider_entrypoints_are_importable(): - assert JavaLocaleCoordinator.locale_name == "java" + assert JavaRuntimeCoordinator.runtime_name == "java" diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 799f62e2fc5e6..cb48295b300f1 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -16,17 +16,17 @@ # specific language governing permissions and limitations # under the License. """ -Locale coordinator for non-Python DAG file processing and task execution. +Runtime coordinator for non-Python DAG file processing and task execution. -Provides :class:`BaseLocaleCoordinator`, the base class for +Provides :class:`BaseRuntimeCoordinator`, the base class for language-specific coordinators that bridge subprocess I/O between the Airflow supervisor and an external-language runtime (Java, Go, Rust, etc.). -The coordinator's :meth:`~BaseLocaleCoordinator.run_dag_parsing` method +The coordinator's :meth:`~BaseRuntimeCoordinator.run_dag_parsing` method handles the full lifecycle: 1. Creates TCP servers for comm and logs channels. -2. Calls :meth:`~BaseLocaleCoordinator.dag_parsing_locale_cmd` (provided +2. Calls :meth:`~BaseRuntimeCoordinator.dag_parsing_runtime_cmd` (provided by the subclass) to obtain the subprocess command. 3. Spawns the subprocess and accepts TCP connections from it. 4. Runs a selector-based bridge that transparently forwards bytes @@ -64,14 +64,14 @@ def _start_server() -> socket.socket: return server -def _send_startup_details(locale_comm: socket.socket, startup_details: StartupDetails) -> None: +def _send_startup_details(runtime_comm: socket.socket, startup_details: StartupDetails) -> None: """ - Re-encode and send the ``StartupDetails`` frame to the locale subprocess. + Re-encode and send the ``StartupDetails`` frame to the runtime subprocess. In the task execution flow, ``task_runner.main()`` consumes the ``StartupDetails`` message from fd 0 (to determine routing) before - delegating to the locale coordinator. This function re-serializes - the message and writes it to the locale subprocess's comm socket so + delegating to the runtime coordinator. This function re-serializes + the message and writes it to the runtime subprocess's comm socket so the subprocess receives it as if it came directly from the supervisor. """ from airflow.sdk.execution_time.comms import _ResponseFrame @@ -81,26 +81,26 @@ def _send_startup_details(locale_comm: socket.socket, startup_details: StartupDe # msgpack extension types (e.g. Timestamp) that non-Python decoders # may not support. frame = _ResponseFrame(id=0, body=startup_details.model_dump(mode="json")) - locale_comm.sendall(frame.as_bytes()) + runtime_comm.sendall(frame.as_bytes()) def _bridge( supervisor_comm: socket.socket, - locale_comm: socket.socket, - locale_logs: socket.socket, - locale_stderr: socket.socket, + runtime_comm: socket.socket, + runtime_logs: socket.socket, + runtime_stderr: socket.socket, proc: subprocess.Popen, log: FilteringBoundLogger, ) -> None: """ - Multiplex I/O between the supervisor and a locale subprocess. + Multiplex I/O between the supervisor and a runtime subprocess. Four channels are registered with the selector: - - ``supervisor_comm`` -> ``locale_comm`` (raw byte forwarding) - - ``locale_comm`` -> ``supervisor_comm`` (raw byte forwarding) - - ``locale_logs`` -> structlog (line-buffered JSON logs) - - ``locale_stderr`` -> structlog (line-buffered stderr output) + - ``supervisor_comm`` -> ``runtime_comm`` (raw byte forwarding) + - ``runtime_comm`` -> ``supervisor_comm`` (raw byte forwarding) + - ``runtime_logs`` -> structlog (line-buffered JSON logs) + - ``runtime_stderr`` -> structlog (line-buffered stderr output) Uses the same ``(handler, on_close)`` callback contract as :class:`~airflow.sdk.execution_time.supervisor.WatchedSubprocess`, @@ -125,24 +125,24 @@ def on_close(sock: socket.socket) -> None: target_loggers = (log,) # Comm: bidirectional raw byte forwarding. - sel.register(supervisor_comm, selectors.EVENT_READ, make_raw_forwarder(locale_comm, on_close)) - sel.register(locale_comm, selectors.EVENT_READ, make_raw_forwarder(supervisor_comm, on_close)) + sel.register(supervisor_comm, selectors.EVENT_READ, make_raw_forwarder(runtime_comm, on_close)) + sel.register(runtime_comm, selectors.EVENT_READ, make_raw_forwarder(supervisor_comm, on_close)) - # TCP logs channel: line-buffered JSON from the locale SDK's LogSender, + # TCP logs channel: line-buffered JSON from the runtime SDK's LogSender, # processed with the same handler as WatchedSubprocess (level mapping, # timestamp parsing, exception extraction). sel.register( - locale_logs, + runtime_logs, selectors.EVENT_READ, make_buffered_socket_reader(process_log_messages_from_subprocess(target_loggers), on_close), ) - # stderr: plain-text output from the locale process's logging framework + # stderr: plain-text output from the runtime process's logging framework # (e.g. SLF4J simple logger). Use forward_to_log which handles raw # text lines, not process_log_messages_from_subprocess which expects JSON. import logging sel.register( - locale_stderr, + runtime_stderr, selectors.EVENT_READ, make_buffered_socket_reader( forward_to_log(target_loggers, logger="task.stderr", level=logging.ERROR), on_close @@ -160,32 +160,32 @@ def on_close(sock: socket.socket) -> None: break sel.close() - for sock in (supervisor_comm, locale_comm, locale_logs, locale_stderr): + for sock in (supervisor_comm, runtime_comm, runtime_logs, runtime_stderr): with contextlib.suppress(OSError): sock.close() -class BaseLocaleCoordinator: +class BaseRuntimeCoordinator: """ - Base coordinator for locale-specific DAG file processing and task execution. + Base coordinator for runtime-specific DAG file processing and task execution. Providers register subclasses in their ``provider.yaml`` under - ``process-coordinators``. Both :class:`ProvidersManager` (airflow-core) + ``runtime-coordinators``. Both :class:`ProvidersManager` (airflow-core) and :class:`ProvidersManagerTaskRuntime` (task-sdk) discover registered coordinators through this single extension point. Subclasses represent a specific language runtime (Java, Go, etc.) and only need to implement :meth:`can_handle_dag_file`, - :meth:`dag_parsing_locale_cmd` and :meth:`task_execution_locale_cmd`. + :meth:`dag_parsing_runtime_cmd` and :meth:`task_execution_runtime_cmd`. The base class owns the entire bridge lifecycle: TCP servers, subprocess management, selector-based I/O loop, and cleanup. """ - locale_name: str + runtime_name: str file_extension: str class DagParsingInfo(NamedTuple): - """Information needed for locale Dag parsing.""" + """Information needed for runtime Dag parsing.""" dag_file_path: str bundle_name: str @@ -193,7 +193,7 @@ class DagParsingInfo(NamedTuple): mode: str = "dag-parsing" class TaskExecutionInfo(NamedTuple): - """Information needed for locale task execution.""" + """Information needed for runtime task execution.""" what: TaskInstance dag_rel_path: str | os.PathLike[str] @@ -221,7 +221,7 @@ def get_code_from_file(cls, fileloc: str) -> str: Called by :class:`~airflow.models.dagcode.DagCode` when persisting DAG source to the metadata database. The default Python path reads ``.py`` - files directly; locale coordinators must override this to extract source + files directly; runtime coordinators must override this to extract source from their native packaging format (e.g. reading an embedded ``.java`` file from a JAR bundle). @@ -232,7 +232,7 @@ def get_code_from_file(cls, fileloc: str) -> str: raise NotImplementedError @classmethod - def dag_parsing_locale_cmd( + def dag_parsing_runtime_cmd( cls, *, dag_file_path: str, @@ -251,12 +251,12 @@ def dag_parsing_locale_cmd( for the bidirectional msgpack comm channel. :param logs_addr: ``host:port`` the subprocess must connect to for the structured JSON log channel. - :returns: Full command list (e.g. ``["java", "-cp", "...", ...]`` based on each locale). + :returns: Full command list (e.g. ``["java", "-cp", "...", ...]`` based on each runtime). """ raise NotImplementedError @classmethod - def task_execution_locale_cmd( + def task_execution_runtime_cmd( cls, *, what: TaskInstance, @@ -283,8 +283,8 @@ def task_execution_locale_cmd( @classmethod def run_dag_parsing(cls, *, path: str, bundle_name: str, bundle_path: str) -> None: - """Entry point for running locale-specific Dag File Processing.""" - cls._locale_subprocess_entrypoint( + """Entry point for running runtime-specific Dag File Processing.""" + cls._runtime_subprocess_entrypoint( cls.DagParsingInfo( dag_file_path=path, bundle_name=bundle_name, @@ -301,7 +301,7 @@ def run_task_execution( bundle_info: BundleInfo, startup_details: StartupDetails, ) -> None: - cls._locale_subprocess_entrypoint( + cls._runtime_subprocess_entrypoint( cls.TaskExecutionInfo( what=what, dag_rel_path=dag_rel_path, @@ -311,15 +311,15 @@ def run_task_execution( ) @classmethod - def _locale_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskExecutionInfo) -> None: + def _runtime_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskExecutionInfo) -> None: """ - Spawn the locale subprocess and bridge I/O with the supervisor. + Spawn the runtime subprocess and bridge I/O with the supervisor. This is called inside the forked child process where fd 0 is the bidirectional comms socket to the supervisor. The method: 1. Creates TCP servers for comm and logs. - 2. Calls :meth:`dag_parsing_locale_cmd` or :meth:`task_execution_locale_cmd` to get the command. + 2. Calls :meth:`dag_parsing_runtime_cmd` or :meth:`task_execution_runtime_cmd` to get the command. 3. Spawns the subprocess with ``stdin=/dev/null`` and stderr captured via a socketpair. 4. Runs the selector-based bridge until the subprocess exits. @@ -340,12 +340,12 @@ def _locale_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskExe log = structlog.get_logger(logger_name="task") log.info( - "Starting locale subprocess", - locale=cls.locale_name, + "Starting runtime subprocess", + runtime=cls.runtime_name, mode=entrypoint_info.mode, ) - # TCP servers for the locale subprocess to connect to. + # TCP servers for the runtime subprocess to connect to. comm_server = _start_server() logs_server = _start_server() comm_host, comm_port = comm_server.getsockname() @@ -360,11 +360,11 @@ def _locale_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskExe # For task execution, hold a BundleVersionLock for the entire # subprocess lifetime to prevent the bundle version from being - # garbage-collected while the locale process is still running. + # garbage-collected while the runtime process is still running. bundle_version_lock: contextlib.AbstractContextManager = contextlib.nullcontext() if isinstance(entrypoint_info, cls.DagParsingInfo): - cmd = cls.dag_parsing_locale_cmd( + cmd = cls.dag_parsing_runtime_cmd( dag_file_path=entrypoint_info.dag_file_path, bundle_name=entrypoint_info.bundle_name, bundle_path=entrypoint_info.bundle_path, @@ -384,7 +384,7 @@ def _locale_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskExe resolved_bundle_path = str(bundle_instance.path) resolved_dag_file_path = os.fspath(Path(bundle_instance.path, entrypoint_info.dag_rel_path)) - cmd = cls.task_execution_locale_cmd( + cmd = cls.task_execution_runtime_cmd( what=entrypoint_info.what, dag_file_path=resolved_dag_file_path, bundle_path=resolved_bundle_path, @@ -410,22 +410,22 @@ def _locale_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskExe child_stderr.close() # Wait for the subprocess to connect to both servers. - locale_comm, _ = comm_server.accept() - locale_logs, _ = logs_server.accept() + runtime_comm, _ = comm_server.accept() + runtime_logs, _ = logs_server.accept() comm_server.close() logs_server.close() # For task execution the supervisor already sent ``StartupDetails`` # on fd 0 and ``task_runner.main()`` consumed it before delegating - # here. Re-encode and forward it to the locale subprocess so it + # here. Re-encode and forward it to the runtime subprocess so it # knows which task to execute. if isinstance(entrypoint_info, cls.TaskExecutionInfo): - _send_startup_details(locale_comm, entrypoint_info.startup_details) + _send_startup_details(runtime_comm, entrypoint_info.startup_details) # fd 0 is the bidirectional comms socket to the supervisor. supervisor_comm = socket.socket(fileno=os.dup(0)) - _bridge(supervisor_comm, locale_comm, locale_logs, read_stderr, proc, log) + _bridge(supervisor_comm, runtime_comm, runtime_logs, read_stderr, proc, log) -__all__ = ["BaseLocaleCoordinator"] +__all__ = ["BaseRuntimeCoordinator"] diff --git a/task-sdk/src/airflow/sdk/execution_time/task_coordinator.py b/task-sdk/src/airflow/sdk/execution_time/task_coordinator.py deleted file mode 100644 index d418b2b384b79..0000000000000 --- a/task-sdk/src/airflow/sdk/execution_time/task_coordinator.py +++ /dev/null @@ -1,20 +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. -"""Task Coordinator for Airflow Task Execution Time SDK.""" - -from __future__ import annotations diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 214273e79a89f..d0a4af14c3874 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -2092,43 +2092,33 @@ def flush_spans(): provider.force_flush(timeout_millis=timeout_millis) -def _resolve_locale_entrypoint(startup_details: StartupDetails, log: Logger) -> Callable[[], None] | None: +def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> Callable[[], None] | None: """ - Check provider-registered process coordinators for a locale-specific entrypoint. + Check provider-registered runtime coordinators for a runtime-specific entrypoint. - If the task's ``language`` field matches a coordinator's ``locale_name``, - return a no-arg callable that bridges fd 0 to the locale subprocess. + If the task's ``language`` field matches a coordinator's ``runtime_name``, + return a no-arg callable that bridges fd 0 to the runtime subprocess. Otherwise return ``None`` to fall through to the standard Python execution path. """ - if TYPE_CHECKING: - from airflow.sdk.execution_time.coordinator import BaseLocaleCoordinator - language = startup_details.ti.language if language is None: return None import functools - from airflow._shared.module_loading import import_string from airflow.sdk.providers_manager_runtime import ProvidersManagerTaskRuntime - for coordinator_path in ProvidersManagerTaskRuntime().process_coordinators: - try: - coordinator_cls: type[BaseLocaleCoordinator] = import_string(coordinator_path) - except Exception: - log.exception("Failed to import process coordinator", path=coordinator_path) - continue - + for coordinator_cls in ProvidersManagerTaskRuntime().runtime_coordinators: if not hasattr(coordinator_cls, "run_task_execution"): continue - if getattr(coordinator_cls, "locale_name", None) != language: + if getattr(coordinator_cls, "runtime_name", None) != language: continue log.debug( - "Resolved locale-specific entrypoint for task", - coordinator=coordinator_path, + "Resolved runtime-specific entrypoint for task", + coordinator=coordinator_cls, language=language, task_id=startup_details.ti.task_id, ) @@ -2141,7 +2131,7 @@ def _resolve_locale_entrypoint(startup_details: StartupDetails, log: Logger) -> ) log.warning( - "No process coordinator found for language", + "No runtime coordinator found for language", language=language, task_id=startup_details.ti.task_id, ) diff --git a/task-sdk/src/airflow/sdk/providers_manager_runtime.py b/task-sdk/src/airflow/sdk/providers_manager_runtime.py index 182eb06b5e36e..b15c477f40779 100644 --- a/task-sdk/src/airflow/sdk/providers_manager_runtime.py +++ b/task-sdk/src/airflow/sdk/providers_manager_runtime.py @@ -51,6 +51,7 @@ from airflow.sdk import BaseHook from airflow.sdk.bases.decorator import TaskDecorator from airflow.sdk.definitions.asset import Asset + from airflow.sdk.execution_time.coordinator import BaseRuntimeCoordinator log = structlog.getLogger(__name__) @@ -150,7 +151,7 @@ def __init__(self): # Keeps dict of hooks keyed by connection type. They are lazy evaluated at access time self._hooks_lazy_dict: LazyDictWithCache[str, HookInfo | Callable] = LazyDictWithCache() self._plugins_set: set[PluginInfo] = set() - self._process_coordinators: list[str] = [] + self._runtime_coordinators: list[type[BaseRuntimeCoordinator]] = [] self._provider_schema_validator = _create_provider_info_schema_validator() self._init_airflow_core_hooks() # Populated by initialize_provider_configs(); holds provider-contributed config sections. @@ -221,11 +222,11 @@ def initialize_providers_taskflow_decorator(self): self.initialize_providers_list() self._discover_taskflow_decorators() - @provider_info_cache("process_coordinators") - def initialize_providers_process_coordinators(self): - """Lazy initialization of providers process coordinators.""" + @provider_info_cache("runtime_coordinators") + def initialize_providers_runtime_coordinators(self): + """Lazy initialization of providers runtime coordinators.""" self.initialize_providers_list() - self._discover_process_coordinators() + self._discover_runtime_coordinators() @provider_info_cache("provider_configs") def initialize_provider_configs(self): @@ -471,13 +472,18 @@ def _import_hook( connection_testable=hasattr(hook_class, "test_connection"), ) - def _discover_process_coordinators(self) -> None: - """Retrieve all process coordinators defined in the providers.""" + def _discover_runtime_coordinators(self) -> None: + """Retrieve and pre-load all runtime coordinators defined in the providers.""" + seen: set[str] = set() for provider_package, provider in self._provider_dict.items(): - for coordinator_class_path in provider.data.get("process-coordinators", []): - if _correctness_check(provider_package, coordinator_class_path, provider): - self._process_coordinators.append(coordinator_class_path) - self._process_coordinators = sorted(set(self._process_coordinators)) + for coordinator_class_path in provider.data.get("runtime-coordinators", []): + if coordinator_class_path in seen: + continue + coordinator_cls = _correctness_check(provider_package, coordinator_class_path, provider) + if coordinator_cls: + seen.add(coordinator_class_path) + self._runtime_coordinators.append(coordinator_cls) + self._runtime_coordinators = sorted(self._runtime_coordinators, key=lambda c: c.__qualname__) def _discover_filesystems(self) -> None: """Retrieve all filesystems defined in the providers.""" @@ -627,10 +633,10 @@ def plugins(self) -> list[PluginInfo]: return sorted(self._plugins_set, key=lambda x: x.plugin_class) @property - def process_coordinators(self) -> list[str]: - """Returns process coordinator class paths available in providers.""" - self.initialize_providers_process_coordinators() - return self._process_coordinators + def runtime_coordinators(self) -> list[type[BaseRuntimeCoordinator]]: + """Returns pre-loaded runtime coordinator classes available in providers.""" + self.initialize_providers_runtime_coordinators() + return self._runtime_coordinators @property def provider_configs(self) -> list[tuple[str, dict[str, Any]]]: @@ -664,7 +670,7 @@ def _cleanup(self): self._asset_uri_handlers.clear() self._asset_factories.clear() self._asset_to_openlineage_converters.clear() - self._process_coordinators.clear() + self._runtime_coordinators.clear() self._provider_configs.clear() # Imported lazily to preserve SDK conf lazy initialization and avoid a configuration/runtime cycle. diff --git a/task-sdk/tests/task_sdk/test_providers_manager_runtime.py b/task-sdk/tests/task_sdk/test_providers_manager_runtime.py index f73d723174946..b7eff9712e4b7 100644 --- a/task-sdk/tests/task_sdk/test_providers_manager_runtime.py +++ b/task-sdk/tests/task_sdk/test_providers_manager_runtime.py @@ -244,13 +244,22 @@ def test_already_initialized_provider_configs_emits_deprecation_warning(self): pm.already_initialized_provider_configs @patch("airflow.sdk.providers_manager_runtime.import_string") - def test_process_coordinators(self, mock_import_string): - mock_import_string.return_value = object() + def test_runtime_coordinators(self, mock_import_string): + class ACoordinator: + pass + + class ZCoordinator: + pass + + mock_import_string.side_effect = lambda path: { + "airflow.providers.languages.java.coordinator.ACoordinator": ACoordinator, + "airflow.providers.languages.java.coordinator.ZCoordinator": ZCoordinator, + }[path] providers_manager = ProvidersManagerTaskRuntime() providers_manager._provider_dict["apache-airflow-providers-languages-java"] = ProviderInfo( version="0.0.1", data={ - "process-coordinators": [ + "runtime-coordinators": [ "airflow.providers.languages.java.coordinator.ZCoordinator", "airflow.providers.languages.java.coordinator.ACoordinator", "airflow.providers.languages.java.coordinator.ZCoordinator", @@ -259,10 +268,7 @@ def test_process_coordinators(self, mock_import_string): ) with patch.object(providers_manager, "initialize_providers_list"): - assert providers_manager.process_coordinators == [ - "airflow.providers.languages.java.coordinator.ACoordinator", - "airflow.providers.languages.java.coordinator.ZCoordinator", - ] + assert providers_manager.runtime_coordinators == [ACoordinator, ZCoordinator] def test_initialize_provider_configs_can_reload_sdk_conf(self): from airflow.sdk.configuration import conf From f491fe9a8c17af8457d6aa3f38e75e31f92b0147 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 10:44:18 +0800 Subject: [PATCH 19/75] Rename stub operator language field as sdk --- .../execution_api/datamodels/taskinstance.py | 2 +- .../api_fastapi/execution_api/versions/__init__.py | 2 -- .../execution_api/versions/v2026_07_01.py | 6 +++--- .../src/airflow/executors/workloads/task.py | 2 +- ...> 0112_3_3_0_add_sdk_field_to_task_instance.py} | 10 +++++----- airflow-core/src/airflow/models/taskinstance.py | 6 +++--- .../serialization/definitions/baseoperator.py | 2 +- .../airflow/providers/standard/decorators/stub.py | 8 ++++---- .../src/airflow/sdk/api/datamodels/_generated.py | 2 +- task-sdk/src/airflow/sdk/bases/operator.py | 6 +++--- .../src/airflow/sdk/definitions/mappedoperator.py | 4 ++-- .../src/airflow/sdk/execution_time/coordinator.py | 6 +++--- .../src/airflow/sdk/execution_time/task_runner.py | 14 +++++++------- 13 files changed, 34 insertions(+), 36 deletions(-) rename airflow-core/src/airflow/migrations/versions/{0112_3_3_0_add_language_field_to_task_instance.py => 0112_3_3_0_add_sdk_field_to_task_instance.py} (82%) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py index 8cf413d5819f3..c1f3ca6ae72bb 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py @@ -256,7 +256,7 @@ class TaskInstance(BaseModel): map_index: int = -1 hostname: str | None = None context_carrier: dict | None = None - language: str | None = None + sdk: str | None = None class AssetReferenceAssetEventDagRun(StrictBaseModel): diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py b/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py index 98685a1e0b25d..ab995da52d062 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py @@ -47,12 +47,10 @@ AddTeamNameField, ) from airflow.api_fastapi.execution_api.versions.v2026_06_30 import AddVariableKeysEndpoint -from airflow.api_fastapi.execution_api.versions.v2026_07_01 import AddLanguageField bundle = VersionBundle( HeadVersion(), Version("2026-06-30", AddVariableKeysEndpoint), - Version("2026-06-16", AddRetryPolicyFields, AddLanguageField), Version( "2026-06-16", AddRetryPolicyFields, diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_07_01.py b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_07_01.py index 7882a51787944..0534365dea2bf 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_07_01.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_07_01.py @@ -22,9 +22,9 @@ from airflow.api_fastapi.execution_api.datamodels.taskinstance import TaskInstance -class AddLanguageField(VersionChange): - """Add the `language` field to the TaskInstance model.""" +class AddSdkField(VersionChange): + """Add the `sdk` field to the TaskInstance model.""" description = __doc__ - instructions_to_migrate_to_previous_version = (schema(TaskInstance).field("language").didnt_exist,) + instructions_to_migrate_to_previous_version = (schema(TaskInstance).field("sdk").didnt_exist,) diff --git a/airflow-core/src/airflow/executors/workloads/task.py b/airflow-core/src/airflow/executors/workloads/task.py index f0f56ed575e6c..41ec428d4fb0a 100644 --- a/airflow-core/src/airflow/executors/workloads/task.py +++ b/airflow-core/src/airflow/executors/workloads/task.py @@ -55,7 +55,7 @@ class BaseTaskInstanceDTO(BaseModel): priority_weight: int executor_config: dict | None = Field(default=None, exclude=True) - language: str | None = None + sdk: str | None = None parent_context_carrier: dict | None = None context_carrier: dict | None = None diff --git a/airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_language_field_to_task_instance.py b/airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_sdk_field_to_task_instance.py similarity index 82% rename from airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_language_field_to_task_instance.py rename to airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_sdk_field_to_task_instance.py index fd0e230251899..2d535c102bf58 100644 --- a/airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_language_field_to_task_instance.py +++ b/airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_sdk_field_to_task_instance.py @@ -17,7 +17,7 @@ # under the License. """ -Add language field to task instance. +Add sdk field to task instance. Revision ID: 7d3c6395b7f6 Revises: 9fabad868fdb @@ -39,12 +39,12 @@ def upgrade(): - """Apply add language field to task instance.""" + """Apply add sdk field to task instance.""" with op.batch_alter_table("task_instance", schema=None) as batch_op: - batch_op.add_column(sa.Column("language", sa.String(length=64), nullable=True)) + batch_op.add_column(sa.Column("sdk", sa.String(length=64), nullable=True)) def downgrade(): - """Unapply add language field to task instance.""" + """Unapply add sdk field to task instance.""" with op.batch_alter_table("task_instance", schema=None) as batch_op: - batch_op.drop_column("language") + batch_op.drop_column("sdk") diff --git a/airflow-core/src/airflow/models/taskinstance.py b/airflow-core/src/airflow/models/taskinstance.py index dc74003ea6ea9..bcac80095faa3 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -578,7 +578,7 @@ class TaskInstance(Base, LoggingMixin, BaseWorkload): priority_weight: Mapped[int | None] = mapped_column(Integer, nullable=True) operator: Mapped[str | None] = mapped_column(String(1000), nullable=True) custom_operator_name: Mapped[str | None] = mapped_column(String(1000), nullable=True) - language: Mapped[str | None] = mapped_column(String(64), nullable=True) + sdk: Mapped[str | None] = mapped_column(String(64), nullable=True) queued_dttm: Mapped[datetime | None] = mapped_column(UtcDateTime, nullable=True) scheduled_dttm: Mapped[datetime | None] = mapped_column(UtcDateTime, nullable=True) queued_by_job_id: Mapped[int | None] = mapped_column(Integer, nullable=True) @@ -766,7 +766,7 @@ def insert_mapping( "executor_config": task.executor_config, "operator": task.task_type, "custom_operator_name": getattr(task, "operator_name", None), - "language": getattr(task, "language", None), + "sdk": getattr(task, "sdk", None), "map_index": map_index, "_task_display_property_value": task.task_display_name, "dag_version_id": dag_version_id, @@ -945,7 +945,7 @@ def refresh_from_task(self, task: Operator, pool_override: str | None = None) -> self.executor = task.executor self.executor_config = task.executor_config self.operator = task.task_type - self.language = getattr(task, "language", None) + self.sdk = getattr(task, "sdk", None) op_name = getattr(task, "operator_name", None) self.custom_operator_name = op_name if isinstance(op_name, str) else "" # Re-apply cluster policy here so that task default do not overload previous data diff --git a/airflow-core/src/airflow/serialization/definitions/baseoperator.py b/airflow-core/src/airflow/serialization/definitions/baseoperator.py index b1b91ff8f7ecb..9eaf9cc3ed906 100644 --- a/airflow-core/src/airflow/serialization/definitions/baseoperator.py +++ b/airflow-core/src/airflow/serialization/definitions/baseoperator.py @@ -195,7 +195,7 @@ def get_serialized_fields(cls): "ignore_first_depends_on_past", "inlets", "is_setup", - "language", + "sdk", "is_teardown", "map_index_template", "max_active_tis_per_dag", diff --git a/providers/standard/src/airflow/providers/standard/decorators/stub.py b/providers/standard/src/airflow/providers/standard/decorators/stub.py index ac2061700e67c..8feecbc51a29f 100644 --- a/providers/standard/src/airflow/providers/standard/decorators/stub.py +++ b/providers/standard/src/airflow/providers/standard/decorators/stub.py @@ -78,7 +78,7 @@ def stub( python_callable: Callable | None = None, queue: str | None = None, executor: str | None = None, - language: str | None = None, + sdk: str | None = None, **kwargs, ) -> TaskDecorator: """ @@ -87,8 +87,8 @@ def stub( Stub tasks exist in the Dag graph only, but the execution must happen in an external environment via the Task Execution Interface. - :param language: The language runtime that should execute this task (e.g. ``"java"``). - When set, the worker routes the task to the matching locale coordinator + :param sdk: The SDK runtime that should execute this task (e.g. ``"java"``). + When set, the worker routes the task to the matching runtime coordinator instead of executing it as Python. """ return task_decorator_factory( @@ -96,6 +96,6 @@ def stub( python_callable=python_callable, queue=queue, executor=executor, - language=language, + sdk=sdk, **kwargs, ) diff --git a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py index 0f3e78739c653..359e3e175df72 100644 --- a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py +++ b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py @@ -512,7 +512,7 @@ class TaskInstance(BaseModel): map_index: Annotated[int | None, Field(title="Map Index")] = -1 hostname: Annotated[str | None, Field(title="Hostname")] = None context_carrier: Annotated[dict[str, Any] | None, Field(title="Context Carrier")] = None - language: Annotated[str | None, Field(title="Language")] = None + sdk: Annotated[str | None, Field(title="Sdk")] = None class BundleInfo(BaseModel): diff --git a/task-sdk/src/airflow/sdk/bases/operator.py b/task-sdk/src/airflow/sdk/bases/operator.py index c0f028f7b0fd7..1490123bbc25e 100644 --- a/task-sdk/src/airflow/sdk/bases/operator.py +++ b/task-sdk/src/airflow/sdk/bases/operator.py @@ -905,7 +905,7 @@ def say_hello_world(**context): max_active_tis_per_dagrun: int | None = None executor: str | None = None executor_config: dict | None = None - language: str | None = None + sdk: str | None = None do_xcom_push: bool = True multiple_outputs: bool = False inlets: list[Any] = field(default_factory=list) @@ -1064,7 +1064,7 @@ def __init__( max_active_tis_per_dagrun: int | None = None, executor: str | None = None, executor_config: dict | None = None, - language: str | None = None, + sdk: str | None = None, do_xcom_push: bool = True, multiple_outputs: bool = False, inlets: Any | None = None, @@ -1144,7 +1144,7 @@ def __init__( self.end_date = timezone.convert_to_utc(end_date) self.executor = executor self.executor_config = executor_config or {} - self.language = language + self.sdk = sdk self.run_as_user = run_as_user # TODO: # self.retries = parse_retries(retries) diff --git a/task-sdk/src/airflow/sdk/definitions/mappedoperator.py b/task-sdk/src/airflow/sdk/definitions/mappedoperator.py index ff7ed9321b283..4ee9e7b688dd8 100644 --- a/task-sdk/src/airflow/sdk/definitions/mappedoperator.py +++ b/task-sdk/src/airflow/sdk/definitions/mappedoperator.py @@ -674,8 +674,8 @@ def executor_config(self) -> dict: return self.partial_kwargs.get("executor_config", {}) @property - def language(self) -> str | None: - return self.partial_kwargs.get("language") + def sdk(self) -> str | None: + return self.partial_kwargs.get("sdk") @property def inlets(self) -> list[Any]: diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index cb48295b300f1..5dfb3068466e2 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -19,8 +19,8 @@ Runtime coordinator for non-Python DAG file processing and task execution. Provides :class:`BaseRuntimeCoordinator`, the base class for -language-specific coordinators that bridge subprocess I/O between the -Airflow supervisor and an external-language runtime (Java, Go, Rust, etc.). +SDK-specific coordinators that bridge subprocess I/O between the +Airflow supervisor and an external-SDK runtime (Java, Go, Rust, etc.). The coordinator's :meth:`~BaseRuntimeCoordinator.run_dag_parsing` method handles the full lifecycle: @@ -174,7 +174,7 @@ class BaseRuntimeCoordinator: and :class:`ProvidersManagerTaskRuntime` (task-sdk) discover registered coordinators through this single extension point. - Subclasses represent a specific language runtime (Java, Go, etc.) and + Subclasses represent a specific SDK runtime (Java, Go, etc.) and only need to implement :meth:`can_handle_dag_file`, :meth:`dag_parsing_runtime_cmd` and :meth:`task_execution_runtime_cmd`. The base class owns the entire bridge lifecycle: TCP servers, diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index d0a4af14c3874..e2e14b43b2917 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -2096,13 +2096,13 @@ def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> """ Check provider-registered runtime coordinators for a runtime-specific entrypoint. - If the task's ``language`` field matches a coordinator's ``runtime_name``, + If the task's ``sdk`` field matches a coordinator's ``runtime_name``, return a no-arg callable that bridges fd 0 to the runtime subprocess. Otherwise return ``None`` to fall through to the standard Python execution path. """ - language = startup_details.ti.language - if language is None: + sdk = startup_details.ti.sdk + if sdk is None: return None import functools @@ -2113,13 +2113,13 @@ def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> if not hasattr(coordinator_cls, "run_task_execution"): continue - if getattr(coordinator_cls, "runtime_name", None) != language: + if getattr(coordinator_cls, "runtime_name", None) != sdk: continue log.debug( "Resolved runtime-specific entrypoint for task", coordinator=coordinator_cls, - language=language, + sdk=sdk, task_id=startup_details.ti.task_id, ) return functools.partial( @@ -2131,8 +2131,8 @@ def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> ) log.warning( - "No runtime coordinator found for language", - language=language, + "No runtime coordinator found for sdk", + sdk=sdk, task_id=startup_details.ti.task_id, ) return None From 80c56d166b6af1dda9e4d45f52556641edaa1f41 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 11:34:35 +0800 Subject: [PATCH 20/75] Rename languages.java provider to sdk.java --- .github/boring-cyborg.yml | 4 +- Dockerfile.ci | 24 +++++++++ .../unit/always/test_providers_manager.py | 12 ++--- providers/{languages => sdk}/java/.gitignore | 0 providers/{languages => sdk}/java/LICENSE | 0 providers/{languages => sdk}/java/NOTICE | 0 providers/{languages => sdk}/java/README.rst | 14 +++--- .../java/docs/.latest-doc-only-change.txt | 0 .../java/docs/changelog.rst | 2 +- .../{languages => sdk}/java/docs/commits.rst | 6 +-- .../{languages => sdk}/java/docs/conf.py | 2 +- .../{languages => sdk}/java/docs/index.rst | 24 ++++----- .../installing-providers-from-sources.rst | 0 .../{languages => sdk}/java/docs/security.rst | 0 .../{languages => sdk}/java/provider.yaml | 10 ++-- .../{languages => sdk}/java/pyproject.toml | 14 +++--- .../java/src/airflow/__init__.py | 0 .../java/src/airflow/providers/__init__.py | 0 .../src/airflow/providers/sdk}/__init__.py | 0 .../airflow/providers/sdk}/java/__init__.py | 2 +- .../providers/sdk}/java/bundle_scanner.py | 0 .../providers/sdk}/java/coordinator.py | 2 +- .../providers/sdk}/java/get_provider_info.py | 10 ++-- .../{languages => sdk}/java/tests/conftest.py | 0 .../java/tests/unit/__init__.py | 0 .../java/tests/unit/sdk}/__init__.py | 0 .../java/tests/unit/sdk}/java/__init__.py | 0 .../unit/sdk}/java/test_java_provider.py | 12 ++--- pyproject.toml | 20 ++++---- scripts/ci/docker-compose/remove-sources.yml | 2 +- scripts/ci/docker-compose/tests-sources.yml | 2 +- .../test_providers_manager_runtime.py | 12 ++--- uv.lock | 50 ++++++++++++++++++- 33 files changed, 148 insertions(+), 76 deletions(-) rename providers/{languages => sdk}/java/.gitignore (100%) rename providers/{languages => sdk}/java/LICENSE (100%) rename providers/{languages => sdk}/java/NOTICE (100%) rename providers/{languages => sdk}/java/README.rst (80%) rename providers/{languages => sdk}/java/docs/.latest-doc-only-change.txt (100%) rename providers/{languages => sdk}/java/docs/changelog.rst (96%) rename providers/{languages => sdk}/java/docs/commits.rst (89%) rename providers/{languages => sdk}/java/docs/conf.py (92%) rename providers/{languages => sdk}/java/docs/index.rst (70%) rename providers/{languages => sdk}/java/docs/installing-providers-from-sources.rst (100%) rename providers/{languages => sdk}/java/docs/security.rst (100%) rename providers/{languages => sdk}/java/provider.yaml (87%) rename providers/{languages => sdk}/java/pyproject.toml (90%) rename providers/{languages => sdk}/java/src/airflow/__init__.py (100%) rename providers/{languages => sdk}/java/src/airflow/providers/__init__.py (100%) rename providers/{languages/java/src/airflow/providers/languages => sdk/java/src/airflow/providers/sdk}/__init__.py (100%) rename providers/{languages/java/src/airflow/providers/languages => sdk/java/src/airflow/providers/sdk}/java/__init__.py (92%) rename providers/{languages/java/src/airflow/providers/languages => sdk/java/src/airflow/providers/sdk}/java/bundle_scanner.py (100%) rename providers/{languages/java/src/airflow/providers/languages => sdk/java/src/airflow/providers/sdk}/java/coordinator.py (98%) rename providers/{languages/java/src/airflow/providers/languages => sdk/java/src/airflow/providers/sdk}/java/get_provider_info.py (84%) rename providers/{languages => sdk}/java/tests/conftest.py (100%) rename providers/{languages => sdk}/java/tests/unit/__init__.py (100%) rename providers/{languages/java/tests/unit/languages => sdk/java/tests/unit/sdk}/__init__.py (100%) rename providers/{languages/java/tests/unit/languages => sdk/java/tests/unit/sdk}/java/__init__.py (100%) rename providers/{languages/java/tests/unit/languages => sdk/java/tests/unit/sdk}/java/test_java_provider.py (74%) diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml index afdcb80deaaf7..3f5477f4512c5 100644 --- a/.github/boring-cyborg.yml +++ b/.github/boring-cyborg.yml @@ -189,8 +189,8 @@ labelPRBasedOnFilePath: provider:keycloak: - providers/keycloak/** - provider:languages-java: - - providers/languages/java/** + provider:sdk-java: + - providers/sdk/java/** provider:microsoft-azure: - providers/microsoft/azure/** diff --git a/Dockerfile.ci b/Dockerfile.ci index a9d8b2e7f1dff..1fb4ce4486ae1 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1222,6 +1222,30 @@ function environment_initialization() { export AIRFLOW__SCHEDULER__GO_WORKER=True fi + if [[ ${JAVA_SDK=} == "true" ]]; then + echo + echo "${COLOR_BLUE}Setting up Java SDK${COLOR_RESET}" + echo + + # Install Java and the Java SDK provider + bash /opt/airflow/scripts/in_container/java_sdk_setup.sh + + # Set JAVA_HOME and PATH before Gradle build so gradlew can find java + export JAVA_HOME=/files/openjdk + export PATH=/files/openjdk/bin:${PATH} + + # Build both Java SDK bundles (stub + pure Java) + export JAVA_SDK_SRC_DIR=/opt/airflow/java-sdk + export BUNDLES_OUTPUT_DIR=/files/java-sdk-bundles + bash /opt/airflow/scripts/in_container/java_sdk_build.sh + + # Source the generated environment configuration + # shellcheck disable=SC1091 + source /files/java-sdk-bundles/java_sdk_env.sh + + echo "${COLOR_BLUE}Java SDK setup complete.${COLOR_RESET}" + fi + RUN_TESTS=${RUN_TESTS:="false"} CI=${CI:="false"} diff --git a/airflow-core/tests/unit/always/test_providers_manager.py b/airflow-core/tests/unit/always/test_providers_manager.py index 2bc51c993e0fe..9d69ce3ebc24b 100644 --- a/airflow-core/tests/unit/always/test_providers_manager.py +++ b/airflow-core/tests/unit/always/test_providers_manager.py @@ -267,18 +267,18 @@ class ZCoordinator: pass mock_import_string.side_effect = lambda path: { - "airflow.providers.languages.java.coordinator.ACoordinator": ACoordinator, - "airflow.providers.languages.java.coordinator.ZCoordinator": ZCoordinator, + "airflow.providers.sdk.java.coordinator.ACoordinator": ACoordinator, + "airflow.providers.sdk.java.coordinator.ZCoordinator": ZCoordinator, }[path] providers_manager = ProvidersManager() providers_manager._provider_dict = LazyDictWithCache() - providers_manager._provider_dict["apache-airflow-providers-languages-java"] = ProviderInfo( + providers_manager._provider_dict["apache-airflow-providers-sdk-java"] = ProviderInfo( version="0.0.1", data={ "runtime-coordinators": [ - "airflow.providers.languages.java.coordinator.ZCoordinator", - "airflow.providers.languages.java.coordinator.ACoordinator", - "airflow.providers.languages.java.coordinator.ZCoordinator", + "airflow.providers.sdk.java.coordinator.ZCoordinator", + "airflow.providers.sdk.java.coordinator.ACoordinator", + "airflow.providers.sdk.java.coordinator.ZCoordinator", ] }, ) diff --git a/providers/languages/java/.gitignore b/providers/sdk/java/.gitignore similarity index 100% rename from providers/languages/java/.gitignore rename to providers/sdk/java/.gitignore diff --git a/providers/languages/java/LICENSE b/providers/sdk/java/LICENSE similarity index 100% rename from providers/languages/java/LICENSE rename to providers/sdk/java/LICENSE diff --git a/providers/languages/java/NOTICE b/providers/sdk/java/NOTICE similarity index 100% rename from providers/languages/java/NOTICE rename to providers/sdk/java/NOTICE diff --git a/providers/languages/java/README.rst b/providers/sdk/java/README.rst similarity index 80% rename from providers/languages/java/README.rst rename to providers/sdk/java/README.rst index 19527bd4191a5..c183b5d7d6d65 100644 --- a/providers/languages/java/README.rst +++ b/providers/sdk/java/README.rst @@ -21,29 +21,29 @@ .. IF YOU WANT TO MODIFY TEMPLATE FOR THIS FILE, YOU SHOULD MODIFY THE TEMPLATE ``PROVIDER_README_TEMPLATE.rst.jinja2`` IN the ``dev/breeze/src/airflow_breeze/templates`` DIRECTORY -Package ``apache-airflow-providers-languages-java`` +Package ``apache-airflow-providers-sdk-java`` Release: ``0.1.0`` -Java language support for Apache Airflow task coordinators and DAG file processors. +Java SDK support for Apache Airflow task coordinators and DAG file processors. Provider package ---------------- -This is a provider package for ``languages.java`` provider. All classes for this provider package -are in ``airflow.providers.languages.java`` python package. +This is a provider package for ``sdk.java`` provider. All classes for this provider package +are in ``airflow.providers.sdk.java`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ You can install this package on top of an existing Airflow installation (see ``Requirements`` below for the minimum Airflow version supported) via -``pip install apache-airflow-providers-languages-java`` +``pip install apache-airflow-providers-sdk-java`` The package supports the following python versions: 3.10,3.11,3.12,3.13,3.14 @@ -57,4 +57,4 @@ PIP package Version required ================== ================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/languages/java/docs/.latest-doc-only-change.txt b/providers/sdk/java/docs/.latest-doc-only-change.txt similarity index 100% rename from providers/languages/java/docs/.latest-doc-only-change.txt rename to providers/sdk/java/docs/.latest-doc-only-change.txt diff --git a/providers/languages/java/docs/changelog.rst b/providers/sdk/java/docs/changelog.rst similarity index 96% rename from providers/languages/java/docs/changelog.rst rename to providers/sdk/java/docs/changelog.rst index e88eef968d669..c6a923fee58d7 100644 --- a/providers/languages/java/docs/changelog.rst +++ b/providers/sdk/java/docs/changelog.rst @@ -22,7 +22,7 @@ The changelog is updated and maintained semi-automatically by release manager. -``apache-airflow-providers-languages-java`` +``apache-airflow-providers-sdk-java`` Changelog diff --git a/providers/languages/java/docs/commits.rst b/providers/sdk/java/docs/commits.rst similarity index 89% rename from providers/languages/java/docs/commits.rst rename to providers/sdk/java/docs/commits.rst index 5b15c6b7169d6..bbd5ad0573136 100644 --- a/providers/languages/java/docs/commits.rst +++ b/providers/sdk/java/docs/commits.rst @@ -23,13 +23,13 @@ .. THE REMAINDER OF THE FILE IS AUTOMATICALLY GENERATED. IT WILL BE OVERWRITTEN! -Package apache-airflow-providers-languages-java +Package apache-airflow-providers-sdk-java ------------------------------------------------------ -Java language support for Apache Airflow task coordinators and DAG file processors. +Java SDK support for Apache Airflow task coordinators and DAG file processors. -This is detailed commit list of changes for versions provider package: ``languages.java``. +This is detailed commit list of changes for versions provider package: ``sdk.java``. For high-level changelog, see :doc:`package information including changelog `. .. airflow-providers-commits:: diff --git a/providers/languages/java/docs/conf.py b/providers/sdk/java/docs/conf.py similarity index 92% rename from providers/languages/java/docs/conf.py rename to providers/sdk/java/docs/conf.py index bbda22da2f1d5..596c5b5c7b5f3 100644 --- a/providers/languages/java/docs/conf.py +++ b/providers/sdk/java/docs/conf.py @@ -22,6 +22,6 @@ import os -os.environ["AIRFLOW_PACKAGE_NAME"] = "apache-airflow-providers-languages-java" +os.environ["AIRFLOW_PACKAGE_NAME"] = "apache-airflow-providers-sdk-java" from docs.provider_conf import * # noqa: F403 diff --git a/providers/languages/java/docs/index.rst b/providers/sdk/java/docs/index.rst similarity index 70% rename from providers/languages/java/docs/index.rst rename to providers/sdk/java/docs/index.rst index cccaf3c8d3f2d..53b0719373c38 100644 --- a/providers/languages/java/docs/index.rst +++ b/providers/sdk/java/docs/index.rst @@ -15,10 +15,10 @@ specific language governing permissions and limitations under the License. -``apache-airflow-providers-languages-java`` +``apache-airflow-providers-sdk-java`` =========================================== -The Languages: Java provider registers Java-specific task coordinator and DAG file processor classes for Apache Airflow. +The SDK: Java provider registers Java-specific task coordinator and DAG file processor classes for Apache Airflow. .. toctree:: :hidden: @@ -34,14 +34,14 @@ The Languages: Java provider registers Java-specific task coordinator and DAG fi :maxdepth: 1 :caption: References - Python API <_api/airflow/providers/languages/java/index> + Python API <_api/airflow/providers/sdk/java/index> .. toctree:: :hidden: :maxdepth: 1 :caption: Resources - PyPI Repository + PyPI Repository Installing from sources .. THE REMAINDER OF THE FILE IS AUTOMATICALLY GENERATED. IT WILL BE OVERWRITTEN AT RELEASE TIME! @@ -55,10 +55,10 @@ The Languages: Java provider registers Java-specific task coordinator and DAG fi Detailed list of commits -apache-airflow-providers-languages-java package +apache-airflow-providers-sdk-java package ------------------------------------------------------ -Java language support for Apache Airflow runtime coordinators. +Java SDK support for Apache Airflow runtime coordinators. Release: 0.1.0 @@ -66,14 +66,14 @@ Release: 0.1.0 Provider package ---------------- -This package is for the ``languages.java`` provider. -All classes for this package are included in the ``airflow.providers.languages.java`` python package. +This package is for the ``sdk.java`` provider. +All classes for this package are included in the ``airflow.providers.sdk.java`` python package. Installation ------------ You can install this package on top of an existing Airflow installation via -``pip install apache-airflow-providers-languages-java``. +``pip install apache-airflow-providers-sdk-java``. For the minimum Airflow version supported, see ``Requirements`` below. Requirements @@ -97,7 +97,7 @@ You can install such cross-provider dependencies when installing from PyPI. For .. code-block:: bash - pip install apache-airflow-providers-languages-java[common.compat] + pip install apache-airflow-providers-sdk-java[common.compat] ================================================================================================================== ================= @@ -112,5 +112,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-languages-java 0.1.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-languages-java 0.1.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-sdk-java 0.1.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-sdk-java 0.1.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/languages/java/docs/installing-providers-from-sources.rst b/providers/sdk/java/docs/installing-providers-from-sources.rst similarity index 100% rename from providers/languages/java/docs/installing-providers-from-sources.rst rename to providers/sdk/java/docs/installing-providers-from-sources.rst diff --git a/providers/languages/java/docs/security.rst b/providers/sdk/java/docs/security.rst similarity index 100% rename from providers/languages/java/docs/security.rst rename to providers/sdk/java/docs/security.rst diff --git a/providers/languages/java/provider.yaml b/providers/sdk/java/provider.yaml similarity index 87% rename from providers/languages/java/provider.yaml rename to providers/sdk/java/provider.yaml index 5153042da02eb..c93b45a4da6aa 100644 --- a/providers/languages/java/provider.yaml +++ b/providers/sdk/java/provider.yaml @@ -16,10 +16,10 @@ # under the License. --- -package-name: apache-airflow-providers-languages-java -name: "Languages: Java" +package-name: apache-airflow-providers-sdk-java +name: "SDK: Java" description: | - Java language support for Apache Airflow runtime coordinators. + Java SDK support for Apache Airflow runtime coordinators. state: ready lifecycle: incubation @@ -39,7 +39,7 @@ integrations: config: java: - description: "Options for the Java language provider." + description: "Options for the Java SDK provider." options: bundles_folder: description: | @@ -55,4 +55,4 @@ config: default: "" runtime-coordinators: - - airflow.providers.languages.java.coordinator.JavaRuntimeCoordinator + - airflow.providers.sdk.java.coordinator.JavaRuntimeCoordinator diff --git a/providers/languages/java/pyproject.toml b/providers/sdk/java/pyproject.toml similarity index 90% rename from providers/languages/java/pyproject.toml rename to providers/sdk/java/pyproject.toml index 493c07a5c10aa..6ed7ec4165a89 100644 --- a/providers/languages/java/pyproject.toml +++ b/providers/sdk/java/pyproject.toml @@ -24,9 +24,9 @@ requires = ["flit_core==3.12.0"] build-backend = "flit_core.buildapi" [project] -name = "apache-airflow-providers-languages-java" +name = "apache-airflow-providers-sdk-java" version = "0.1.0" -description = "Provider package apache-airflow-providers-languages-java for Apache Airflow" +description = "Provider package apache-airflow-providers-sdk-java for Apache Airflow" readme = "README.rst" license = "Apache-2.0" license-files = ['LICENSE', 'NOTICE'] @@ -36,7 +36,7 @@ authors = [ maintainers = [ {name="Apache Software Foundation", email="dev@airflow.apache.org"}, ] -keywords = [ "airflow-provider", "languages.java", "airflow", "integration" ] +keywords = [ "airflow-provider", "sdk.java", "airflow", "integration" ] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", @@ -104,8 +104,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-languages-java/0.1.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-languages-java/0.1.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-sdk-java/0.1.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-sdk-java/0.1.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" @@ -113,7 +113,7 @@ apache-airflow-providers-standard = {workspace = true} "YouTube" = "https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/" [project.entry-points."apache_airflow_provider"] -provider_info = "airflow.providers.languages.java.get_provider_info:get_provider_info" +provider_info = "airflow.providers.sdk.java.get_provider_info:get_provider_info" [tool.flit.module] -name = "airflow.providers.languages.java" +name = "airflow.providers.sdk.java" diff --git a/providers/languages/java/src/airflow/__init__.py b/providers/sdk/java/src/airflow/__init__.py similarity index 100% rename from providers/languages/java/src/airflow/__init__.py rename to providers/sdk/java/src/airflow/__init__.py diff --git a/providers/languages/java/src/airflow/providers/__init__.py b/providers/sdk/java/src/airflow/providers/__init__.py similarity index 100% rename from providers/languages/java/src/airflow/providers/__init__.py rename to providers/sdk/java/src/airflow/providers/__init__.py diff --git a/providers/languages/java/src/airflow/providers/languages/__init__.py b/providers/sdk/java/src/airflow/providers/sdk/__init__.py similarity index 100% rename from providers/languages/java/src/airflow/providers/languages/__init__.py rename to providers/sdk/java/src/airflow/providers/sdk/__init__.py diff --git a/providers/languages/java/src/airflow/providers/languages/java/__init__.py b/providers/sdk/java/src/airflow/providers/sdk/java/__init__.py similarity index 92% rename from providers/languages/java/src/airflow/providers/languages/java/__init__.py rename to providers/sdk/java/src/airflow/providers/sdk/java/__init__.py index d90a03c004ffd..0e177079fd3de 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/__init__.py +++ b/providers/sdk/java/src/airflow/providers/sdk/java/__init__.py @@ -35,5 +35,5 @@ "3.0.0" ): raise RuntimeError( - f"The package `apache-airflow-providers-languages-java:{__version__}` needs Apache Airflow 3.0.0+" + f"The package `apache-airflow-providers-sdk-java:{__version__}` needs Apache Airflow 3.0.0+" ) diff --git a/providers/languages/java/src/airflow/providers/languages/java/bundle_scanner.py b/providers/sdk/java/src/airflow/providers/sdk/java/bundle_scanner.py similarity index 100% rename from providers/languages/java/src/airflow/providers/languages/java/bundle_scanner.py rename to providers/sdk/java/src/airflow/providers/sdk/java/bundle_scanner.py diff --git a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py b/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py similarity index 98% rename from providers/languages/java/src/airflow/providers/languages/java/coordinator.py rename to providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py index 3a5b80875c7e8..e23740549104a 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/coordinator.py +++ b/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py @@ -25,7 +25,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from airflow.providers.languages.java.bundle_scanner import BundleScanner, read_dag_code +from airflow.providers.sdk.java.bundle_scanner import BundleScanner, read_dag_code from airflow.sdk.execution_time.coordinator import BaseRuntimeCoordinator if TYPE_CHECKING: diff --git a/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py b/providers/sdk/java/src/airflow/providers/sdk/java/get_provider_info.py similarity index 84% rename from providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py rename to providers/sdk/java/src/airflow/providers/sdk/java/get_provider_info.py index dc0cb5ed67c41..48fd8238c8a5f 100644 --- a/providers/languages/java/src/airflow/providers/languages/java/get_provider_info.py +++ b/providers/sdk/java/src/airflow/providers/sdk/java/get_provider_info.py @@ -23,15 +23,15 @@ def get_provider_info(): return { - "package-name": "apache-airflow-providers-languages-java", - "name": "Languages: Java", - "description": "Java language support for Apache Airflow runtime coordinators.\n", + "package-name": "apache-airflow-providers-sdk-java", + "name": "SDK: Java", + "description": "Java SDK support for Apache Airflow runtime coordinators.\n", "integrations": [ {"integration-name": "Java", "external-doc-url": "https://openjdk.org/", "tags": ["software"]} ], "config": { "java": { - "description": "Options for the Java language provider.", + "description": "Options for the Java SDK provider.", "options": { "bundles_folder": { "description": "Path to the directory containing Java DAG bundle JARs.\nWhen using Python stub DAGs that delegate task execution to Java,\nthe coordinator scans this directory to find the JAR bundle matching\nthe target dag_id. Each immediate subdirectory is treated as a\nseparate bundle home, and the directory itself is also checked\n(flat layout).\n", @@ -43,5 +43,5 @@ def get_provider_info(): }, } }, - "runtime-coordinators": ["airflow.providers.languages.java.coordinator.JavaRuntimeCoordinator"], + "runtime-coordinators": ["airflow.providers.sdk.java.coordinator.JavaRuntimeCoordinator"], } diff --git a/providers/languages/java/tests/conftest.py b/providers/sdk/java/tests/conftest.py similarity index 100% rename from providers/languages/java/tests/conftest.py rename to providers/sdk/java/tests/conftest.py diff --git a/providers/languages/java/tests/unit/__init__.py b/providers/sdk/java/tests/unit/__init__.py similarity index 100% rename from providers/languages/java/tests/unit/__init__.py rename to providers/sdk/java/tests/unit/__init__.py diff --git a/providers/languages/java/tests/unit/languages/__init__.py b/providers/sdk/java/tests/unit/sdk/__init__.py similarity index 100% rename from providers/languages/java/tests/unit/languages/__init__.py rename to providers/sdk/java/tests/unit/sdk/__init__.py diff --git a/providers/languages/java/tests/unit/languages/java/__init__.py b/providers/sdk/java/tests/unit/sdk/java/__init__.py similarity index 100% rename from providers/languages/java/tests/unit/languages/java/__init__.py rename to providers/sdk/java/tests/unit/sdk/java/__init__.py diff --git a/providers/languages/java/tests/unit/languages/java/test_java_provider.py b/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py similarity index 74% rename from providers/languages/java/tests/unit/languages/java/test_java_provider.py rename to providers/sdk/java/tests/unit/sdk/java/test_java_provider.py index 782373158ce92..94c7c1794197e 100644 --- a/providers/languages/java/tests/unit/languages/java/test_java_provider.py +++ b/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py @@ -17,15 +17,15 @@ # under the License. from __future__ import annotations -from airflow.providers.languages.java.coordinator import JavaRuntimeCoordinator -from airflow.providers.languages.java.get_provider_info import get_provider_info +from airflow.providers.sdk.java.coordinator import JavaRuntimeCoordinator +from airflow.providers.sdk.java.get_provider_info import get_provider_info def test_get_provider_info_exposes_java_runtime_components(): assert get_provider_info() == { - "package-name": "apache-airflow-providers-languages-java", - "name": "Languages: Java", - "description": "Java language support for Apache Airflow runtime coordinators.\n", + "package-name": "apache-airflow-providers-sdk-java", + "name": "SDK: Java", + "description": "Java SDK support for Apache Airflow runtime coordinators.\n", "integrations": [ { "integration-name": "Java", @@ -34,7 +34,7 @@ def test_get_provider_info_exposes_java_runtime_components(): } ], "runtime-coordinators": [ - "airflow.providers.languages.java.coordinator.JavaRuntimeCoordinator", + "airflow.providers.sdk.java.coordinator.JavaRuntimeCoordinator", ], } diff --git a/pyproject.toml b/pyproject.toml index 5508763a5159c..f1c6e8b5fdfca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -266,9 +266,6 @@ apache-airflow = "airflow.__main__:main" "keycloak" = [ "apache-airflow-providers-keycloak>=0.0.1" ] -"languages.java" = [ - "apache-airflow-providers-languages-java>=0.1.0" # Set from local provider pyproject.toml -] "microsoft.azure" = [ "apache-airflow-providers-microsoft-azure>=10.5.1" ] @@ -341,6 +338,9 @@ apache-airflow = "airflow.__main__:main" "samba" = [ "apache-airflow-providers-samba>=4.9.0" ] +"sdk.java" = [ + "apache-airflow-providers-sdk-java>=0.1.0" # Set from local provider pyproject.toml +] "segment" = [ "apache-airflow-providers-segment>=3.7.0" ] @@ -460,7 +460,6 @@ apache-airflow = "airflow.__main__:main" "apache-airflow-providers-jdbc>=4.5.2", "apache-airflow-providers-jenkins>=3.7.2", "apache-airflow-providers-keycloak>=0.0.1", - "apache-airflow-providers-languages-java>=0.1.0", # Set from local provider pyproject.toml "apache-airflow-providers-microsoft-azure>=10.5.1", "apache-airflow-providers-microsoft-mssql>=3.9.2", "apache-airflow-providers-microsoft-psrp>=3.0.0", @@ -485,6 +484,7 @@ apache-airflow = "airflow.__main__:main" "apache-airflow-providers-redis>=4.0.0", "apache-airflow-providers-salesforce>=5.9.0", "apache-airflow-providers-samba>=4.9.0", + "apache-airflow-providers-sdk-java>=0.1.0", # Set from local provider pyproject.toml "apache-airflow-providers-segment>=3.7.0", "apache-airflow-providers-sendgrid>=4.0.0", "apache-airflow-providers-sftp>=5.0.0", @@ -1190,8 +1190,6 @@ mypy_path = [ "$MYPY_CONFIG_FILE_DIR/providers/jenkins/tests", "$MYPY_CONFIG_FILE_DIR/providers/keycloak/src", "$MYPY_CONFIG_FILE_DIR/providers/keycloak/tests", - "$MYPY_CONFIG_FILE_DIR/providers/languages/java/src", - "$MYPY_CONFIG_FILE_DIR/providers/languages/java/tests", "$MYPY_CONFIG_FILE_DIR/providers/microsoft/azure/src", "$MYPY_CONFIG_FILE_DIR/providers/microsoft/azure/tests", "$MYPY_CONFIG_FILE_DIR/providers/microsoft/mssql/src", @@ -1240,6 +1238,8 @@ mypy_path = [ "$MYPY_CONFIG_FILE_DIR/providers/salesforce/tests", "$MYPY_CONFIG_FILE_DIR/providers/samba/src", "$MYPY_CONFIG_FILE_DIR/providers/samba/tests", + "$MYPY_CONFIG_FILE_DIR/providers/sdk/java/src", + "$MYPY_CONFIG_FILE_DIR/providers/sdk/java/tests", "$MYPY_CONFIG_FILE_DIR/providers/segment/src", "$MYPY_CONFIG_FILE_DIR/providers/segment/tests", "$MYPY_CONFIG_FILE_DIR/providers/sendgrid/src", @@ -1472,7 +1472,6 @@ apache-airflow-providers-informatica = false apache-airflow-providers-jdbc = false apache-airflow-providers-jenkins = false apache-airflow-providers-keycloak = false -apache-airflow-providers-languages-java = false apache-airflow-providers-microsoft-azure = false apache-airflow-providers-microsoft-mssql = false apache-airflow-providers-microsoft-psrp = false @@ -1497,6 +1496,7 @@ apache-airflow-providers-qdrant = false apache-airflow-providers-redis = false apache-airflow-providers-salesforce = false apache-airflow-providers-samba = false +apache-airflow-providers-sdk-java = false apache-airflow-providers-segment = false apache-airflow-providers-sendgrid = false apache-airflow-providers-sftp = false @@ -1623,7 +1623,6 @@ apache-airflow-providers-informatica = false apache-airflow-providers-jdbc = false apache-airflow-providers-jenkins = false apache-airflow-providers-keycloak = false -apache-airflow-providers-languages-java = false apache-airflow-providers-microsoft-azure = false apache-airflow-providers-microsoft-mssql = false apache-airflow-providers-microsoft-psrp = false @@ -1648,6 +1647,7 @@ apache-airflow-providers-qdrant = false apache-airflow-providers-redis = false apache-airflow-providers-salesforce = false apache-airflow-providers-samba = false +apache-airflow-providers-sdk-java = false apache-airflow-providers-segment = false apache-airflow-providers-sendgrid = false apache-airflow-providers-sftp = false @@ -1785,7 +1785,6 @@ apache-airflow-providers-informatica = { workspace = true } apache-airflow-providers-jdbc = { workspace = true } apache-airflow-providers-jenkins = { workspace = true } apache-airflow-providers-keycloak = { workspace = true } -apache-airflow-providers-languages-java = { workspace = true } apache-airflow-providers-microsoft-azure = { workspace = true } apache-airflow-providers-microsoft-mssql = { workspace = true } apache-airflow-providers-microsoft-psrp = { workspace = true } @@ -1810,6 +1809,7 @@ apache-airflow-providers-qdrant = { workspace = true } apache-airflow-providers-redis = { workspace = true } apache-airflow-providers-salesforce = { workspace = true } apache-airflow-providers-samba = { workspace = true } +apache-airflow-providers-sdk-java = { workspace = true } apache-airflow-providers-segment = { workspace = true } apache-airflow-providers-sendgrid = { workspace = true } apache-airflow-providers-sftp = { workspace = true } @@ -1923,7 +1923,6 @@ members = [ "providers/jdbc", "providers/jenkins", "providers/keycloak", - "providers/languages/java", "providers/microsoft/azure", "providers/microsoft/mssql", "providers/microsoft/psrp", @@ -1948,6 +1947,7 @@ members = [ "providers/redis", "providers/salesforce", "providers/samba", + "providers/sdk/java", "providers/segment", "providers/sendgrid", "providers/sftp", diff --git a/scripts/ci/docker-compose/remove-sources.yml b/scripts/ci/docker-compose/remove-sources.yml index aac26d76bcf24..24ca15bbb0c47 100644 --- a/scripts/ci/docker-compose/remove-sources.yml +++ b/scripts/ci/docker-compose/remove-sources.yml @@ -83,7 +83,6 @@ services: - ../../../empty:/opt/airflow/providers/jdbc/src - ../../../empty:/opt/airflow/providers/jenkins/src - ../../../empty:/opt/airflow/providers/keycloak/src - - ../../../empty:/opt/airflow/providers/languages/java/src - ../../../empty:/opt/airflow/providers/microsoft/azure/src - ../../../empty:/opt/airflow/providers/microsoft/mssql/src - ../../../empty:/opt/airflow/providers/microsoft/psrp/src @@ -108,6 +107,7 @@ services: - ../../../empty:/opt/airflow/providers/redis/src - ../../../empty:/opt/airflow/providers/salesforce/src - ../../../empty:/opt/airflow/providers/samba/src + - ../../../empty:/opt/airflow/providers/sdk/java/src - ../../../empty:/opt/airflow/providers/segment/src - ../../../empty:/opt/airflow/providers/sendgrid/src - ../../../empty:/opt/airflow/providers/sftp/src diff --git a/scripts/ci/docker-compose/tests-sources.yml b/scripts/ci/docker-compose/tests-sources.yml index a18a6539ee536..de736d60237ae 100644 --- a/scripts/ci/docker-compose/tests-sources.yml +++ b/scripts/ci/docker-compose/tests-sources.yml @@ -96,7 +96,6 @@ services: - ../../../providers/jdbc/tests:/opt/airflow/providers/jdbc/tests - ../../../providers/jenkins/tests:/opt/airflow/providers/jenkins/tests - ../../../providers/keycloak/tests:/opt/airflow/providers/keycloak/tests - - ../../../providers/languages/java/tests:/opt/airflow/providers/languages/java/tests - ../../../providers/microsoft/azure/tests:/opt/airflow/providers/microsoft/azure/tests - ../../../providers/microsoft/mssql/tests:/opt/airflow/providers/microsoft/mssql/tests - ../../../providers/microsoft/psrp/tests:/opt/airflow/providers/microsoft/psrp/tests @@ -121,6 +120,7 @@ services: - ../../../providers/redis/tests:/opt/airflow/providers/redis/tests - ../../../providers/salesforce/tests:/opt/airflow/providers/salesforce/tests - ../../../providers/samba/tests:/opt/airflow/providers/samba/tests + - ../../../providers/sdk/java/tests:/opt/airflow/providers/sdk/java/tests - ../../../providers/segment/tests:/opt/airflow/providers/segment/tests - ../../../providers/sendgrid/tests:/opt/airflow/providers/sendgrid/tests - ../../../providers/sftp/tests:/opt/airflow/providers/sftp/tests diff --git a/task-sdk/tests/task_sdk/test_providers_manager_runtime.py b/task-sdk/tests/task_sdk/test_providers_manager_runtime.py index b7eff9712e4b7..edea4fd35ede0 100644 --- a/task-sdk/tests/task_sdk/test_providers_manager_runtime.py +++ b/task-sdk/tests/task_sdk/test_providers_manager_runtime.py @@ -252,17 +252,17 @@ class ZCoordinator: pass mock_import_string.side_effect = lambda path: { - "airflow.providers.languages.java.coordinator.ACoordinator": ACoordinator, - "airflow.providers.languages.java.coordinator.ZCoordinator": ZCoordinator, + "airflow.providers.sdk.java.coordinator.ACoordinator": ACoordinator, + "airflow.providers.sdk.java.coordinator.ZCoordinator": ZCoordinator, }[path] providers_manager = ProvidersManagerTaskRuntime() - providers_manager._provider_dict["apache-airflow-providers-languages-java"] = ProviderInfo( + providers_manager._provider_dict["apache-airflow-providers-sdk-java"] = ProviderInfo( version="0.0.1", data={ "runtime-coordinators": [ - "airflow.providers.languages.java.coordinator.ZCoordinator", - "airflow.providers.languages.java.coordinator.ACoordinator", - "airflow.providers.languages.java.coordinator.ZCoordinator", + "airflow.providers.sdk.java.coordinator.ZCoordinator", + "airflow.providers.sdk.java.coordinator.ACoordinator", + "airflow.providers.sdk.java.coordinator.ZCoordinator", ] }, ) diff --git a/uv.lock b/uv.lock index bca09add09083..108cad880e51b 100644 --- a/uv.lock +++ b/uv.lock @@ -80,8 +80,8 @@ apache-airflow-providers-salesforce = false apache-airflow-providers-ssh = false apache-airflow-providers-papermill = false apache-airflow-providers-google = false -apache-airflow-providers-microsoft-psrp = false apache-airflow-providers-vertica = false +apache-airflow-providers-microsoft-psrp = false apache-airflow-providers-apache-hdfs = false apache-airflow-shared-template-rendering = false apache-airflow-mypy = false @@ -152,6 +152,7 @@ apache-airflow-providers-smtp = false apache-airflow-providers-dingding = false apache-airflow-providers-apache-kylin = false apache-airflow-providers-cloudant = false +apache-airflow-providers-sdk-java = false apache-aurflow-docker-stack = false [manifest] @@ -249,6 +250,7 @@ members = [ "apache-airflow-providers-redis", "apache-airflow-providers-salesforce", "apache-airflow-providers-samba", + "apache-airflow-providers-sdk-java", "apache-airflow-providers-segment", "apache-airflow-providers-sendgrid", "apache-airflow-providers-sftp", @@ -1064,6 +1066,7 @@ all = [ { name = "apache-airflow-providers-redis" }, { name = "apache-airflow-providers-salesforce" }, { name = "apache-airflow-providers-samba" }, + { name = "apache-airflow-providers-sdk-java" }, { name = "apache-airflow-providers-segment" }, { name = "apache-airflow-providers-sendgrid" }, { name = "apache-airflow-providers-sftp" }, @@ -1383,6 +1386,9 @@ salesforce = [ samba = [ { name = "apache-airflow-providers-samba" }, ] +sdk-java = [ + { name = "apache-airflow-providers-sdk-java" }, +] segment = [ { name = "apache-airflow-providers-segment" }, ] @@ -1703,6 +1709,8 @@ requires-dist = [ { name = "apache-airflow-providers-salesforce", marker = "extra == 'salesforce'", editable = "providers/salesforce" }, { name = "apache-airflow-providers-samba", marker = "extra == 'all'", editable = "providers/samba" }, { name = "apache-airflow-providers-samba", marker = "extra == 'samba'", editable = "providers/samba" }, + { name = "apache-airflow-providers-sdk-java", marker = "extra == 'all'", editable = "providers/sdk/java" }, + { name = "apache-airflow-providers-sdk-java", marker = "extra == 'sdk-java'", editable = "providers/sdk/java" }, { name = "apache-airflow-providers-segment", marker = "extra == 'all'", editable = "providers/segment" }, { name = "apache-airflow-providers-segment", marker = "extra == 'segment'", editable = "providers/segment" }, { name = "apache-airflow-providers-sendgrid", marker = "extra == 'all'", editable = "providers/sendgrid" }, @@ -7193,6 +7201,46 @@ dev = [ ] docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "devel-common" }] +[[package]] +name = "apache-airflow-providers-sdk-java" +version = "0.1.0" +source = { editable = "providers/sdk/java" } +dependencies = [ + { name = "apache-airflow" }, +] + +[package.optional-dependencies] +common-compat = [ + { name = "apache-airflow-providers-common-compat" }, +] + +[package.dev-dependencies] +dev = [ + { name = "apache-airflow" }, + { name = "apache-airflow-devel-common" }, + { name = "apache-airflow-providers-common-compat" }, + { name = "apache-airflow-task-sdk" }, +] +docs = [ + { name = "apache-airflow-devel-common", extra = ["docs"] }, +] + +[package.metadata] +requires-dist = [ + { name = "apache-airflow", editable = "." }, + { name = "apache-airflow-providers-common-compat", marker = "extra == 'common-compat'", editable = "providers/common/compat" }, +] +provides-extras = ["common-compat"] + +[package.metadata.requires-dev] +dev = [ + { name = "apache-airflow", editable = "." }, + { name = "apache-airflow-devel-common", editable = "devel-common" }, + { name = "apache-airflow-providers-common-compat", editable = "providers/common/compat" }, + { name = "apache-airflow-task-sdk", editable = "task-sdk" }, +] +docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "devel-common" }] + [[package]] name = "apache-airflow-providers-segment" version = "3.9.4" From 0843cd3f78894d977b15bfd7264f5e1c2eec53a1 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 19:20:42 +0800 Subject: [PATCH 21/75] Add unit tests for socket handling and selector loop functionality --- .../execution_time/test_coordinator.py | 586 ++++++++++++++++++ .../execution_time/test_selector_loop.py | 479 ++++++++++++++ 2 files changed, 1065 insertions(+) create mode 100644 task-sdk/tests/task_sdk/execution_time/test_coordinator.py create mode 100644 task-sdk/tests/task_sdk/execution_time/test_selector_loop.py diff --git a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py new file mode 100644 index 0000000000000..a069e33cd4536 --- /dev/null +++ b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py @@ -0,0 +1,586 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import os +import socket +import subprocess +from unittest.mock import MagicMock, patch + +import pytest + +from airflow.sdk.execution_time.coordinator import ( + BaseRuntimeCoordinator, + _bridge, + _send_startup_details, + _start_server, +) + + +class TestStartServer: + def test_binds_to_localhost(self): + server = _start_server() + try: + host, port = server.getsockname() + assert host == "127.0.0.1" + assert port > 0 + finally: + server.close() + + def test_assigns_random_port(self): + s1 = _start_server() + s2 = _start_server() + try: + _, port1 = s1.getsockname() + _, port2 = s2.getsockname() + # Two servers should get different ports + assert port1 != port2 + finally: + s1.close() + s2.close() + + def test_accepts_connection(self): + server = _start_server() + try: + addr = server.getsockname() + client = socket.socket() + client.connect(addr) + conn, _ = server.accept() + conn.sendall(b"ping") + assert client.recv(4) == b"ping" + conn.close() + client.close() + finally: + server.close() + + +class TestSendStartupDetails: + def test_sends_frame_bytes_to_socket(self): + """Verify _send_startup_details calls sendall with a length-prefixed msgpack frame.""" + mock_startup = MagicMock() + mock_startup.model_dump.return_value = {"type": "StartupDetails", "ti": {}} + + mock_socket = MagicMock(spec=socket.socket) + + _send_startup_details(mock_socket, mock_startup) + + mock_startup.model_dump.assert_called_once_with(mode="json") + mock_socket.sendall.assert_called_once() + + sent_bytes = mock_socket.sendall.call_args[0][0] + # First 4 bytes are the big-endian length prefix + assert len(sent_bytes) > 4 + length = int.from_bytes(sent_bytes[:4], "big") + assert length == len(sent_bytes) - 4 + + def test_frame_contains_response_id_zero(self): + """The frame should use id=0.""" + import msgpack + + mock_startup = MagicMock() + mock_startup.model_dump.return_value = {"type": "StartupDetails"} + + mock_socket = MagicMock(spec=socket.socket) + + _send_startup_details(mock_socket, mock_startup) + + sent_bytes = mock_socket.sendall.call_args[0][0] + # Frame is encoded as [id, body, error] + frame = msgpack.unpackb(sent_bytes[4:]) + assert frame[0] == 0 + + def test_frame_body_matches_model_dump(self): + """The frame body should be the model_dump(mode='json') output.""" + import msgpack + + body = {"type": "StartupDetails", "ti": {"task_id": "t1"}, "dag_rel_path": "test.jar"} + mock_startup = MagicMock() + mock_startup.model_dump.return_value = body + + mock_socket = MagicMock(spec=socket.socket) + + _send_startup_details(mock_socket, mock_startup) + + sent_bytes = mock_socket.sendall.call_args[0][0] + # Frame is encoded as [id, body, error] + frame = msgpack.unpackb(sent_bytes[4:]) + assert frame[1] == body + + def test_real_socket_roundtrip(self): + """Send through real sockets and verify the frame is receivable.""" + import msgpack + + server = socket.socket() + server.bind(("127.0.0.1", 0)) + server.listen(1) + addr = server.getsockname() + + client = socket.socket() + client.connect(addr) + conn, _ = server.accept() + + try: + body = {"type": "StartupDetails", "value": 42} + mock_startup = MagicMock() + mock_startup.model_dump.return_value = body + + _send_startup_details(conn, mock_startup) + + # Read the length prefix + length_bytes = client.recv(4) + length = int.from_bytes(length_bytes, "big") + + # Read the payload — frame is [id, body, error] + data = client.recv(length) + frame = msgpack.unpackb(data) + assert frame[0] == 0 + assert frame[1] == body + finally: + conn.close() + client.close() + server.close() + + +class TestBaseRuntimeCoordinatorDefaults: + def test_can_handle_dag_file_returns_false(self): + assert BaseRuntimeCoordinator.can_handle_dag_file("bundle", "/path/to/dag.py") is False + + def test_get_code_from_file_raises_not_implemented(self): + with pytest.raises(NotImplementedError): + BaseRuntimeCoordinator.get_code_from_file("/path/to/dag.jar") + + def test_dag_parsing_runtime_cmd_raises_not_implemented(self): + with pytest.raises(NotImplementedError): + BaseRuntimeCoordinator.dag_parsing_runtime_cmd( + dag_file_path="/dag.jar", + bundle_name="b", + bundle_path="/path", + comm_addr="127.0.0.1:1234", + logs_addr="127.0.0.1:1235", + ) + + def test_task_execution_runtime_cmd_raises_not_implemented(self): + with pytest.raises(NotImplementedError): + BaseRuntimeCoordinator.task_execution_runtime_cmd( + what=MagicMock(), + dag_file_path="/dag.jar", + bundle_path="/path", + bundle_info=MagicMock(), + comm_addr="127.0.0.1:1234", + logs_addr="127.0.0.1:1235", + ) + + +class TestCoordinatorNamedTuples: + def test_dag_parsing_info_defaults(self): + info = BaseRuntimeCoordinator.DagParsingInfo( + dag_file_path="/dag.jar", + bundle_name="my-bundle", + bundle_path="/bundles/my-bundle", + ) + assert info.mode == "dag-parsing" + assert info.dag_file_path == "/dag.jar" + assert info.bundle_name == "my-bundle" + assert info.bundle_path == "/bundles/my-bundle" + + def test_task_execution_info_defaults(self): + mock_ti = MagicMock() + mock_bundle = MagicMock() + mock_startup = MagicMock() + info = BaseRuntimeCoordinator.TaskExecutionInfo( + what=mock_ti, + dag_rel_path="dags/example.jar", + bundle_info=mock_bundle, + startup_details=mock_startup, + ) + assert info.mode == "task-execution" + assert info.what is mock_ti + assert info.dag_rel_path == "dags/example.jar" + + +class TestBridge: + def test_bridge_forwards_comm_bidirectionally(self): + """Verify _bridge sets up bidirectional forwarding and processes all channels.""" + # Use real socketpairs for the 4 channels + sup_send, sup_recv = socket.socketpair() + rt_send, rt_recv = socket.socketpair() + log_send, log_recv = socket.socketpair() + stderr_send, stderr_recv = socket.socketpair() + + mock_proc = MagicMock(spec=subprocess.Popen) + # Make the process "exit" immediately so the bridge drains and stops + mock_proc.poll.return_value = 0 + mock_log = MagicMock() + + try: + # Send data before starting the bridge + sup_send.sendall(b"from_supervisor") + rt_send.sendall(b"from_runtime") + log_send.sendall(b'{"event":"hello","level":"info"}\n') + stderr_send.sendall(b"stderr line\n") + + # Close sending sides so the bridge will see EOF + sup_send.close() + rt_send.close() + log_send.close() + stderr_send.close() + + _bridge(sup_recv, rt_recv, log_recv, stderr_recv, mock_proc, mock_log) + + # If we got here without hanging, the bridge correctly processed all channels + finally: + for s in (sup_send, rt_send, log_send, stderr_send, sup_recv, rt_recv, log_recv, stderr_recv): + try: + s.close() + except OSError: + pass + + def test_bridge_drains_after_process_exit(self): + """Verify _bridge drains remaining data after the subprocess exits.""" + sup_local, sup_remote = socket.socketpair() + rt_local, rt_remote = socket.socketpair() + log_local, log_remote = socket.socketpair() + stderr_local, stderr_remote = socket.socketpair() + + mock_proc = MagicMock(spec=subprocess.Popen) + # First poll: still running; subsequent: exited + mock_proc.poll.side_effect = [None, 0, 0, 0, 0, 0, 0, 0, 0, 0] + mock_log = MagicMock() + + try: + # Send data after bridge starts its first iteration + stderr_local.sendall(b"error output\n") + stderr_local.close() + sup_local.close() + rt_local.close() + log_local.close() + + _bridge(sup_remote, rt_remote, log_remote, stderr_remote, mock_proc, mock_log) + finally: + for s in ( + sup_local, + sup_remote, + rt_local, + rt_remote, + log_local, + log_remote, + stderr_local, + stderr_remote, + ): + try: + s.close() + except OSError: + pass + + def test_bridge_closes_all_sockets(self): + """Verify _bridge closes all four sockets when done.""" + sup = MagicMock(spec=socket.socket) + rt = MagicMock(spec=socket.socket) + logs = MagicMock(spec=socket.socket) + stderr = MagicMock(spec=socket.socket) + + mock_proc = MagicMock(spec=subprocess.Popen) + mock_proc.poll.return_value = 0 + mock_log = MagicMock() + + # Patch the selector to avoid real I/O; service_selector is imported inside + # _bridge so we patch it on the selector_loop module + with ( + patch("airflow.sdk.execution_time.coordinator.selectors.DefaultSelector") as mock_sel_cls, + patch("airflow.sdk.execution_time.selector_loop.service_selector"), + ): + mock_sel = MagicMock() + mock_sel_cls.return_value = mock_sel + # Empty selector map so the while loop exits immediately + mock_sel.get_map.return_value = {} + + _bridge(sup, rt, logs, stderr, mock_proc, mock_log) + + sup.close.assert_called() + rt.close.assert_called() + logs.close.assert_called() + stderr.close.assert_called() + mock_sel.close.assert_called_once() + + +class TestRunDagParsing: + @patch.object(BaseRuntimeCoordinator, "_runtime_subprocess_entrypoint") + def test_run_dag_parsing_creates_dag_parsing_info(self, mock_entrypoint): + BaseRuntimeCoordinator.run_dag_parsing( + path="/bundles/my-bundle/dags/example.jar", + bundle_name="my-bundle", + bundle_path="/bundles/my-bundle", + ) + + mock_entrypoint.assert_called_once() + info = mock_entrypoint.call_args[0][0] + assert isinstance(info, BaseRuntimeCoordinator.DagParsingInfo) + assert info.dag_file_path == "/bundles/my-bundle/dags/example.jar" + assert info.bundle_name == "my-bundle" + assert info.bundle_path == "/bundles/my-bundle" + assert info.mode == "dag-parsing" + + +class TestRunTaskExecution: + @patch.object(BaseRuntimeCoordinator, "_runtime_subprocess_entrypoint") + def test_run_task_execution_creates_task_execution_info(self, mock_entrypoint): + mock_ti = MagicMock() + mock_bundle_info = MagicMock() + mock_startup = MagicMock() + + BaseRuntimeCoordinator.run_task_execution( + what=mock_ti, + dag_rel_path="dags/example.jar", + bundle_info=mock_bundle_info, + startup_details=mock_startup, + ) + + mock_entrypoint.assert_called_once() + info = mock_entrypoint.call_args[0][0] + assert isinstance(info, BaseRuntimeCoordinator.TaskExecutionInfo) + assert info.what is mock_ti + assert info.dag_rel_path == "dags/example.jar" + assert info.bundle_info is mock_bundle_info + assert info.startup_details is mock_startup + assert info.mode == "task-execution" + + +class TestRuntimeSubprocessEntrypoint: + def test_unknown_entrypoint_info_type_raises(self): + class TestCoordinator(BaseRuntimeCoordinator): + runtime_name = "test" + file_extension = ".test" + + # Needs a 'mode' attribute (accessed during logging) but must not be + # an instance of DagParsingInfo or TaskExecutionInfo. + fake_info = MagicMock() + fake_info.mode = "unknown" + + with pytest.raises(ValueError, match="Unknown entrypoint_info type"): + TestCoordinator._runtime_subprocess_entrypoint(fake_info) # type: ignore[arg-type] + + @patch("airflow.sdk.execution_time.coordinator._bridge") + @patch("airflow.sdk.execution_time.coordinator._send_startup_details") + @patch("subprocess.Popen", autospec=True) + @patch("airflow.sdk.execution_time.coordinator._start_server") + @patch("os.dup", return_value=99) + def test_dag_parsing_flow(self, mock_dup, mock_start_server, mock_popen, mock_send_startup, mock_bridge): + """Verify the dag-parsing entrypoint wires up servers, spawns subprocess, and bridges.""" + # Set up mock servers + comm_server = MagicMock(spec=socket.socket) + comm_server.getsockname.return_value = ("127.0.0.1", 5000) + logs_server = MagicMock(spec=socket.socket) + logs_server.getsockname.return_value = ("127.0.0.1", 5001) + mock_start_server.side_effect = [comm_server, logs_server] + + # The runtime connects back + runtime_comm = MagicMock(spec=socket.socket) + runtime_logs = MagicMock(spec=socket.socket) + comm_server.accept.return_value = (runtime_comm, ("127.0.0.1", 9000)) + logs_server.accept.return_value = (runtime_logs, ("127.0.0.1", 9001)) + + # Mock socketpair for stderr + child_stderr = MagicMock(spec=socket.socket) + read_stderr = MagicMock(spec=socket.socket) + child_stderr.fileno.return_value = 10 + + # Mock supervisor_comm created from os.dup(0) + supervisor_comm = MagicMock(spec=socket.socket) + + class TestCoordinator(BaseRuntimeCoordinator): + runtime_name = "test" + file_extension = ".test" + + @classmethod + def dag_parsing_runtime_cmd(cls, **kwargs): + return ["test-runtime", "--parse", kwargs["dag_file_path"]] + + info = BaseRuntimeCoordinator.DagParsingInfo( + dag_file_path="/dag.test", + bundle_name="test-bundle", + bundle_path="/bundles/test-bundle", + ) + + with ( + patch("socket.socketpair", return_value=(child_stderr, read_stderr)), + patch("airflow.sdk.execution_time.coordinator.socket.socket", return_value=supervisor_comm), + ): + TestCoordinator._runtime_subprocess_entrypoint(info) + + # Subprocess spawned + mock_popen.assert_called_once() + cmd = mock_popen.call_args[0][0] + assert cmd == ["test-runtime", "--parse", "/dag.test"] + + # Servers accepted and closed + comm_server.accept.assert_called_once() + logs_server.accept.assert_called_once() + comm_server.close.assert_called_once() + logs_server.close.assert_called_once() + + # stderr child side closed after Popen + child_stderr.close.assert_called_once() + + # _send_startup_details NOT called for dag parsing + mock_send_startup.assert_not_called() + + # _bridge called with the supervisor_comm socket + mock_bridge.assert_called_once() + assert mock_bridge.call_args[0][0] is supervisor_comm + + @patch("airflow.sdk.execution_time.coordinator._bridge") + @patch("airflow.sdk.execution_time.coordinator._send_startup_details") + @patch("subprocess.Popen", autospec=True) + @patch("airflow.sdk.execution_time.coordinator._start_server") + @patch("os.dup", return_value=99) + @patch("airflow.sdk.execution_time.task_runner.resolve_bundle") + @patch("airflow.dag_processing.bundles.base.BundleVersionLock", autospec=True) + def test_task_execution_flow( + self, + mock_bundle_lock, + mock_resolve_bundle, + mock_dup, + mock_start_server, + mock_popen, + mock_send_startup, + mock_bridge, + ): + """Verify the task-execution entrypoint resolves bundle, sends startup details, and bridges.""" + # Mock servers + comm_server = MagicMock(spec=socket.socket) + comm_server.getsockname.return_value = ("127.0.0.1", 6000) + logs_server = MagicMock(spec=socket.socket) + logs_server.getsockname.return_value = ("127.0.0.1", 6001) + mock_start_server.side_effect = [comm_server, logs_server] + + runtime_comm = MagicMock(spec=socket.socket) + runtime_logs = MagicMock(spec=socket.socket) + comm_server.accept.return_value = (runtime_comm, ("127.0.0.1", 9000)) + logs_server.accept.return_value = (runtime_logs, ("127.0.0.1", 9001)) + + child_stderr = MagicMock(spec=socket.socket) + read_stderr = MagicMock(spec=socket.socket) + child_stderr.fileno.return_value = 10 + + # Mock resolved bundle + mock_bundle_instance = MagicMock() + mock_bundle_instance.path = "/resolved/bundles/test-bundle" + mock_resolve_bundle.return_value = mock_bundle_instance + + # BundleVersionLock as context manager + mock_lock_instance = MagicMock() + mock_bundle_lock.return_value = mock_lock_instance + mock_lock_instance.__enter__ = MagicMock(return_value=mock_lock_instance) + mock_lock_instance.__exit__ = MagicMock(return_value=False) + + mock_ti = MagicMock() + mock_bundle_info = MagicMock() + mock_bundle_info.name = "test-bundle" + mock_bundle_info.version = "v1" + mock_startup = MagicMock() + + class TestCoordinator(BaseRuntimeCoordinator): + runtime_name = "test" + file_extension = ".test" + + @classmethod + def task_execution_runtime_cmd(cls, **kwargs): + return ["test-runtime", "--execute", kwargs["dag_file_path"]] + + info = BaseRuntimeCoordinator.TaskExecutionInfo( + what=mock_ti, + dag_rel_path="dags/example.test", + bundle_info=mock_bundle_info, + startup_details=mock_startup, + ) + + supervisor_comm = MagicMock(spec=socket.socket) + + with ( + patch("socket.socketpair", return_value=(child_stderr, read_stderr)), + patch("airflow.sdk.execution_time.coordinator.socket.socket", return_value=supervisor_comm), + ): + TestCoordinator._runtime_subprocess_entrypoint(info) + + # Bundle resolved + mock_resolve_bundle.assert_called_once() + + # BundleVersionLock used + mock_bundle_lock.assert_called_once_with(bundle_name="test-bundle", bundle_version="v1") + + # Subprocess spawned with resolved path + mock_popen.assert_called_once() + cmd = mock_popen.call_args[0][0] + assert cmd == ["test-runtime", "--execute", "/resolved/bundles/test-bundle/dags/example.test"] + + # StartupDetails forwarded to the runtime subprocess + mock_send_startup.assert_called_once_with(runtime_comm, mock_startup) + + # _bridge called + mock_bridge.assert_called_once() + + @patch("airflow.sdk.execution_time.coordinator._bridge") + @patch("subprocess.Popen", autospec=True) + @patch("airflow.sdk.execution_time.coordinator._start_server") + @patch("os.dup", return_value=99) + def test_sets_process_context_env_var(self, mock_dup, mock_start_server, mock_popen, mock_bridge): + """Verify _AIRFLOW_PROCESS_CONTEXT is set to 'client'.""" + comm_server = MagicMock(spec=socket.socket) + comm_server.getsockname.return_value = ("127.0.0.1", 7000) + logs_server = MagicMock(spec=socket.socket) + logs_server.getsockname.return_value = ("127.0.0.1", 7001) + mock_start_server.side_effect = [comm_server, logs_server] + + runtime_comm = MagicMock(spec=socket.socket) + runtime_logs = MagicMock(spec=socket.socket) + comm_server.accept.return_value = (runtime_comm, ("127.0.0.1", 9000)) + logs_server.accept.return_value = (runtime_logs, ("127.0.0.1", 9001)) + + child_stderr = MagicMock(spec=socket.socket) + read_stderr = MagicMock(spec=socket.socket) + child_stderr.fileno.return_value = 10 + + class TestCoordinator(BaseRuntimeCoordinator): + runtime_name = "test" + file_extension = ".test" + + @classmethod + def dag_parsing_runtime_cmd(cls, **kwargs): + return ["echo", "test"] + + info = BaseRuntimeCoordinator.DagParsingInfo( + dag_file_path="/dag.test", + bundle_name="b", + bundle_path="/path", + ) + + supervisor_comm = MagicMock(spec=socket.socket) + + old_val = os.environ.get("_AIRFLOW_PROCESS_CONTEXT") + try: + with ( + patch("socket.socketpair", return_value=(child_stderr, read_stderr)), + patch("airflow.sdk.execution_time.coordinator.socket.socket", return_value=supervisor_comm), + ): + TestCoordinator._runtime_subprocess_entrypoint(info) + assert os.environ["_AIRFLOW_PROCESS_CONTEXT"] == "client" + finally: + if old_val is None: + os.environ.pop("_AIRFLOW_PROCESS_CONTEXT", None) + else: + os.environ["_AIRFLOW_PROCESS_CONTEXT"] = old_val diff --git a/task-sdk/tests/task_sdk/execution_time/test_selector_loop.py b/task-sdk/tests/task_sdk/execution_time/test_selector_loop.py new file mode 100644 index 0000000000000..efbfa83adecf8 --- /dev/null +++ b/task-sdk/tests/task_sdk/execution_time/test_selector_loop.py @@ -0,0 +1,479 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import selectors +import socket +from unittest.mock import MagicMock + +import pytest + +from airflow.sdk.execution_time.selector_loop import ( + make_buffered_socket_reader, + make_raw_forwarder, + service_selector, +) + + +def _make_generator(): + """Return a generator that collects sent lines into a list.""" + received: list[bytes | bytearray] = [] + + def gen(): + while True: + line = yield + received.append(bytes(line)) + + g = gen() + return g, received + + +def _make_socket_pair(): + """Create a connected TCP socket pair on localhost.""" + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.bind(("127.0.0.1", 0)) + server.listen(1) + addr = server.getsockname() + + client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client.connect(addr) + conn, _ = server.accept() + server.close() + return client, conn + + +class TestMakeBufferedSocketReader: + def test_single_complete_line(self): + gen, received = _make_generator() + on_close = MagicMock() + handler, returned_on_close = make_buffered_socket_reader(gen, on_close) + + sock = MagicMock(spec=socket.socket) + # recv_into writes data and returns count + data = b"hello world\n" + sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, data) + + result = handler(sock) + + assert result is True + assert received == [b"hello world\n"] + assert returned_on_close is on_close + + def test_multiple_lines_in_single_recv(self): + gen, received = _make_generator() + on_close = MagicMock() + handler, _ = make_buffered_socket_reader(gen, on_close) + + sock = MagicMock(spec=socket.socket) + data = b"line1\nline2\nline3\n" + sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, data) + + result = handler(sock) + + assert result is True + assert received == [b"line1\n", b"line2\n", b"line3\n"] + + def test_partial_line_accumulated_across_calls(self): + gen, received = _make_generator() + on_close = MagicMock() + handler, _ = make_buffered_socket_reader(gen, on_close) + + sock = MagicMock(spec=socket.socket) + + # First call: partial line (no newline) + sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"hell") + result = handler(sock) + assert result is True + assert received == [] + + # Second call: rest of the line + sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"o\n") + result = handler(sock) + assert result is True + assert received == [b"hello\n"] + + def test_eof_flushes_remaining_buffer(self): + gen, received = _make_generator() + on_close = MagicMock() + handler, _ = make_buffered_socket_reader(gen, on_close) + + sock = MagicMock(spec=socket.socket) + + # Send partial data (no newline) + sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"leftover") + handler(sock) + assert received == [] + + # EOF (recv_into returns 0) — clear side_effect so return_value takes effect + sock.recv_into.side_effect = None + sock.recv_into.return_value = 0 + result = handler(sock) + + assert result is False + assert received == [b"leftover"] + + def test_eof_with_empty_buffer(self): + gen, received = _make_generator() + on_close = MagicMock() + handler, _ = make_buffered_socket_reader(gen, on_close) + + sock = MagicMock(spec=socket.socket) + sock.recv_into.return_value = 0 + + result = handler(sock) + + assert result is False + assert received == [] + + def test_generator_stop_iteration_returns_false(self): + """If the generator is exhausted, handler returns False.""" + + def limited_gen(): + yield # startup + yield # receive one line, then stop + + gen = limited_gen() + on_close = MagicMock() + handler, _ = make_buffered_socket_reader(gen, on_close) + + sock = MagicMock(spec=socket.socket) + # First line succeeds + sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"line1\n") + result = handler(sock) + assert result is True + + # Second line triggers StopIteration in the generator + sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"line2\n") + result = handler(sock) + assert result is False + + def test_mixed_complete_and_partial_lines(self): + gen, received = _make_generator() + on_close = MagicMock() + handler, _ = make_buffered_socket_reader(gen, on_close) + + sock = MagicMock(spec=socket.socket) + # Data contains one complete line and a partial line + sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"complete\npart") + handler(sock) + assert received == [b"complete\n"] + + # Finish the partial line + sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"ial\n") + handler(sock) + assert received == [b"complete\n", b"partial\n"] + + def test_custom_buffer_size(self): + gen, received = _make_generator() + on_close = MagicMock() + handler, _ = make_buffered_socket_reader(gen, on_close, buffer_size=8) + + sock = MagicMock(spec=socket.socket) + # Data larger than buffer_size — recv_into only reads buffer_size bytes + full_data = b"abcdefghijklmnop\n" + # Simulate chunked reads + sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, full_data[: len(buf)]) + handler(sock) + # Only first 8 bytes read, no newline yet + assert received == [] + + sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, full_data[8:16]) + handler(sock) + assert received == [] + + sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, full_data[16:]) + handler(sock) + assert received == [b"abcdefghijklmnop\n"] + + +def _fill_buffer(buf: bytearray, data: bytes) -> int: + """Helper to simulate socket.recv_into by filling the buffer.""" + n = min(len(data), len(buf)) + buf[:n] = data[:n] + return n + + +class TestMakeRawForwarder: + def test_forwards_data_to_dest(self): + on_close = MagicMock() + dest = MagicMock(spec=socket.socket) + handler, returned_on_close = make_raw_forwarder(dest, on_close) + + src = MagicMock(spec=socket.socket) + src.recv.return_value = b"hello" + + result = handler(src) + + assert result is True + dest.sendall.assert_called_once_with(b"hello") + assert returned_on_close is on_close + + def test_eof_returns_false(self): + on_close = MagicMock() + dest = MagicMock(spec=socket.socket) + handler, _ = make_raw_forwarder(dest, on_close) + + src = MagicMock(spec=socket.socket) + src.recv.return_value = b"" + + result = handler(src) + + assert result is False + dest.sendall.assert_not_called() + + @pytest.mark.parametrize( + "exception", + [BrokenPipeError, ConnectionResetError, OSError], + ids=["broken_pipe", "connection_reset", "os_error"], + ) + def test_sendall_exception_returns_false(self, exception): + on_close = MagicMock() + dest = MagicMock(spec=socket.socket) + dest.sendall.side_effect = exception + handler, _ = make_raw_forwarder(dest, on_close) + + src = MagicMock(spec=socket.socket) + src.recv.return_value = b"data" + + result = handler(src) + + assert result is False + + def test_multiple_forwards(self): + on_close = MagicMock() + dest = MagicMock(spec=socket.socket) + handler, _ = make_raw_forwarder(dest, on_close) + + src = MagicMock(spec=socket.socket) + + for chunk in [b"chunk1", b"chunk2", b"chunk3"]: + src.recv.return_value = chunk + assert handler(src) is True + + assert dest.sendall.call_count == 3 + + +class TestServiceSelector: + def test_calls_handler_for_ready_sockets(self): + sel = MagicMock(spec=selectors.DefaultSelector) + handler = MagicMock(return_value=True) + on_close = MagicMock() + sock = MagicMock(spec=socket.socket) + + key = MagicMock() + key.data = (handler, on_close) + key.fileobj = sock + + sel.select.return_value = [(key, selectors.EVENT_READ)] + + service_selector(sel, timeout=1.0) + + handler.assert_called_once_with(sock) + on_close.assert_not_called() + sock.close.assert_not_called() + + def test_on_close_and_sock_close_when_handler_returns_false(self): + sel = MagicMock(spec=selectors.DefaultSelector) + handler = MagicMock(return_value=False) + on_close = MagicMock() + sock = MagicMock(spec=socket.socket) + + key = MagicMock() + key.data = (handler, on_close) + key.fileobj = sock + + sel.select.return_value = [(key, selectors.EVENT_READ)] + + service_selector(sel, timeout=1.0) + + handler.assert_called_once_with(sock) + on_close.assert_called_once_with(sock) + sock.close.assert_called_once() + + @pytest.mark.parametrize( + "exception", + [BrokenPipeError, ConnectionResetError], + ids=["broken_pipe", "connection_reset"], + ) + def test_pipe_errors_treated_as_eof(self, exception): + sel = MagicMock(spec=selectors.DefaultSelector) + handler = MagicMock(side_effect=exception) + on_close = MagicMock() + sock = MagicMock(spec=socket.socket) + + key = MagicMock() + key.data = (handler, on_close) + key.fileobj = sock + + sel.select.return_value = [(key, selectors.EVENT_READ)] + + service_selector(sel, timeout=1.0) + + on_close.assert_called_once_with(sock) + sock.close.assert_called_once() + + def test_empty_selector_no_events(self): + sel = MagicMock(spec=selectors.DefaultSelector) + sel.select.return_value = [] + + # Should not raise + service_selector(sel, timeout=1.0) + + @pytest.mark.parametrize( + ("input_timeout", "expected_min"), + [ + (0.0, 0.01), + (-1.0, 0.01), + (-100.0, 0.01), + (0.5, 0.5), + (2.0, 2.0), + ], + ids=["zero", "negative", "very_negative", "positive_half", "positive_two"], + ) + def test_timeout_clamped_to_minimum(self, input_timeout, expected_min): + sel = MagicMock(spec=selectors.DefaultSelector) + sel.select.return_value = [] + + service_selector(sel, timeout=input_timeout) + + sel.select.assert_called_once() + actual_timeout = sel.select.call_args[1].get("timeout") or sel.select.call_args[0][0] + assert actual_timeout == pytest.approx(expected_min) + + def test_multiple_ready_sockets(self): + sel = MagicMock(spec=selectors.DefaultSelector) + + handler1 = MagicMock(return_value=True) + on_close1 = MagicMock() + sock1 = MagicMock(spec=socket.socket) + key1 = MagicMock() + key1.data = (handler1, on_close1) + key1.fileobj = sock1 + + handler2 = MagicMock(return_value=False) + on_close2 = MagicMock() + sock2 = MagicMock(spec=socket.socket) + key2 = MagicMock() + key2.data = (handler2, on_close2) + key2.fileobj = sock2 + + sel.select.return_value = [(key1, selectors.EVENT_READ), (key2, selectors.EVENT_READ)] + + service_selector(sel, timeout=1.0) + + # First socket: handler returns True, stays open + handler1.assert_called_once_with(sock1) + on_close1.assert_not_called() + sock1.close.assert_not_called() + + # Second socket: handler returns False, closed + handler2.assert_called_once_with(sock2) + on_close2.assert_called_once_with(sock2) + sock2.close.assert_called_once() + + +class TestSelectorLoopIntegration: + def test_buffered_reader_with_real_sockets(self): + """End-to-end: send lines through real sockets and verify buffered reading.""" + gen, received = _make_generator() + sender, reader = _make_socket_pair() + try: + sel = selectors.DefaultSelector() + + def on_close(sock): + sel.unregister(sock) + + sel.register(reader, selectors.EVENT_READ, make_buffered_socket_reader(gen, on_close)) + + sender.sendall(b"first line\nsecond line\n") + + service_selector(sel, timeout=1.0) + + assert b"first line\n" in received + assert b"second line\n" in received + + # Close sender, then drain + sender.close() + sender = None + + service_selector(sel, timeout=0.5) + + sel.close() + finally: + if sender: + sender.close() + reader.close() + + def test_raw_forwarder_with_real_sockets(self): + """End-to-end: forward raw bytes between real socket pairs.""" + src_send, src_recv = _make_socket_pair() + # Use socketpair for the destination so reads/writes are symmetric + dst_write, dst_read = socket.socketpair() + try: + sel = selectors.DefaultSelector() + + def on_close(sock): + sel.unregister(sock) + + sel.register(src_recv, selectors.EVENT_READ, make_raw_forwarder(dst_write, on_close)) + + src_send.sendall(b"raw data payload") + + service_selector(sel, timeout=1.0) + + dst_read.setblocking(False) + forwarded = dst_read.recv(4096) + + assert forwarded == b"raw data payload" + + sel.close() + finally: + for s in (src_send, src_recv, dst_write, dst_read): + s.close() + + def test_eof_triggers_on_close_with_real_sockets(self): + """When the sender closes, the selector callback chain fires on_close.""" + gen, received = _make_generator() + sender, reader = _make_socket_pair() + closed_sockets: list[socket.socket] = [] + try: + sel = selectors.DefaultSelector() + + def on_close(sock): + sel.unregister(sock) + closed_sockets.append(sock) + + sel.register(reader, selectors.EVENT_READ, make_buffered_socket_reader(gen, on_close)) + + # Send data then close + sender.sendall(b"final\n") + service_selector(sel, timeout=1.0) + assert received == [b"final\n"] + + sender.close() + sender = None + service_selector(sel, timeout=0.5) + + # on_close should have been called, and socket closed by service_selector + assert len(closed_sockets) == 1 + + sel.close() + finally: + if sender: + sender.close() + reader.close() From e1df49385548fd8ab555f8385cd4e17168099971 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 21 Apr 2026 18:37:27 +0800 Subject: [PATCH 22/75] Move TaskInstanceDTO to share to make task_runner retrieve TI.queue --- airflow-core/pyproject.toml | 2 + airflow-core/src/airflow/_shared/workloads | 1 + .../src/airflow/executors/base_executor.py | 5 +- .../src/airflow/executors/workloads/task.py | 3 +- pyproject.toml | 5 + shared/workloads/.gitignore | 1 + shared/workloads/pyproject.toml | 55 + .../src/airflow_shared/workloads/__init__.py | 48 + shared/workloads/uv.lock | 2125 +++++++++++++++++ task-sdk/pyproject.toml | 2 + task-sdk/src/airflow/sdk/_shared/workloads | 1 + .../src/airflow/sdk/execution_time/comms.py | 4 +- .../airflow/sdk/execution_time/coordinator.py | 9 +- .../airflow/sdk/execution_time/supervisor.py | 7 +- .../airflow/sdk/execution_time/task_runner.py | 5 +- .../execution_time/test_supervisor.py | 25 +- uv.lock | 23 + 17 files changed, 2302 insertions(+), 19 deletions(-) create mode 120000 airflow-core/src/airflow/_shared/workloads create mode 100644 shared/workloads/.gitignore create mode 100644 shared/workloads/pyproject.toml create mode 100644 shared/workloads/src/airflow_shared/workloads/__init__.py create mode 100644 shared/workloads/uv.lock create mode 120000 task-sdk/src/airflow/sdk/_shared/workloads diff --git a/airflow-core/pyproject.toml b/airflow-core/pyproject.toml index 664ff7d905f97..f89c0f88d8155 100644 --- a/airflow-core/pyproject.toml +++ b/airflow-core/pyproject.toml @@ -250,6 +250,7 @@ exclude = [ "../shared/serialization/src/airflow_shared/serialization" = "src/airflow/_shared/serialization" "../shared/state/src/airflow_shared/state" = "src/airflow/_shared/state" "../shared/timezones/src/airflow_shared/timezones" = "src/airflow/_shared/timezones" +"../shared/workloads/src/airflow_shared/workloads" = "src/airflow/_shared/workloads" "../shared/listeners/src/airflow_shared/listeners" = "src/airflow/_shared/listeners" "../shared/plugins_manager/src/airflow_shared/plugins_manager" = "src/airflow/_shared/plugins_manager" "../shared/providers_discovery/src/airflow_shared/providers_discovery" = "src/airflow/_shared/providers_discovery" @@ -339,6 +340,7 @@ shared_distributions = [ "apache-airflow-shared-serialization", "apache-airflow-shared-state", "apache-airflow-shared-timezones", + "apache-airflow-shared-workloads", "apache-airflow-shared-plugins-manager", "apache-airflow-shared-providers-discovery", ] diff --git a/airflow-core/src/airflow/_shared/workloads b/airflow-core/src/airflow/_shared/workloads new file mode 120000 index 0000000000000..f25d9e16ea1d9 --- /dev/null +++ b/airflow-core/src/airflow/_shared/workloads @@ -0,0 +1 @@ +../../../../shared/workloads/src/airflow_shared/workloads \ No newline at end of file diff --git a/airflow-core/src/airflow/executors/base_executor.py b/airflow-core/src/airflow/executors/base_executor.py index eff6ff0771474..18011d72917fb 100644 --- a/airflow-core/src/airflow/executors/base_executor.py +++ b/airflow-core/src/airflow/executors/base_executor.py @@ -70,6 +70,7 @@ def get_execution_api_server_url(conf_source: AirflowConfigParser | ExecutorConf from sqlalchemy.orm import Session + from airflow._shared.workloads import TaskInstanceDTO from airflow.api_fastapi.auth.tokens import JWTGenerator from airflow.callbacks.base_callback_sink import BaseCallbackSink from airflow.callbacks.callback_requests import CallbackRequest @@ -653,10 +654,8 @@ def run_workload( if isinstance(workload, ExecuteTask): from airflow.sdk.execution_time.supervisor import supervise_task - # workload.ti is a TaskInstanceDTO which duck-types as TaskInstance. - # TODO: Create a protocol for this. return supervise_task( - ti=workload.ti, # type: ignore[arg-type] + ti=cast("TaskInstanceDTO", workload.ti), bundle_info=workload.bundle_info, dag_rel_path=workload.dag_rel_path, token=workload.token, diff --git a/airflow-core/src/airflow/executors/workloads/task.py b/airflow-core/src/airflow/executors/workloads/task.py index 41ec428d4fb0a..5309eed1a42f8 100644 --- a/airflow-core/src/airflow/executors/workloads/task.py +++ b/airflow-core/src/airflow/executors/workloads/task.py @@ -18,7 +18,6 @@ from __future__ import annotations -import uuid from pathlib import Path from typing import TYPE_CHECKING, Literal @@ -28,6 +27,8 @@ from airflow.utils.state import TaskInstanceState if TYPE_CHECKING: + import uuid + from airflow.api_fastapi.auth.tokens import JWTGenerator from airflow.models.taskinstance import TaskInstance as TIModel from airflow.models.taskinstancekey import TaskInstanceKey diff --git a/pyproject.toml b/pyproject.toml index f1c6e8b5fdfca..3de5c2b45a418 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1351,6 +1351,7 @@ dev = [ "apache-airflow-shared-state", "apache-airflow-shared-template-rendering", "apache-airflow-shared-timezones", + "apache-airflow-shared-workloads", ] # To build docs: @@ -1532,6 +1533,7 @@ apache-airflow-shared-serialization = false apache-airflow-shared-state = false apache-airflow-shared-template-rendering = false apache-airflow-shared-timezones = false +apache-airflow-shared-workloads = false apache-airflow-task-sdk = false apache-airflow-task-sdk-integration-tests = false apache-aurflow-docker-stack = false @@ -1683,6 +1685,7 @@ apache-airflow-shared-serialization = false apache-airflow-shared-state = false apache-airflow-shared-template-rendering = false apache-airflow-shared-timezones = false +apache-airflow-shared-workloads = false apache-airflow-task-sdk = false apache-airflow-task-sdk-integration-tests = false apache-aurflow-docker-stack = false @@ -1728,6 +1731,7 @@ apache-airflow-shared-serialization = { workspace = true } apache-airflow-shared-state = { workspace = true } apache-airflow-shared-template-rendering = { workspace = true } apache-airflow-shared-timezones = { workspace = true } +apache-airflow-shared-workloads = { workspace = true } # Automatically generated provider workspace items (update_airflow_pyproject_toml.py) apache-airflow-providers-airbyte = { workspace = true } apache-airflow-providers-akeyless = { workspace = true } @@ -1866,6 +1870,7 @@ members = [ "shared/state", "shared/template_rendering", "shared/timezones", + "shared/workloads", # Automatically generated provider workspace members (update_airflow_pyproject_toml.py) "providers/airbyte", "providers/akeyless", diff --git a/shared/workloads/.gitignore b/shared/workloads/.gitignore new file mode 100644 index 0000000000000..bff2d7629604d --- /dev/null +++ b/shared/workloads/.gitignore @@ -0,0 +1 @@ +*.iml diff --git a/shared/workloads/pyproject.toml b/shared/workloads/pyproject.toml new file mode 100644 index 0000000000000..1a1c1dded04be --- /dev/null +++ b/shared/workloads/pyproject.toml @@ -0,0 +1,55 @@ +# 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. + +[project] +name = "apache-airflow-shared-workloads" +description = "Shared workload data transfer objects for Airflow distributions" +version = "0.0" +classifiers = [ + "Private :: Do Not Upload", +] + +dependencies = [ + "pydantic>=2.10.0", +] + +[dependency-groups] +dev = [ + "apache-airflow-devel-common", +] + +[build-system] +requires = [ + "hatchling==1.29.0", + "packaging==26.0", + "pathspec==1.0.4", + "pluggy==1.6.0", + "tomli==2.4.1; python_version < '3.11'", + "trove-classifiers==2026.1.14.14", +] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/airflow_shared"] + +[tool.ruff] +extend = "../../pyproject.toml" +src = ["src"] + +[tool.ruff.lint.per-file-ignores] +# Ignore Doc rules et al for anything outside of tests +"!src/*" = ["D", "S101", "TRY002"] diff --git a/shared/workloads/src/airflow_shared/workloads/__init__.py b/shared/workloads/src/airflow_shared/workloads/__init__.py new file mode 100644 index 0000000000000..3fc5ebe5277c1 --- /dev/null +++ b/shared/workloads/src/airflow_shared/workloads/__init__.py @@ -0,0 +1,48 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""Shared workload data transfer objects for Airflow distributions.""" + +from __future__ import annotations + +import uuid + +from pydantic import BaseModel, Field + + +class TaskInstanceDTO(BaseModel): + """Schema for TaskInstance with minimal required fields needed for Executors and Task SDK.""" + + id: uuid.UUID + dag_version_id: uuid.UUID + task_id: str + dag_id: str + run_id: str + try_number: int + map_index: int = -1 + + pool_slots: int + queue: str + priority_weight: int + executor_config: dict | None = Field(default=None, exclude=True) + + language: str | None = None + + parent_context_carrier: dict | None = None + context_carrier: dict | None = None + + +__all__ = ["TaskInstanceDTO"] diff --git a/shared/workloads/uv.lock b/shared/workloads/uv.lock new file mode 100644 index 0000000000000..60097a87185e6 --- /dev/null +++ b/shared/workloads/uv.lock @@ -0,0 +1,2125 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[options] +exclude-newer = "2026-04-17T08:44:15.45316Z" +exclude-newer-span = "P4D" + +[options.exclude-newer-package] +apache-airflow-providers-informatica = false +apache-airflow-providers-amazon = false +apache-airflow-providers-elasticsearch = false +apache-airflow-providers-microsoft-winrm = false +apache-airflow-docker-tests = false +apache-airflow-providers = false +apache-airflow-providers-fab = false +apache-airflow-providers-openlineage = false +apache-airflow-providers-sftp = false +apache-airflow-e2e-tests = false +apache-airflow-shared-logging = false +apache-airflow-providers-apache-drill = false +apache-airflow-providers-pgvector = false +apache-airflow-providers-imap = false +apache-airflow-providers-qdrant = false +apache-airflow-providers-edge3 = false +apache-airflow-providers-neo4j = false +apache-airflow-providers-discord = false +apache-airflow-providers-opensearch = false +apache-airflow-providers-samba = false +apache-airflow-providers-arangodb = false +apache-airflow-providers-apache-spark = false +apache-airflow-providers-ftp = false +apache-airflow-helm-tests = false +apache-airflow-providers-jenkins = false +apache-airflow-shared-listeners = false +apache-airflow-providers-telegram = false +apache-airflow-shared-providers-discovery = false +apache-airflow-providers-celery = false +apache-airflow-providers-docker = false +apache-airflow-providers-sendgrid = false +apache-airflow-providers-common-ai = false +apache-airflow = false +apache-airflow-shared-observability = false +apache-airflow-dev = false +apache-airflow-providers-dbt-cloud = false +apache-airflow-providers-openfaas = false +apache-airflow-devel-common = false +apache-airflow-providers-apache-cassandra = false +apache-airflow-providers-asana = false +apache-airflow-providers-oracle = false +apache-airflow-providers-mysql = false +apache-airflow-providers-alibaba = false +apache-airflow-providers-microsoft-mssql = false +apache-airflow-providers-teradata = false +apache-airflow-providers-jdbc = false +apache-airflow-providers-common-io = false +apache-airflow-providers-cohere = false +apache-airflow-providers-pinecone = false +apache-airflow-providers-segment = false +apache-airflow-providers-redis = false +apache-airflow-shared-dagnode = false +apache-airflow-providers-apache-pinot = false +apache-airflow-providers-weaviate = false +apache-airflow-providers-salesforce = false +apache-airflow-providers-ssh = false +apache-airflow-providers-papermill = false +apache-airflow-providers-google = false +apache-airflow-providers-microsoft-psrp = false +apache-airflow-providers-vertica = false +apache-airflow-providers-apache-hdfs = false +apache-airflow-shared-template-rendering = false +apache-airflow-providers-http = false +apache-airflow-providers-slack = false +apache-airflow-providers-vespa = false +apache-airflow-providers-databricks = false +apache-airflow-providers-sqlite = false +apache-airflow-shared-module-loading = false +apache-airflow-providers-yandex = false +apache-airflow-shared-serialization = false +apache-airflow-scripts = false +apache-airflow-providers-exasol = false +apache-airflow-providers-mongo = false +apache-airflow-providers-apprise = false +apache-airflow-providers-apache-impala = false +apache-airflow-ctl = false +apache-airflow-providers-github = false +apache-airflow-providers-snowflake = false +apache-airflow-providers-zendesk = false +apache-airflow-providers-presto = false +apache-airflow-providers-airbyte = false +apache-airflow-providers-apache-hive = false +apache-airflow-kubernetes-tests = false +apache-airflow-providers-grpc = false +apache-airflow-providers-apache-druid = false +apache-airflow-providers-cncf-kubernetes = false +apache-airflow-providers-apache-flink = false +apache-airflow-providers-apache-pig = false +apache-airflow-providers-apache-tinkerpop = false +apache-airflow-shared-timezones = false +apache-airflow-providers-apache-iceberg = false +apache-airflow-breeze = false +apache-airflow-providers-opsgenie = false +apache-airflow-providers-apache-livy = false +apache-airflow-core = false +apache-airflow-providers-hashicorp = false +apache-airflow-providers-pagerduty = false +apache-airflow-providers-datadog = false +apache-airflow-providers-apache-kafka = false +apache-airflow-providers-influxdb = false +apache-airflow-providers-keycloak = false +apache-airflow-providers-trino = false +apache-airflow-providers-common-messaging = false +apache-airflow-providers-standard = false +apache-airflow-providers-languages-java = false +apache-airflow-providers-singularity = false +apache-airflow-providers-common-compat = false +apache-airflow-ctl-tests = false +apache-airflow-providers-tableau = false +apache-airflow-providers-common-sql = false +apache-airflow-shared-configuration = false +apache-airflow-providers-facebook = false +apache-airflow-providers-ydb = false +apache-airflow-providers-microsoft-azure = false +apache-airflow-shared-plugins-manager = false +apache-airflow-shared-secrets-backend = false +apache-airflow-shared-secrets-masker = false +apache-airflow-providers-git = false +apache-airflow-task-sdk = false +apache-airflow-providers-atlassian-jira = false +apache-airflow-providers-odbc = false +apache-airflow-providers-postgres = false +apache-airflow-providers-openai = false +apache-airflow-task-sdk-integration-tests = false +apache-airflow-providers-smtp = false +apache-airflow-providers-dingding = false +apache-airflow-providers-apache-kylin = false +apache-airflow-providers-cloudant = false +apache-aurflow-docker-stack = false + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aioresponses" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/03/532bbc645bdebcf3b6af3b25d46655259d66ce69abba7720b71ebfabbade/aioresponses-0.7.8.tar.gz", hash = "sha256:b861cdfe5dc58f3b8afac7b0a6973d5d7b2cb608dd0f6253d16b8ee8eaf6df11", size = 40253, upload-time = "2025-01-19T18:14:03.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b7/584157e43c98aa89810bc2f7099e7e01c728ecf905a66cf705106009228f/aioresponses-0.7.8-py2.py3-none-any.whl", hash = "sha256:b73bd4400d978855e55004b23a3a84cb0f018183bcf066a85ad392800b5b9a94", size = 12518, upload-time = "2025-01-19T18:13:59.633Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "apache-airflow-devel-common" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aioresponses" }, + { name = "black" }, + { name = "coverage" }, + { name = "filelock" }, + { name = "ipdb" }, + { name = "jmespath" }, + { name = "kgb" }, + { name = "mypy" }, + { name = "pdbr" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-custom-exit-code" }, + { name = "pytest-icdiff" }, + { name = "pytest-instafail" }, + { name = "pytest-mock" }, + { name = "pytest-rerunfailures" }, + { name = "pytest-timeouts" }, + { name = "pytest-unordered" }, + { name = "pytest-xdist" }, + { name = "requests-mock" }, + { name = "rich" }, + { name = "ruff" }, + { name = "semver" }, + { name = "time-machine" }, + { name = "types-aiofiles" }, + { name = "types-certifi" }, + { name = "types-croniter" }, + { name = "types-deprecated" }, + { name = "types-docutils" }, + { name = "types-markdown" }, + { name = "types-paramiko" }, + { name = "types-protobuf" }, + { name = "types-pymysql" }, + { name = "types-python-dateutil" }, + { name = "types-python-slugify" }, + { name = "types-pytz" }, + { name = "types-pyyaml" }, + { name = "types-redis" }, + { name = "types-requests" }, + { name = "types-setuptools" }, + { name = "types-tabulate" }, + { name = "types-toml" }, + { name = "wheel" }, + { name = "yamllint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/cf/b1f97e6b6f2e57adda012429c541f1881610061e646945e75dfe7d811a9c/apache_airflow_devel_common-0.1.1.tar.gz", hash = "sha256:b066b2d3fdf940b4c2859af23036a7ef31f8e81694209b1ab0e056530e286232", size = 63846, upload-time = "2025-05-16T20:05:09.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/df/9f05b0e6fc132e59b5dec26f4ec610bc611b5f256495b7db25066f5b743b/apache_airflow_devel_common-0.1.1-py2.py3-none-any.whl", hash = "sha256:951ab206cf2f95768d08ad8c15b1cfaa7df404308c9e32998e3e16d217d2b032", size = 100909, upload-time = "2025-05-16T20:05:07.52Z" }, +] + +[[package]] +name = "apache-airflow-shared-workloads" +version = "0.0" +source = { editable = "." } +dependencies = [ + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "apache-airflow-devel-common" }, +] + +[package.metadata] +requires-dist = [{ name = "pydantic", specifier = ">=2.10.0" }] + +[package.metadata.requires-dev] +dev = [{ name = "apache-airflow-devel-common" }] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "black" +version = "26.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "filelock" +version = "3.28.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/17/6e8890271880903e3538660a21d63a6c1fea969ac71d0d6b608b78727fa9/filelock-3.28.0.tar.gz", hash = "sha256:4ed1010aae813c4ee8d9c660e4792475ee60c4a0ba76073ceaf862bd317e3ca6", size = 56474, upload-time = "2026-04-14T22:54:33.625Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/21/2f728888c45033d34a417bfcd248ea2564c9e08ab1bfd301377cf05d5586/filelock-3.28.0-py3-none-any.whl", hash = "sha256:de9af6712788e7171df1b28b15eba2446c69721433fa427a9bee07b17820a9db", size = 39189, upload-time = "2026-04-14T22:54:32.037Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "icdiff" +version = "2.0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/6d/41c8ff13b67c30ceb9325398ff6bb9fc4b77208c6c1d79db8d5913840bc6/icdiff-2.0.10.tar.gz", hash = "sha256:75a3de5c9af35ab45fb0504df59770c514a12c0d2b2c99e5f9c5c2429957e133", size = 16378, upload-time = "2026-02-08T15:03:26.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/4b/bb94514476624bfff977599e5ef3ac9aea603abb693639ed305b22ed1b35/icdiff-2.0.10-py3-none-any.whl", hash = "sha256:fd8ce592d94261c435e2a410f668a41ec3be4527878f8df01100797021d8edb0", size = 17103, upload-time = "2026-02-08T15:03:25.723Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "ipdb" +version = "0.13.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "decorator" }, + { name = "ipython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/4c/b075da0092003d9a55cf2ecc1cae9384a1ca4f650d51b00fc59875fe76f6/ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4", size = 12130, upload-time = "2023-03-09T15:40:55.021Z" }, +] + +[[package]] +name = "ipython" +version = "9.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "kgb" +version = "7.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/00/9e56dee65ec791a92348fb54e8ced08c4c4db494b0f58cfb34737d087fb4/kgb-7.3.tar.gz", hash = "sha256:b8af7e79cb8b0df5a2ec596010b8e5d014845cfaa9203577b85b99d4df192927", size = 62922, upload-time = "2025-12-11T23:56:24.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d6/1c81a1292fc50ad93d0b145f1c241ecb7d541fba4dcec7166e2e1d99f9cd/kgb-7.3-py2.py3-none-any.whl", hash = "sha256:0b300cd6d234a951f60e54ccda78c99a355393d6ae878d3d5925e726ae2f0450", size = 59662, upload-time = "2025-12-11T23:56:23.699Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/1e/a587a862c766a755a58b62d8c00aed11b74a15dc415c1bf5da7b607b0efd/mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974", size = 2995901, upload-time = "2024-03-08T16:10:12.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/96/40f0f605b1d4e2ad1fb11d21988ce3a3e205886c0fcbd35c9789a214de9a/mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd", size = 10725390, upload-time = "2024-03-08T16:10:01.099Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d2/072e40384b53051106b4fcf03537fb88e2a6ad0757d2ab7f6c8c2f188a69/mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6", size = 9731292, upload-time = "2024-03-08T16:08:48.463Z" }, + { url = "https://files.pythonhosted.org/packages/85/a5/b7dc7eb69eda899fd07e71403b51b598a1f4df0f452d1da5844374082bcd/mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185", size = 12455450, upload-time = "2024-03-08T16:08:57.375Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1b/3e962a201d2f0f57c9fa1990e0dd6076f4f2f94954ab56e4a701ec3cc070/mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913", size = 12530368, upload-time = "2024-03-08T16:09:17.061Z" }, + { url = "https://files.pythonhosted.org/packages/72/1f/8b214b69d08cc5e4bd8c3769ac55a43318f3529362ea55e5957774b69924/mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6", size = 9319112, upload-time = "2024-03-08T16:09:07.961Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/0ba2eaedca52bf5276275e8489951c26206030b3d31bf06f00875ae75d5d/mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e", size = 2555887, upload-time = "2024-03-08T16:09:48.584Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pdbr" +version = "0.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/47/1458f133732817d5e98e1323b454b032e70a07589c52562e8985e7add8e5/pdbr-0.9.7.tar.gz", hash = "sha256:8baba0b0dfe0ee2b758da4341507a71c20a6f6a3f92d9ee6bcd9650ae2e805bf", size = 15615, upload-time = "2026-01-05T08:36:11.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/9e/1ea57f0c7b13cc85f280eded3f24d0ee1f264709289ebc0804dc4608f34e/pdbr-0.9.7-py3-none-any.whl", hash = "sha256:b88c8df940897d01eb0944062b80cd4422e46903b2b910d281b215748029ba21", size = 16201, upload-time = "2026-01-05T08:36:09.942Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pprintpp" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/1a/7737e7a0774da3c3824d654993cf57adc915cb04660212f03406334d8c0b/pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403", size = 17995, upload-time = "2018-07-01T01:42:34.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/d1/e4ed95fdd3ef13b78630280d9e9e240aeb65cc7c544ec57106149c3942fb/pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d", size = 16952, upload-time = "2018-07-01T01:42:36.496Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/6b/1353beb3d1cd5cf61cdec5b6f87a9872399de3bc5cae0b7ce07ff4de2ab0/pydantic-2.13.1.tar.gz", hash = "sha256:a0f829b279ddd1e39291133fe2539d2aa46cc6b150c1706a270ff0879e3774d2", size = 843746, upload-time = "2026-04-15T14:57:19.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/5a/2225f4c176dbfed0d809e848b50ef08f70e61daa667b7fa14b0d311ae44d/pydantic-2.13.1-py3-none-any.whl", hash = "sha256:9557ecc2806faaf6037f85b1fbd963d01e30511c48085f0d573650fdeaad378a", size = 471917, upload-time = "2026-04-15T14:57:17.277Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/93/f97a86a7eb28faa1d038af2fd5d6166418b4433659108a4c311b57128b2d/pydantic_core-2.46.1.tar.gz", hash = "sha256:d408153772d9f298098fb5d620f045bdf0f017af0d5cb6e309ef8c205540caa4", size = 471230, upload-time = "2026-04-15T14:49:34.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/fb/caaa8ee23861c170f07dbd58fc2be3a2c02a32637693cbb23eef02e84808/pydantic_core-2.46.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae8c8c5eb4c796944f3166f2f0dab6c761c2c2cc5bd20e5f692128be8600b9a4", size = 2119472, upload-time = "2026-04-15T14:49:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/fa/61/bcffaa52894489ff89e5e1cdde67429914bf083c0db7296bef153020f786/pydantic_core-2.46.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:daba6f5f5b986aa0682623a1a4f8d1ecb0ec00ce09cfa9ca71a3b742bc383e3a", size = 1951230, upload-time = "2026-04-15T14:52:27.646Z" }, + { url = "https://files.pythonhosted.org/packages/f8/95/80d2f43a2a1a1e3220fd329d614aa5a39e0a75d24353a3aaf226e605f1c2/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0265f3a2460539ecc97817a80c7a23c458dd84191229b655522a2674f701f14e", size = 1976394, upload-time = "2026-04-15T14:50:32.742Z" }, + { url = "https://files.pythonhosted.org/packages/8d/31/2c5b1a207926b5fc1961a2d11da940129bc3841c36cc4df03014195b2966/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb16c0156c4b4e94aa3719138cc43c53d30ff21126b6a3af63786dcc0757b56e", size = 2068455, upload-time = "2026-04-15T14:50:01.286Z" }, + { url = "https://files.pythonhosted.org/packages/7d/36/c6aa07274359a51ac62895895325ce90107e811c6cea39d2617a99ef10d7/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b42d80fad8e4b283e1e4138f1142f0d038c46d137aad2f9824ad9086080dd41", size = 2239049, upload-time = "2026-04-15T14:53:02.216Z" }, + { url = "https://files.pythonhosted.org/packages/0a/3f/77cdd0db8bddc714842dfd93f737c863751cf02001c993341504f6b0cd53/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cced85896d5b795293bc36b7e2fb0347a36c828551b50cbba510510d928548c", size = 2318681, upload-time = "2026-04-15T14:50:04.539Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a3/09d929a40e6727274b0b500ad06e1b3f35d4f4665ae1c8ba65acbb17e9b5/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a641cb1e74b44c418adaf9f5f450670dbec53511f030d8cde8d8accb66edc363", size = 2096527, upload-time = "2026-04-15T14:53:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/89/ae/544c3a82456ebc254a9fcbe2715bab76c70acf9d291aaea24391147943e4/pydantic_core-2.46.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:191e7a122ab14eb12415fe3f92610fc06c7f1d2b4b9101d24d490d447ac92506", size = 2170407, upload-time = "2026-04-15T14:51:27.138Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ce/0dfd881c7af4c522f47b325707bd9a2cdcf4f40e4f2fd30df0e9a3e8d393/pydantic_core-2.46.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fe4ff660f7938b5d92f21529ce331b011aa35e481ab64b7cd03f52384e544bb", size = 2188578, upload-time = "2026-04-15T14:50:39.655Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e9/980ea2a6d5114dd1a62ecc5f56feb3d34555f33bd11043f042e5f7f0724a/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:18fcea085b3adc3868d8d19606da52d7a52d8bccd8e28652b0778dbe5e6a6660", size = 2188959, upload-time = "2026-04-15T14:52:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f1/595e0f50f4bfc56cde2fe558f2b0978f29f2865da894c6226231e17464a5/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e8e589e7c9466e022d79e13c5764c2239b2e5a7993ba727822b021234f89b56b", size = 2339973, upload-time = "2026-04-15T14:52:10.642Z" }, + { url = "https://files.pythonhosted.org/packages/49/44/be9f979a6ab6b8c36865ccd92c3a38a760c66055e1f384665f35525134c4/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f78eb3d4027963bdc9baccd177f02a98bf8714bc51fe17153d8b51218918b5bc", size = 2385228, upload-time = "2026-04-15T14:51:00.77Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d4/c826cd711787d240219f01d0d3ca116cb55516b8b95277820aa9c85e1882/pydantic_core-2.46.1-cp312-cp312-win32.whl", hash = "sha256:54fe30c20cab03844dc63bdc6ddca67f74a2eb8482df69c1e5f68396856241be", size = 1978828, upload-time = "2026-04-15T14:50:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/8a1fcf8181be4c7a9cfc34e5fbf2d9c3866edc9dfd3c48d5401806e0a523/pydantic_core-2.46.1-cp312-cp312-win_amd64.whl", hash = "sha256:aea4e22ed4c53f2774221435e39969a54d2e783f4aee902cdd6c8011415de893", size = 2070015, upload-time = "2026-04-15T14:49:47.301Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/fea36ad2882b99c174ef4ffbc7ea6523f6abe26060fbc1f77d6441670232/pydantic_core-2.46.1-cp312-cp312-win_arm64.whl", hash = "sha256:f76fb49c34b4d66aa6e552ce9e852ea97a3a06301a9f01ae82f23e449e3a55f8", size = 2030176, upload-time = "2026-04-15T14:50:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d2/bda39bad2f426cb5078e6ad28076614d3926704196efe0d7a2a19a99025d/pydantic_core-2.46.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:cdc8a5762a9c4b9d86e204d555444e3227507c92daba06259ee66595834de47a", size = 2119092, upload-time = "2026-04-15T14:49:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/69631e64d69cb3481494b2bddefe0ddd07771209f74e9106d066f9138c2a/pydantic_core-2.46.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba381dfe9c85692c566ecb60fa5a77a697a2a8eebe274ec5e4d6ec15fafad799", size = 1951400, upload-time = "2026-04-15T14:51:06.588Z" }, + { url = "https://files.pythonhosted.org/packages/53/1c/21cb3db6ae997df31be8e91f213081f72ffa641cb45c89b8a1986832b1f9/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1593d8de98207466dc070118322fef68307a0cc6a5625e7b386f6fdae57f9ab6", size = 1976864, upload-time = "2026-04-15T14:50:54.804Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/05c819f734318ce5a6ca24da300d93696c105af4adb90494ee571303afd8/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8262c74a1af5b0fdf795f5537f7145785a63f9fbf9e15405f547440c30017ed8", size = 2066669, upload-time = "2026-04-15T14:51:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/cb/23/fadddf1c7f2f517f58731aea9b35c914e6005250f08dac9b8e53904cdbaa/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b88949a24182e83fbbb3f7ca9b7858d0d37b735700ea91081434b7d37b3b444", size = 2238737, upload-time = "2026-04-15T14:50:45.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/0cd4f95cb0359c8b1ec71e89c3777e7932c8dfeb9cd54740289f310aaead/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8f3708cd55537aeaf3fd0ea55df0d68d0da51dcb07cbc8508745b34acc4c6e0", size = 2316258, upload-time = "2026-04-15T14:51:08.471Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/6fc24c3766a19c222a0d60d652b78f0283339d4cd4c173fab06b7ee76571/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f79292435fff1d4f0c18d9cfaf214025cc88e4f5104bfaed53f173621da1c743", size = 2097474, upload-time = "2026-04-15T14:49:56.543Z" }, + { url = "https://files.pythonhosted.org/packages/4b/af/f39795d1ce549e35d0841382b9c616ae211caffb88863147369a8d74fba9/pydantic_core-2.46.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a2e607aeb59cf4575bb364470288db3b9a1f0e7415d053a322e3e154c1a0802e", size = 2168383, upload-time = "2026-04-15T14:51:29.269Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/0d563f74582795779df6cc270c3fc220f49f4daf7860d74a5a6cda8491ff/pydantic_core-2.46.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec5ca190b75878a9f6ae1fc8f5eb678497934475aef3d93204c9fa01e97370b6", size = 2186182, upload-time = "2026-04-15T14:50:19.097Z" }, + { url = "https://files.pythonhosted.org/packages/5c/07/1c10d5ce312fc4cf86d1e50bdcdbb8ef248409597b099cab1b4bb3a093f7/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:1f80535259dcdd517d7b8ca588d5ca24b4f337228e583bebedf7a3adcdf5f721", size = 2187859, upload-time = "2026-04-15T14:49:22.974Z" }, + { url = "https://files.pythonhosted.org/packages/92/01/e1f62d4cb39f0913dbf5c95b9b119ef30ddba9493dff8c2b012f0cdd67dc/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:24820b3c82c43df61eca30147e42853e6c127d8b868afdc0c162df829e011eb4", size = 2338372, upload-time = "2026-04-15T14:49:53.316Z" }, + { url = "https://files.pythonhosted.org/packages/44/ed/218dfeea6127fb1781a6ceca241ec6edf00e8a8933ff331af2215975a534/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f12794b1dd8ac9fb66619e0b3a0427189f5d5638e55a3de1385121a9b7bf9b39", size = 2384039, upload-time = "2026-04-15T14:53:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1e/011e763cd059238249fbd5780e0f8d0b04b47f86c8925e22784f3e5fc977/pydantic_core-2.46.1-cp313-cp313-win32.whl", hash = "sha256:9bc09aed935cdf50f09e908923f9efbcca54e9244bd14a5a0e2a6c8d2c21b4e9", size = 1977943, upload-time = "2026-04-15T14:52:17.969Z" }, + { url = "https://files.pythonhosted.org/packages/8c/06/b559a490d3ed106e9b1777b8d5c8112dd8d31716243cd662616f66c1f8ea/pydantic_core-2.46.1-cp313-cp313-win_amd64.whl", hash = "sha256:fac2d6c8615b8b42bee14677861ba09d56ee076ba4a65cfb9c3c3d0cc89042f2", size = 2068729, upload-time = "2026-04-15T14:53:07.288Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/32a198946e2e19508532aa9da02a61419eb15bd2d96bab57f810f2713e31/pydantic_core-2.46.1-cp313-cp313-win_arm64.whl", hash = "sha256:f978329f12ace9f3cb814a5e44d98bbeced2e36f633132bafa06d2d71332e33e", size = 2029550, upload-time = "2026-04-15T14:52:22.707Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2b/6793fe89ab66cb2d3d6e5768044eab80bba1d0fae8fd904d0a1574712e17/pydantic_core-2.46.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9917cb61effac7ec0f448ef491ec7584526d2193be84ff981e85cbf18b68c42a", size = 2118110, upload-time = "2026-04-15T14:50:52.947Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/e9a905ddfcc2fd7bd862b340c02be6ab1f827922822d425513635d0ac774/pydantic_core-2.46.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e749679ca9f8a9d0bff95fb7f6b57bb53f2207fa42ffcc1ec86de7e0029ab89", size = 1948645, upload-time = "2026-04-15T14:51:55.577Z" }, + { url = "https://files.pythonhosted.org/packages/15/23/26e67f86ed62ac9d6f7f3091ee5220bf14b5ac36fb811851d601365ef896/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2ecacee70941e233a2dad23f7796a06f86cc10cc2fbd1c97c7dd5b5a79ffa4f", size = 1977576, upload-time = "2026-04-15T14:49:37.58Z" }, + { url = "https://files.pythonhosted.org/packages/b8/78/813c13c0de323d4de54ee2e6fdd69a0271c09ac8dd65a8a000931aa487a5/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:647d0a2475b8ed471962eed92fa69145b864942f9c6daa10f95ac70676637ae7", size = 2060358, upload-time = "2026-04-15T14:51:40.087Z" }, + { url = "https://files.pythonhosted.org/packages/09/5e/4caf2a15149271fbd2b4d968899a450853c800b85152abcf54b11531417f/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac9cde61965b0697fce6e6cc372df9e1ad93734828aac36e9c1c42a22ad02897", size = 2235980, upload-time = "2026-04-15T14:50:34.535Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c1/a2cdabb5da6f5cb63a3558bcafffc20f790fa14ccffbefbfb1370fadc93f/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a2eb0864085f8b641fb3f54a2fb35c58aff24b175b80bc8a945050fcde03204", size = 2316800, upload-time = "2026-04-15T14:52:46.999Z" }, + { url = "https://files.pythonhosted.org/packages/76/fd/19d711e4e9331f9d77f222bffc202bf30ea0d74f6419046376bb82f244c8/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b83ce9fede4bc4fb649281d9857f06d30198b8f70168f18b987518d713111572", size = 2101762, upload-time = "2026-04-15T14:49:24.278Z" }, + { url = "https://files.pythonhosted.org/packages/dc/64/ce95625448e1a4e219390a2923fd594f3fa368599c6b42ac71a5df7238c9/pydantic_core-2.46.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:cb33192753c60f269d2f4a1db8253c95b0df6e04f2989631a8cc1b0f4f6e2e92", size = 2167737, upload-time = "2026-04-15T14:50:41.637Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/413572d03ca3e73b408f00f54418b91a8be6401451bc791eaeff210328e5/pydantic_core-2.46.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:96611d51f953f87e1ae97637c01ee596a08b7f494ea00a5afb67ea6547b9f53b", size = 2185658, upload-time = "2026-04-15T14:51:46.799Z" }, + { url = "https://files.pythonhosted.org/packages/36/09/e4f581353bdf3f0c7de8a8b27afd14fc761da29d78146376315a6fedc487/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9b176fa55f9107db5e6c86099aa5bfd934f1d3ba6a8b43f714ddeebaed3f42b7", size = 2184154, upload-time = "2026-04-15T14:52:49.629Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/d0d52849933f5a4bf1ad9d8da612792f96469b37e286a269e3ee9c60bbb1/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:79a59f63a4ce4f3330e27e6f3ce281dd1099453b637350e97d7cf24c207cd120", size = 2332379, upload-time = "2026-04-15T14:49:55.009Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/25bfb08fdbef419f73290e573899ce938a327628c34e8f3a4bafeea30126/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:f200fce071808a385a314b7343f5e3688d7c45746be3d64dc71ee2d3e2a13268", size = 2377964, upload-time = "2026-04-15T14:51:59.649Z" }, + { url = "https://files.pythonhosted.org/packages/15/36/b777766ff83fef1cf97473d64764cd44f38e0d8c269ed06faace9ae17666/pydantic_core-2.46.1-cp314-cp314-win32.whl", hash = "sha256:3a07eccc0559fb9acc26d55b16bf8ebecd7f237c74a9e2c5741367db4e6d8aff", size = 1976450, upload-time = "2026-04-15T14:51:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/7b/4b/4cd19d2437acfc18ca166db5a2067040334991eb862c4ecf2db098c91fbf/pydantic_core-2.46.1-cp314-cp314-win_amd64.whl", hash = "sha256:1706d270309ac7d071ffe393988c471363705feb3d009186e55d17786ada9622", size = 2067750, upload-time = "2026-04-15T14:49:38.941Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a0/490751c0ef8f5b27aae81731859aed1508e72c1a9b5774c6034269db773b/pydantic_core-2.46.1-cp314-cp314-win_arm64.whl", hash = "sha256:22d4e7457ade8af06528012f382bc994a97cc2ce6e119305a70b3deff1e409d6", size = 2021109, upload-time = "2026-04-15T14:50:27.728Z" }, + { url = "https://files.pythonhosted.org/packages/36/3a/2a018968245fffd25d5f1972714121ad309ff2de19d80019ad93494844f9/pydantic_core-2.46.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:607ff9db0b7e2012e7eef78465e69f9a0d7d1c3e7c6a84cf0c4011db0fcc3feb", size = 2111548, upload-time = "2026-04-15T14:52:08.273Z" }, + { url = "https://files.pythonhosted.org/packages/77/5b/4103b6192213217e874e764e5467d2ff10d8873c1147d01fa432ac281880/pydantic_core-2.46.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cda3eacaea13bd02a1bea7e457cc9fc30b91c5a91245cef9b215140f80dd78c", size = 1926745, upload-time = "2026-04-15T14:50:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/c3/70/602a667cf4be4bec6c3334512b12ae4ea79ce9bfe41dc51be1fd34434453/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9493279cdc7997fe19e5ed9b41f30cbc3806bd4722adb402fedb6f6d41bd72a", size = 1965922, upload-time = "2026-04-15T14:51:12.555Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/06a89ce5323e755b7d2812189f9706b87aaebe49b34d247b380502f7992c/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3644e5e10059999202355b6c6616e624909e23773717d8f76deb8a6e2a72328c", size = 2043221, upload-time = "2026-04-15T14:51:18.995Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6e/b1d9ad907d9d76964903903349fd2e33c87db4b993cc44713edcad0fc488/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ad6c9de57683e26c92730991960c0c3571b8053263b042de2d3e105930b2767", size = 2243655, upload-time = "2026-04-15T14:50:10.718Z" }, + { url = "https://files.pythonhosted.org/packages/ef/73/787abfaad51174641abb04c8aa125322279b40ad7ce23c495f5a69f76554/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:557ebaa27c7617e7088002318c679a8ce685fa048523417cd1ca52b7f516d955", size = 2295976, upload-time = "2026-04-15T14:53:09.694Z" }, + { url = "https://files.pythonhosted.org/packages/56/0b/b7c5a631b6d5153d4a1ea4923b139aea256dc3bd99c8e6c7b312c7733146/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cd37e39b22b796ba0298fe81e9421dd7b65f97acfbb0fb19b33ffdda7b9a7b4", size = 2103439, upload-time = "2026-04-15T14:50:08.32Z" }, + { url = "https://files.pythonhosted.org/packages/2a/3f/952ee470df69e5674cdec1cbde22331adf643b5cc2ff79f4292d80146ee4/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:6689443b59714992e67d62505cdd2f952d6cf1c14cc9fd9aeec6719befc6f23b", size = 2132871, upload-time = "2026-04-15T14:50:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8b/1dea3b1e683c60c77a60f710215f90f486755962aa8939dbcb7c0f975ac3/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f32c41ca1e3456b5dd691827b7c1433c12d5f0058cc186afbb3615bc07d97b8", size = 2168658, upload-time = "2026-04-15T14:52:24.897Z" }, + { url = "https://files.pythonhosted.org/packages/67/97/32ae283810910d274d5ba9f48f856f5f2f612410b78b249f302d297816f5/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:88cd1355578852db83954dc36e4f58f299646916da976147c20cf6892ba5dc43", size = 2171184, upload-time = "2026-04-15T14:52:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/a2/57/c9a855527fe56c2072070640221f53095b0b19eaf651f3c77643c9cabbe3/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:a170fefdb068279a473cc9d34848b85e61d68bfcc2668415b172c5dfc6f213bf", size = 2316573, upload-time = "2026-04-15T14:52:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/14c39ffc7399819c5448007c7bcb4e6da5669850cfb7dcbb727594290b48/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:556a63ff1006934dba4eed7ea31b58274c227e29298ec398e4275eda4b905e95", size = 2378340, upload-time = "2026-04-15T14:51:02.619Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/a37461fbb29c053ea4e62cfc5c2d56425cb5efbef8316e63f6d84ae45718/pydantic_core-2.46.1-cp314-cp314t-win32.whl", hash = "sha256:3b146d8336a995f7d7da6d36e4a779b7e7dff2719ac00a1eb8bd3ded00bec87b", size = 1960843, upload-time = "2026-04-15T14:52:06.103Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/97e1221197d17a27f768363f87ec061519eeeed15bbd315d2e9d1429ff03/pydantic_core-2.46.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f1bc856c958e6fe9ec071e210afe6feb695f2e2e81fd8d2b102f558d364c4c17", size = 2048696, upload-time = "2026-04-15T14:52:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/19/d5/4eac95255c7d35094b46a32ec1e4d80eac94729c694726ee1d69948bd5f0/pydantic_core-2.46.1-cp314-cp314t-win_arm64.whl", hash = "sha256:21a5bfd8a1aa4de60494cdf66b0c912b1495f26a8899896040021fbd6038d989", size = 2022343, upload-time = "2026-04-15T14:49:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/f4/97/95de673a1356a88b2efdaa120eb6af357a81555c35f6809a7a1423ff7aef/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:5f9107a24a4bc00293434dfa95cf8968751ad0dd703b26ea83a75a56f7326041", size = 2107564, upload-time = "2026-04-15T14:50:49.14Z" }, + { url = "https://files.pythonhosted.org/packages/00/fc/a7c16d85211ea9accddc693b7d049f20b0c06440d9264d1e1c074394ee6c/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:2b1801ba99876984d0a03362782819238141c4d0f3f67f69093663691332fc35", size = 1939925, upload-time = "2026-04-15T14:50:36.188Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/87841169d77820ddabeb81d82002c95dcb82163846666d74f5bdeeaec750/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7fd82a91a20ed6d54fa8c91e7a98255b1ff45bf09b051bfe7fe04eb411e232e", size = 1995313, upload-time = "2026-04-15T14:50:22.538Z" }, + { url = "https://files.pythonhosted.org/packages/ea/96/b46609359a354fa9cd336fc5d93334f1c358b756cc81e4b397347a88fa6f/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f135bf07c92c93def97008bc4496d16934da9efefd7204e5f22a2c92523cb1f", size = 2151197, upload-time = "2026-04-15T14:51:22.925Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/18/82fcb4ee47d66d99f6cd1efc0b11b2a25029f303c599a5afda7c1bca4254/pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609", size = 53298, upload-time = "2024-12-13T06:12:44.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/56/2ee0cab25c11d4e38738a2a98c645a8f002e2ecf7b5ed774c70d53b92bb1/pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3", size = 19245, upload-time = "2024-12-13T06:12:41.805Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-custom-exit-code" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/9d/e1eb0af5e96a5c34f59b9aa69dfb680764420fe60f2ec28cfbc5339f99f8/pytest-custom_exit_code-0.3.0.tar.gz", hash = "sha256:51ffff0ee2c1ddcc1242e2ddb2a5fd02482717e33a2326ef330e3aa430244635", size = 3633, upload-time = "2019-08-07T09:45:15.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/a0/effb6cbbccfd1c106c572d3d619b3418d71093afb4cd4f91f51e6a1799d2/pytest_custom_exit_code-0.3.0-py3-none-any.whl", hash = "sha256:6e0ce6e57ce3a583cb7e5023f7d1021e19dfec22be41d9ad345bae2fc61caf3b", size = 4055, upload-time = "2019-08-07T09:45:13.767Z" }, +] + +[[package]] +name = "pytest-icdiff" +version = "0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "icdiff" }, + { name = "pprintpp" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/0c/66e1e2590e98f4428e374a3b6448dc086a908d15b1e24b914539d13b7ac4/pytest-icdiff-0.9.tar.gz", hash = "sha256:13aede616202e57fcc882568b64589002ef85438046f012ac30a8d959dac8b75", size = 7110, upload-time = "2023-12-05T11:18:30.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/e1/cafe1edf7a30be6fa1bbbf43f7af12b34682eadcf19eb6e9f7352062c422/pytest_icdiff-0.9-py3-none-any.whl", hash = "sha256:efee0da3bd1b24ef2d923751c5c547fbb8df0a46795553fba08ef57c3ca03d82", size = 4994, upload-time = "2023-12-05T11:18:28.572Z" }, +] + +[[package]] +name = "pytest-instafail" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/bd/e0ba6c3cd20b9aa445f0af229f3a9582cce589f083537978a23e6f14e310/pytest-instafail-0.5.0.tar.gz", hash = "sha256:33a606f7e0c8e646dc3bfee0d5e3a4b7b78ef7c36168cfa1f3d93af7ca706c9e", size = 5849, upload-time = "2023-03-31T17:17:32.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/c0/c32dc39fc172e684fdb3d30169843efb65c067be1e12689af4345731126e/pytest_instafail-0.5.0-py3-none-any.whl", hash = "sha256:6855414487e9e4bb76a118ce952c3c27d3866af15487506c4ded92eb72387819", size = 4176, upload-time = "2023-03-31T17:17:30.065Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "pytest-rerunfailures" +version = "16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/04/71e9520551fc8fe2cf5c1a1842e4e600265b0815f2016b7c27ec85688682/pytest_rerunfailures-16.1.tar.gz", hash = "sha256:c38b266db8a808953ebd71ac25c381cb1981a78ff9340a14bcb9f1b9bff1899e", size = 30889, upload-time = "2025-10-10T07:06:01.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/54/60eabb34445e3db3d3d874dc1dfa72751bfec3265bd611cb13c8b290adea/pytest_rerunfailures-16.1-py3-none-any.whl", hash = "sha256:5d11b12c0ca9a1665b5054052fcc1084f8deadd9328962745ef6b04e26382e86", size = 14093, upload-time = "2025-10-10T07:06:00.019Z" }, +] + +[[package]] +name = "pytest-timeouts" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/47/9a8d7fa18736d69d218e303345dfe21755e956fd863b0f043ef854ff84a6/pytest-timeouts-1.2.1.tar.gz", hash = "sha256:390351afc7ecb422ea0ec38081e0acd91cad416b383944a9a3358087de50c2fb", size = 5002, upload-time = "2019-09-21T06:03:25.167Z" } + +[[package]] +name = "pytest-unordered" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/3e/6ec9ec74551804c9e005d5b3cbe1fd663f03ed3bd4bdb1ce764c3d334d8e/pytest_unordered-0.7.0.tar.gz", hash = "sha256:0f953a438db00a9f6f99a0f4727f2d75e72dd93319b3d548a97ec9db4903a44f", size = 7930, upload-time = "2025-06-03T12:56:04.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/95/ae2875e19472797e9672b65412858ab6639d8e55defd9859241e5ff80d02/pytest_unordered-0.7.0-py3-none-any.whl", hash = "sha256:486b26d24a2d3b879a275c3d16d14eda1bd9c32aafddbb17b98ac755daba7584", size = 6210, upload-time = "2025-06-03T12:36:06.66Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "requests-mock" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901, upload-time = "2024-03-29T03:54:29.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511, upload-time = "2025-03-21T13:31:17.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146, upload-time = "2025-03-21T13:30:26.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092, upload-time = "2025-03-21T13:30:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082, upload-time = "2025-03-21T13:30:39.962Z" }, + { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818, upload-time = "2025-03-21T13:30:42.551Z" }, + { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251, upload-time = "2025-03-21T13:30:45.196Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566, upload-time = "2025-03-21T13:30:47.516Z" }, + { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721, upload-time = "2025-03-21T13:30:49.56Z" }, + { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274, upload-time = "2025-03-21T13:30:52.055Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284, upload-time = "2025-03-21T13:30:54.24Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861, upload-time = "2025-03-21T13:30:56.757Z" }, + { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560, upload-time = "2025-03-21T13:30:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091, upload-time = "2025-03-21T13:31:01.45Z" }, + { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133, upload-time = "2025-03-21T13:31:04.013Z" }, + { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514, upload-time = "2025-03-21T13:31:06.166Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835, upload-time = "2025-03-21T13:31:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713, upload-time = "2025-03-21T13:31:13.148Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990, upload-time = "2025-03-21T13:31:15.206Z" }, +] + +[[package]] +name = "semver" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "time-machine" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/fc/37b02f6094dbb1f851145330460532176ed2f1dc70511a35828166c41e52/time_machine-3.2.0.tar.gz", hash = "sha256:a4ddd1cea17b8950e462d1805a42b20c81eb9aafc8f66b392dd5ce997e037d79", size = 14804, upload-time = "2025-12-17T23:33:02.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/8b/080c8eedcd67921a52ba5bd0e075362062509ab63c86fc1a0442fad241a6/time_machine-3.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc4bee5b0214d7dc4ebc91f4a4c600f1a598e9b5606ac751f42cb6f6740b1dbb", size = 19255, upload-time = "2025-12-17T23:31:58.057Z" }, + { url = "https://files.pythonhosted.org/packages/66/17/0e5291e9eb705bf8a5a1305f826e979af307bbeb79def4ddbf4b3f9a81e0/time_machine-3.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ca036304b4460ae2fdc1b52dd8b1fa7cf1464daa427fc49567413c09aa839c1", size = 15360, upload-time = "2025-12-17T23:31:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/9ab87b71d2e2b62463b9b058b7ae7ac09fb57f8fcd88729dec169d304340/time_machine-3.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5442735b41d7a2abc2f04579b4ca6047ed4698a8338a4fec92c7c9423e7938cb", size = 33029, upload-time = "2025-12-17T23:32:00.413Z" }, + { url = "https://files.pythonhosted.org/packages/4b/26/b5ca19da6f25ea905b3e10a0ea95d697c1aeba0404803a43c68f1af253e6/time_machine-3.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:97da3e971e505cb637079fb07ab0bcd36e33279f8ecac888ff131f45ef1e4d8d", size = 34579, upload-time = "2025-12-17T23:32:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/79/ca/6ac7ad5f10ea18cc1d9de49716ba38c32132c7b64532430d92ef240c116b/time_machine-3.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3cdda6dee4966e38aeb487309bb414c6cb23a81fc500291c77a8fcd3098832e7", size = 35961, upload-time = "2025-12-17T23:32:02.521Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/390dd958bed395ab32d79a9fe61fe111825c0dd4ded54dbba7e867f171e6/time_machine-3.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:33d9efd302a6998bcc8baa4d84f259f8a4081105bd3d7f7af7f1d0abd3b1c8aa", size = 34668, upload-time = "2025-12-17T23:32:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/da/57/c88fff034a4e9538b3ae7c68c9cfb283670b14d17522c5a8bc17d29f9a4b/time_machine-3.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3a0b0a33971f14145853c9bd95a6ab0353cf7e0019fa2a7aa1ae9fddfe8eab50", size = 32891, upload-time = "2025-12-17T23:32:04.656Z" }, + { url = "https://files.pythonhosted.org/packages/2d/70/ebbb76022dba0fec8f9156540fc647e4beae1680c787c01b1b6200e56d70/time_machine-3.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2d0be9e5f22c38082d247a2cdcd8a936504e9db60b7b3606855fb39f299e9548", size = 34080, upload-time = "2025-12-17T23:32:06.146Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/2ca9e7af3df540dc1c79e3de588adeddb7dcc2107829248e6969c4f14167/time_machine-3.2.0-cp312-cp312-win32.whl", hash = "sha256:3f74623648b936fdce5f911caf386c0a0b579456410975de8c0dfeaaffece1d8", size = 17371, upload-time = "2025-12-17T23:32:07.164Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ce/21d23efc9c2151939af1b7ee4e60d86d661b74ef32b8eaa148f6fe8c899c/time_machine-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:34e26a41d994b5e4b205136a90e9578470386749cc9a2ecf51ca18f83ce25e23", size = 18132, upload-time = "2025-12-17T23:32:08.447Z" }, + { url = "https://files.pythonhosted.org/packages/2f/34/c2b70be483accf6db9e5d6c3139bce3c38fe51f898ccf64e8d3fe14fbf4d/time_machine-3.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:0615d3d82c418d6293f271c348945c5091a71f37e37173653d5c26d0e74b13a8", size = 16930, upload-time = "2025-12-17T23:32:09.477Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cd/43ad5efc88298af3c59b66769cea7f055567a85071579ed40536188530c1/time_machine-3.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c421a8eb85a4418a7675a41bf8660224318c46cc62e4751c8f1ceca752059090", size = 19318, upload-time = "2025-12-17T23:32:10.518Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f6/084010ef7f4a3f38b5a4900923d7c85b29e797655c4f6ee4ce54d903cca8/time_machine-3.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4e758f7727d0058c4950c66b58200c187072122d6f7a98b610530a4233ea7b", size = 15390, upload-time = "2025-12-17T23:32:11.625Z" }, + { url = "https://files.pythonhosted.org/packages/25/aa/1cabb74134f492270dc6860cb7865859bf40ecf828be65972827646e91ad/time_machine-3.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:154bd3f75c81f70218b2585cc12b60762fb2665c507eec5ec5037d8756d9b4e0", size = 33115, upload-time = "2025-12-17T23:32:13.219Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/78c5d7dfa366924eb4dbfcc3fc917c39a4280ca234b12819cc1f16c03d88/time_machine-3.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50cfe5ebea422c896ad8d278af9648412b7533b8ea6adeeee698a3fd9b1d3b7", size = 34705, upload-time = "2025-12-17T23:32:14.29Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/d5e877c24541f674c6869ff6e9c56833369796010190252e92c9d7ae5f0f/time_machine-3.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636576501724bd6a9124e69d86e5aef263479e89ef739c5db361469f0463a0a1", size = 36104, upload-time = "2025-12-17T23:32:15.354Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/d4bae72f388f67efc9609f89b012e434bb19d9549c7a7b47d6c7d9e5c55d/time_machine-3.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40e6f40c57197fcf7ec32d2c563f4df0a82c42cdcc3cab27f688e98f6060df10", size = 34765, upload-time = "2025-12-17T23:32:16.434Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c3/ac378cf301d527d8dfad2f0db6bad0dfb1ab73212eaa56d6b96ee5d9d20b/time_machine-3.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a1bcf0b846bbfc19a79bc19e3fa04d8c7b1e8101c1b70340ffdb689cd801ea53", size = 33010, upload-time = "2025-12-17T23:32:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/06/35/7ce897319accda7a6970b288a9a8c52d25227342a7508505a2b3d235b649/time_machine-3.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae55a56c179f4fe7a62575ad5148b6ed82f6c7e5cf2f9a9ec65f2f5b067db5f5", size = 34185, upload-time = "2025-12-17T23:32:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/bf/28/f922022269749cb02eee2b62919671153c4088994fa955a6b0e50327ff81/time_machine-3.2.0-cp313-cp313-win32.whl", hash = "sha256:a66fe55a107e46916007a391d4030479df8864ec6ad6f6a6528221befc5c886e", size = 17397, upload-time = "2025-12-17T23:32:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/fd87cde397f4a7bea493152f0aca8fd569ec709cad9e0f2ca7011eb8c7f7/time_machine-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:30c9ce57165df913e4f74e285a8ab829ff9b7aa3e5ec0973f88f642b9a7b3d15", size = 18139, upload-time = "2025-12-17T23:32:20.991Z" }, + { url = "https://files.pythonhosted.org/packages/75/81/b8ce58233addc5d7d54d2fabc49dcbc02d79e3f079d150aa1bec3d5275ef/time_machine-3.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:89cad7e179e9bdcc84dcf09efe52af232c4cc7a01b3de868356bbd59d95bd9b8", size = 16964, upload-time = "2025-12-17T23:32:22.075Z" }, + { url = "https://files.pythonhosted.org/packages/67/e7/487f0ba5fe6c58186a5e1af2a118dfa2c160fedb37ef53a7e972d410408e/time_machine-3.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:59d71545e62525a4b85b6de9ab5c02ee3c61110fd7f636139914a2335dcbfc9c", size = 20000, upload-time = "2025-12-17T23:32:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/e1/17/eb2c0054c8d44dd42df84ccd434539249a9c7d0b8eb53f799be2102500ab/time_machine-3.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:999672c621c35362bc28e03ca0c7df21500195540773c25993421fd8d6cc5003", size = 15657, upload-time = "2025-12-17T23:32:24.125Z" }, + { url = "https://files.pythonhosted.org/packages/43/21/93443b5d1dd850f8bb9442e90d817a9033dcce6bfbdd3aabbb9786251c80/time_machine-3.2.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5faf7397f0580c7b9d67288522c8d7863e85f0cffadc0f1fccdb2c3dfce5783e", size = 39216, upload-time = "2025-12-17T23:32:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9e/18544cf8acc72bb1dc03762231c82ecc259733f4bb6770a7bbe5cd138603/time_machine-3.2.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3dd886ec49f1fa5a00e844f5947e5c0f98ce574750c24b7424c6f77fc1c3e87", size = 40764, upload-time = "2025-12-17T23:32:26.643Z" }, + { url = "https://files.pythonhosted.org/packages/27/f7/9fe9ce2795636a3a7467307af6bdf38bb613ddb701a8a5cd50ec713beb5e/time_machine-3.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0ecd96bc7bbe450acaaabe569d84e81688f1be8ad58d1470e42371d145fb53", size = 43526, upload-time = "2025-12-17T23:32:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/a93e975ba9dec22e87ec92d18c28e67d36bd536f9119ffa439b2892b0c9c/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:158220e946c1c4fb8265773a0282c88c35a7e3bb5d78e3561214e3b3231166f3", size = 41727, upload-time = "2025-12-17T23:32:28.985Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fb/e3633e5a6bbed1c76bb2e9810dabc2f8467532ffcd29b9aed404b473061a/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c1aee29bc54356f248d5d7dfdd131e12ca825e850a08c0ebdb022266d073013", size = 38952, upload-time = "2025-12-17T23:32:30.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/3d/02e9fb2526b3d6b1b45bc8e4d912d95d1cd699d1a3f6df985817d37a0600/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8ed2224f09d25b1c2fc98683613aca12f90f682a427eabb68fc824d27014e4a", size = 39829, upload-time = "2025-12-17T23:32:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/85/c8/c14265212436da8e0814c45463987b3f57de3eca4de023cc2eabb0c62ef3/time_machine-3.2.0-cp313-cp313t-win32.whl", hash = "sha256:3498719f8dab51da76d29a20c1b5e52ee7db083dddf3056af7fa69c1b94e1fe6", size = 17852, upload-time = "2025-12-17T23:32:32.079Z" }, + { url = "https://files.pythonhosted.org/packages/1d/bc/8acb13cf6149f47508097b158a9a8bec9ec4530a70cb406124e8023581f5/time_machine-3.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e0d90bee170b219e1d15e6a58164aa808f5170090e4f090bd0670303e34181b1", size = 18918, upload-time = "2025-12-17T23:32:33.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/c443ee508c2708fd2514ccce9052f5e48888783ce690506919629ebc8eb0/time_machine-3.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:051de220fdb6e20d648111bbad423d9506fdbb2e44d4429cef3dc0382abf1fc2", size = 17261, upload-time = "2025-12-17T23:32:34.446Z" }, + { url = "https://files.pythonhosted.org/packages/61/70/b4b980d126ed155c78d1879c50d60c8dcbd47bd11cb14ee7be50e0dfc07f/time_machine-3.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1398980c017fe5744d66f419e0115ee48a53b00b146d738e1416c225eb610b82", size = 19303, upload-time = "2025-12-17T23:32:35.796Z" }, + { url = "https://files.pythonhosted.org/packages/73/73/eaa33603c69a68fe2b6f54f9dd75481693d62f1d29676531002be06e2d1c/time_machine-3.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f8f4e35f4191ef70c2ab8ff490761ee9051b891afce2bf86dde3918eb7b537b", size = 15431, upload-time = "2025-12-17T23:32:37.244Z" }, + { url = "https://files.pythonhosted.org/packages/76/10/b81e138e86cc7bab40cdb59d294b341e172201f4a6c84bb0ec080407977a/time_machine-3.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6db498686ecf6163c5aa8cf0bcd57bbe0f4081184f247edf3ee49a2612b584f9", size = 33206, upload-time = "2025-12-17T23:32:38.713Z" }, + { url = "https://files.pythonhosted.org/packages/d3/72/4deab446b579e8bd5dca91de98595c5d6bd6a17ce162abf5c5f2ce40d3d8/time_machine-3.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:027c1807efb74d0cd58ad16524dec94212fbe900115d70b0123399883657ac0f", size = 34792, upload-time = "2025-12-17T23:32:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/2c/39/439c6b587ddee76d533fe972289d0646e0a5520e14dc83d0a30aeb5565f7/time_machine-3.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92432610c05676edd5e6946a073c6f0c926923123ce7caee1018dc10782c713d", size = 36187, upload-time = "2025-12-17T23:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/4b/db/2da4368db15180989bab83746a857bde05ad16e78f326801c142bb747a06/time_machine-3.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c25586b62480eb77ef3d953fba273209478e1ef49654592cd6a52a68dfe56a67", size = 34855, upload-time = "2025-12-17T23:32:42.817Z" }, + { url = "https://files.pythonhosted.org/packages/88/84/120a431fee50bc4c241425bee4d3a4910df4923b7ab5f7dff1bf0c772f08/time_machine-3.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6bf3a2fa738d15e0b95d14469a0b8ea42635467408d8b490e263d5d45c9a177f", size = 33222, upload-time = "2025-12-17T23:32:43.94Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ea/89cfda82bb8c57ff91bb9a26751aa234d6d90e9b4d5ab0ad9dce0f9f0329/time_machine-3.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ce76b82276d7ad2a66cdc85dad4df19d1422b69183170a34e8fbc4c3f35502f7", size = 34270, upload-time = "2025-12-17T23:32:45.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/aa/235357da4f69a51a8d35fcbfcfa77cdc7dc24f62ae54025006570bda7e2d/time_machine-3.2.0-cp314-cp314-win32.whl", hash = "sha256:14d6778273c543441863dff712cd1d7803dee946b18de35921eb8df10714539d", size = 17544, upload-time = "2025-12-17T23:32:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/6c8405a7276be79693b792cff22ce41067ec05db26a7d02f2d5b06324434/time_machine-3.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbf821da96dbc80d349fa9e7c36e670b41d68a878d28c8850057992fed430eef", size = 18423, upload-time = "2025-12-17T23:32:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/d9/03/a3cf419e20c35fc203c6e4fed48b5b667c1a2b4da456d9971e605f73ecef/time_machine-3.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:71c75d71f8e68abc8b669bca26ed2ddd558430a6c171e32b8620288565f18c0e", size = 17050, upload-time = "2025-12-17T23:32:48.91Z" }, + { url = "https://files.pythonhosted.org/packages/86/a1/142de946dc4393f910bf4564b5c3ba819906e1f49b06c9cb557519c849e4/time_machine-3.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4e374779021446fc2b5c29d80457ec9a3b1a5df043dc2aae07d7c1415d52323c", size = 19991, upload-time = "2025-12-17T23:32:49.933Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/7f17def6289901f94726921811a16b9adce46e666362c75d45730c60274f/time_machine-3.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:122310a6af9c36e9a636da32830e591e7923e8a07bdd0a43276c3a36c6821c90", size = 15707, upload-time = "2025-12-17T23:32:50.969Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d3/3502fb9bd3acb159c18844b26c43220201a0d4a622c0c853785d07699a92/time_machine-3.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba3eeb0f018cc362dd8128befa3426696a2e16dd223c3fb695fde184892d4d8c", size = 39207, upload-time = "2025-12-17T23:32:52.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/be/8b27f4aa296fda14a5a2ad7f588ddd450603c33415ab3f8e85b2f1a44678/time_machine-3.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:77d38ba664b381a7793f8786efc13b5004f0d5f672dae814430445b8202a67a6", size = 40764, upload-time = "2025-12-17T23:32:53.167Z" }, + { url = "https://files.pythonhosted.org/packages/42/cd/fe4c4e5c8ab6d48fab3624c32be9116fb120173a35fe67e482e5cf68b3d2/time_machine-3.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09abeb8f03f044d72712207e0489a62098ad3ad16dac38927fcf80baca4d6a7", size = 43508, upload-time = "2025-12-17T23:32:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/28/5a3ba2fce85b97655a425d6bb20a441550acd2b304c96b2c19d3839f721a/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b28367ce4f73987a55e230e1d30a57a3af85da8eb1a140074eb6e8c7e6ef19f", size = 41712, upload-time = "2025-12-17T23:32:55.781Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/e38084be7fdabb4835db68a3a47e58c34182d79fc35df1ecbe0db2c5359f/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:903c7751c904581da9f7861c3015bed7cdc40047321291d3694a3cdc783bbca3", size = 38939, upload-time = "2025-12-17T23:32:56.867Z" }, + { url = "https://files.pythonhosted.org/packages/40/d0/ad3feb0a392ef4e0c08bc32024950373ddc0669002cbdcbb9f3bf0c2d114/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:528217cad85ede5f85c8bc78b0341868d3c3cfefc6ecb5b622e1cacb6c73247b", size = 39837, upload-time = "2025-12-17T23:32:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/9e/5f4b2ea63b267bd78f3245e76f5528836611b5f2d30b5e7300a722fe4428/time_machine-3.2.0-cp314-cp314t-win32.whl", hash = "sha256:75724762ffd517e7e80aaec1fad1ff5a7414bd84e2b3ee7a0bacfeb67c14926e", size = 18091, upload-time = "2025-12-17T23:32:59.403Z" }, + { url = "https://files.pythonhosted.org/packages/39/6f/456b1f4d2700ae02b19eba830f870596a4b89b74bac3b6c80666f1b108c5/time_machine-3.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2526abbd053c5bca898d1b3e7898eec34626b12206718d8c7ce88fd12c1c9c5c", size = 19208, upload-time = "2025-12-17T23:33:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/8063101427ecd3d2652aada4d21d0876b07a3dc789125bca2ee858fec3ed/time_machine-3.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7f2fb6784b414edbe2c0b558bfaab0c251955ba27edd62946cce4a01675a992c", size = 17359, upload-time = "2025-12-17T23:33:01.54Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "types-aiofiles" +version = "25.1.0.20260409" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/66/9e62a2692792bc96c0f423f478149f4a7b84720704c546c8960b0a047c89/types_aiofiles-25.1.0.20260409.tar.gz", hash = "sha256:49e67d72bdcf9fe406f5815758a78dc34a1249bb5aa2adba78a80aec0a775435", size = 14812, upload-time = "2026-04-09T04:22:35.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/d0/28236f869ba4dfb223ecdbc267eb2bdb634b81a561dd992230a4f9ec48fa/types_aiofiles-25.1.0.20260409-py3-none-any.whl", hash = "sha256:923fedb532c772cc0f62e0ce4282725afa82ca5b41cabd9857f06b55e5eee8de", size = 14372, upload-time = "2026-04-09T04:22:34.328Z" }, +] + +[[package]] +name = "types-certifi" +version = "2021.10.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095, upload-time = "2022-06-09T15:19:05.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136, upload-time = "2022-06-09T15:19:03.127Z" }, +] + +[[package]] +name = "types-cffi" +version = "2.0.0.20260408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/67/eb4ef3408fdc0b4e5af38b30c0e6ad4663b41bdae9fb85a9f09a8db61a99/types_cffi-2.0.0.20260408.tar.gz", hash = "sha256:aa8b9c456ab715c079fc655929811f21f331bfb940f4a821987c581bf4e36230", size = 17541, upload-time = "2026-04-08T04:36:03.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/a3/7fbd93ededcc7c77e9e5948b9794161733ebdbf618a27965b1bea0e728a4/types_cffi-2.0.0.20260408-py3-none-any.whl", hash = "sha256:68bd296742b4ff7c0afe3547f50bd0acc55416ecf322ffefd2b7344ef6388a42", size = 20101, upload-time = "2026-04-08T04:36:02.995Z" }, +] + +[[package]] +name = "types-croniter" +version = "6.2.2.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/e4/89a0101471d6fe4e912dad24c54ae7afd90a9eaa5c74adef2c81f383f8da/types_croniter-6.2.2.20260408.tar.gz", hash = "sha256:a28a18908db371654990d30a3fd99856adc5137e475a23dbda4b10dce85525da", size = 12040, upload-time = "2026-04-08T04:27:20.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/05/b32e67944ff33e83c181cadf5835858d63f4292a2f2ff5bf6a1edb7f6fed/types_croniter-6.2.2.20260408-py3-none-any.whl", hash = "sha256:242087a5b6e201b7004e55f71ed34f466951b74551c64ef1c6a8a08c47d3cc0d", size = 9732, upload-time = "2026-04-08T04:27:19.229Z" }, +] + +[[package]] +name = "types-deprecated" +version = "1.3.1.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/db/076de3e81b106d3cec17aec9640ab1b2d02f29bad441de280459c161ce65/types_deprecated-1.3.1.20260408.tar.gz", hash = "sha256:62d6a86d0cc754c14bb2de31162d069b1c6a07ce11ee65e5258f8f75308eb3a3", size = 8524, upload-time = "2026-04-08T04:26:39.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/d0/d3258379deb749d949c3c72313981c9d2cceec518b87dcf506f022f5d49f/types_deprecated-1.3.1.20260408-py3-none-any.whl", hash = "sha256:b64e1eab560d4fa9394a27a3099211344b0e0f2f3ac8026d825c86e70d65cdd5", size = 9079, upload-time = "2026-04-08T04:26:38.752Z" }, +] + +[[package]] +name = "types-docutils" +version = "0.22.3.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/49/48a386fe15539556de085b87a69568b028cca2fa4b92596a3d4f79ac6784/types_docutils-0.22.3.20260408.tar.gz", hash = "sha256:22d5d45e4e0d65a1bc8280987a73e28669bb1cc9d16b18d0afc91713d1be26da", size = 57383, upload-time = "2026-04-08T04:27:26.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/47/1667fda6e9fcb044f8fb797f6dc4367b88dc2ab40f1a035e387f5405e870/types_docutils-0.22.3.20260408-py3-none-any.whl", hash = "sha256:2545a86966022cdf1468d430b0007eba0837be77974a7f3fafa1b04a6815d531", size = 91981, upload-time = "2026-04-08T04:27:25.934Z" }, +] + +[[package]] +name = "types-markdown" +version = "3.10.2.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/0e/a690840934c459aa50e0470e7550d7f151632eafa4a8e3c21d18009ad15c/types_markdown-3.10.2.20260408.tar.gz", hash = "sha256:d5cba15ed65a1420e80e31c17e3d4a2ad7208a3f3a4da97fd2c5f093caf523cd", size = 19784, upload-time = "2026-04-08T04:33:07.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/7e/265a8df257c8dced6ea89295f793a19f0a49ccbfeae1ed562368b2caf7a3/types_markdown-3.10.2.20260408-py3-none-any.whl", hash = "sha256:b0bbe8b7a8174db732067b86e391262898f5f536589ea81efec6d35ceb829331", size = 25857, upload-time = "2026-04-08T04:33:06.769Z" }, +] + +[[package]] +name = "types-paramiko" +version = "4.0.0.20260408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/f5/2a556b03ba264508b6bc6a65131500265f210ff3ebf5d76dbe51b53c3979/types_paramiko-4.0.0.20260408.tar.gz", hash = "sha256:978191a2e11064fa4c7f9ada0fccf49159a17beb98b780310dd2c2d2b4106063", size = 29116, upload-time = "2026-04-08T04:35:04.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/e2/cf451598a6a8820139d021b2be08a836b9b905d744bcc73b72172e7e10b3/types_paramiko-4.0.0.20260408-py3-none-any.whl", hash = "sha256:350bf53edb4eb88181be68854d598e1cc3a8764fe905d49913025b86e831adbc", size = 38816, upload-time = "2026-04-08T04:35:03.503Z" }, +] + +[[package]] +name = "types-protobuf" +version = "7.34.1.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/b1/4521e68c2cc17703d80eb42796751345376dd4c706f84007ef5e7c707774/types_protobuf-7.34.1.20260408.tar.gz", hash = "sha256:e2c0a0430e08c75b52671a6f0035abfdcc791aad12af16274282de1b721758ab", size = 68835, upload-time = "2026-04-08T04:26:43.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/b5/0bc9874d89c58fb0ce851e150055ce732d254dbb10b06becbc7635d0d635/types_protobuf-7.34.1.20260408-py3-none-any.whl", hash = "sha256:ebbcd4e27b145aef6a59bc0cb6c013b3528151c1ba5e7f7337aeee355d276a5e", size = 86012, upload-time = "2026-04-08T04:26:42.566Z" }, +] + +[[package]] +name = "types-pymysql" +version = "1.1.0.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/04/c3570f05ebab083f28698c829dddf754ffefc30aae4e29915610848e44db/types_pymysql-1.1.0.20260408.tar.gz", hash = "sha256:b784dc37908479e3767e2d794ab507b3674adb1c686ca3d13fc9e2960dbcb9ec", size = 22344, upload-time = "2026-04-08T04:27:47.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/b3/15dee33878709705a4cc83bcc1bb30e00e95bbe038b472cb1207a15b50a1/types_pymysql-1.1.0.20260408-py3-none-any.whl", hash = "sha256:da630647eaaa7a926a3907794f4067f269cd245b2c202c74aa3c6a3bd660a9db", size = 23071, upload-time = "2026-04-08T04:27:46.735Z" }, +] + +[[package]] +name = "types-pyopenssl" +version = "24.1.0.20240722" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "types-cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458, upload-time = "2024-07-22T02:32:22.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499, upload-time = "2024-07-22T02:32:21.232Z" }, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/f3/2427775f80cd5e19a0a71ba8e5ab7645a01a852f43a5fd0ffc24f66338e0/types_python_dateutil-2.9.0.20260408.tar.gz", hash = "sha256:8b056ec01568674235f64ecbcef928972a5fac412f5aab09c516dfa2acfbb582", size = 16981, upload-time = "2026-04-08T04:28:10.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c6/eeba37bfee282a6a97f889faef9352d6172c6a5088eb9a4daf570d9d748d/types_python_dateutil-2.9.0.20260408-py3-none-any.whl", hash = "sha256:473139d514a71c9d1fbd8bb328974bedcb1cc3dba57aad04ffa4157f483c216f", size = 18437, upload-time = "2026-04-08T04:28:10.095Z" }, +] + +[[package]] +name = "types-python-slugify" +version = "8.0.2.20240310" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/cb/7fdc1982b117d216a9ebbe4ecc6619690991c48f994d93dee7888f459976/types-python-slugify-8.0.2.20240310.tar.gz", hash = "sha256:5157b508c7fed587520c70d77f62aea0fafdc6620893c2ec8972f13a1faf5560", size = 3661, upload-time = "2024-03-10T02:19:03.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/6d/873300a60133e51b284a5287a618d3d948a65160e3954b90b3dc5e562667/types_python_slugify-8.0.2.20240310-py3-none-any.whl", hash = "sha256:0efec18b802c69ebd22dcee55c91afaeaa80e1e40ddd66ccabf69fd42ce87b74", size = 3566, upload-time = "2024-03-10T02:19:02.575Z" }, +] + +[[package]] +name = "types-pytz" +version = "2026.1.1.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/b7/33f5a4f29b1f285b99ff79a607751a7996194cbb98705e331dab7a2daa28/types_pytz-2026.1.1.20260408.tar.gz", hash = "sha256:89b6a34b9198ea2a4b98a9d15cbca987053f52a105fd44f7ce3789cae4349408", size = 10788, upload-time = "2026-04-08T04:28:14.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/90/12c059e6bb330a22d9cc97daf027ac7fb7f50fbf518e4d88185b4d39120e/types_pytz-2026.1.1.20260408-py3-none-any.whl", hash = "sha256:c7e4dec76221fb7d0c97b91ad8561d689bebe39b6bcb7b728387e7ffd8cde788", size = 10124, upload-time = "2026-04-08T04:28:13.353Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/73/b759b1e413c31034cc01ecdfb96b38115d0ab4db55a752a3929f0cd449fd/types_pyyaml-6.0.12.20260408.tar.gz", hash = "sha256:92a73f2b8d7f39ef392a38131f76b970f8c66e4c42b3125ae872b7c93b556307", size = 17735, upload-time = "2026-04-08T04:30:50.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/f0/c391068b86abb708882c6d75a08cd7d25b2c7227dab527b3a3685a3c635b/types_pyyaml-6.0.12.20260408-py3-none-any.whl", hash = "sha256:fbc42037d12159d9c801ebfcc79ebd28335a7c13b08a4cfbc6916df78fee9384", size = 20339, upload-time = "2026-04-08T04:30:50.113Z" }, +] + +[[package]] +name = "types-redis" +version = "4.6.0.20241004" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "types-pyopenssl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/95/c054d3ac940e8bac4ca216470c80c26688a0e79e09f520a942bb27da3386/types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", size = 49679, upload-time = "2024-10-04T02:43:59.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/82/7d25dce10aad92d2226b269bce2f85cfd843b4477cd50245d7d40ecf8f89/types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed", size = 58737, upload-time = "2024-10-04T02:43:57.968Z" }, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20260408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/6a/749dc53a54a3f35842c1f8197b3ca6b54af6d7458a1bfc75f6629b6da666/types_requests-2.33.0.20260408.tar.gz", hash = "sha256:95b9a86376807a216b2fb412b47617b202091c3ea7c078f47cc358d5528ccb7b", size = 23882, upload-time = "2026-04-08T04:34:49.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/b8/78fd6c037de4788c040fdd323b3369804400351b7827473920f6c1d03c10/types_requests-2.33.0.20260408-py3-none-any.whl", hash = "sha256:81f31d5ea4acb39f03be7bc8bed569ba6d5a9c5d97e89f45ac43d819b68ca50f", size = 20739, upload-time = "2026-04-08T04:34:48.325Z" }, +] + +[[package]] +name = "types-setuptools" +version = "82.0.0.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/12/3464b410c50420dd4674fa5fe9d3880711c1dbe1a06f5fe4960ee9067b9e/types_setuptools-82.0.0.20260408.tar.gz", hash = "sha256:036c68caf7e672a699f5ebbf914708d40644c14e05298bc49f7272be91cf43d3", size = 44861, upload-time = "2026-04-08T04:29:33.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/e1/46a4fc3ef03aabf5d18bac9df5cf37c6b02c3bddf3e05c3533f4b4588331/types_setuptools-82.0.0.20260408-py3-none-any.whl", hash = "sha256:ece0a215cdfa6463a65fd6f68bd940f39e455729300ddfe61cab1147ed1d2462", size = 68428, upload-time = "2026-04-08T04:29:32.175Z" }, +] + +[[package]] +name = "types-tabulate" +version = "0.10.0.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/59/b563bfb6e216b8573052c09cb4abcbdca836487db4cfad9b7d492c327c0b/types_tabulate-0.10.0.20260408.tar.gz", hash = "sha256:903d62fdf7e5a0ff659fd5d629df716232f7658c6d30e98f0374488d06ffacf4", size = 8367, upload-time = "2026-04-08T04:30:00.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/d1/34e27f543dd944f51fc6b0013a1a41113079cede9cc3be0a5f426f2f8d9d/types_tabulate-0.10.0.20260408-py3-none-any.whl", hash = "sha256:2b19d193603d38c34645de53c0c1087e2364487d518d4a2f44268db2366723cc", size = 8139, upload-time = "2026-04-08T04:29:59.699Z" }, +] + +[[package]] +name = "types-toml" +version = "0.10.8.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/9b/887564a51a84c96ba08b715570e546f0ea793df6372b736bfbc596ca5536/types_toml-0.10.8.20260408.tar.gz", hash = "sha256:6b30b031235565a12febb1388900b129f1adeabfcfa594da46d0372b2ac107ad", size = 9341, upload-time = "2026-04-08T04:27:54.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/f1/942d95ba026779bc6e3064f8b094216588dc3276cc328cf8e03a0541918d/types_toml-0.10.8.20260408-py3-none-any.whl", hash = "sha256:e958d4c660385e548705a298f17dc162baf44c8b6d6aff79aeefe75f4f77ac87", size = 9677, upload-time = "2026-04-08T04:27:53.526Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "wheel" +version = "0.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/24/a2eb353a6edac9a0303977c4cb048134959dd2a51b48a269dfc9dde00c8a/wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803", size = 60605, upload-time = "2026-01-22T12:39:49.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", size = 30557, upload-time = "2026-01-22T12:39:48.099Z" }, +] + +[[package]] +name = "yamllint" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathspec" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/a0/8fc2d68e132cf918f18273fdc8a1b8432b60d75ac12fdae4b0ef5c9d2e8d/yamllint-1.38.0.tar.gz", hash = "sha256:09e5f29531daab93366bb061e76019d5e91691ef0a40328f04c927387d1d364d", size = 142446, upload-time = "2026-01-13T07:47:53.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/92/aed08e68de6e6a3d7c2328ce7388072cd6affc26e2917197430b646aed02/yamllint-1.38.0-py3-none-any.whl", hash = "sha256:fc394a5b3be980a4062607b8fdddc0843f4fa394152b6da21722f5d59013c220", size = 68940, upload-time = "2026-01-13T07:47:51.343Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] diff --git a/task-sdk/pyproject.toml b/task-sdk/pyproject.toml index 5937011e8d243..ccdab19e5b1aa 100644 --- a/task-sdk/pyproject.toml +++ b/task-sdk/pyproject.toml @@ -144,6 +144,7 @@ path = "src/airflow/sdk/__init__.py" "../shared/secrets_masker/src/airflow_shared/secrets_masker" = "src/airflow/sdk/_shared/secrets_masker" "../shared/serialization/src/airflow_shared/serialization" = "src/airflow/sdk/_shared/serialization" "../shared/timezones/src/airflow_shared/timezones" = "src/airflow/sdk/_shared/timezones" +"../shared/workloads/src/airflow_shared/workloads" = "src/airflow/sdk/_shared/workloads" "../shared/listeners/src/airflow_shared/listeners" = "src/airflow/sdk/_shared/listeners" "../shared/plugins_manager/src/airflow_shared/plugins_manager" = "src/airflow/sdk/_shared/plugins_manager" "../shared/providers_discovery/src/airflow_shared/providers_discovery" = "src/airflow/sdk/_shared/providers_discovery" @@ -320,6 +321,7 @@ shared_distributions = [ "apache-airflow-shared-serialization", "apache-airflow-shared-state", "apache-airflow-shared-timezones", + "apache-airflow-shared-workloads", "apache-airflow-shared-observability", "apache-airflow-shared-plugins-manager", "apache-airflow-shared-providers-discovery", diff --git a/task-sdk/src/airflow/sdk/_shared/workloads b/task-sdk/src/airflow/sdk/_shared/workloads new file mode 120000 index 0000000000000..25036f1ff8e02 --- /dev/null +++ b/task-sdk/src/airflow/sdk/_shared/workloads @@ -0,0 +1 @@ +../../../../../shared/workloads/src/airflow_shared/workloads \ No newline at end of file diff --git a/task-sdk/src/airflow/sdk/execution_time/comms.py b/task-sdk/src/airflow/sdk/execution_time/comms.py index 7b494cab83592..7208963fe0ff2 100644 --- a/task-sdk/src/airflow/sdk/execution_time/comms.py +++ b/task-sdk/src/airflow/sdk/execution_time/comms.py @@ -64,6 +64,7 @@ import structlog from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, JsonValue, TypeAdapter +from airflow.sdk._shared.workloads import TaskInstanceDTO # noqa: TC001 -- Pydantic needs this at runtime from airflow.sdk.api.datamodels._generated import ( AssetEventDagRunReference, AssetEventResponse, @@ -80,7 +81,6 @@ PreviousTIResponse, PrevSuccessfulDagRunResponse, TaskBreadcrumbsResponse, - TaskInstance, TaskInstanceState, TaskStateResponse, TaskStatesResponse, @@ -334,7 +334,7 @@ def _get_response(self) -> ReceiveMsgType | None: class StartupDetails(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) - ti: TaskInstance + ti: TaskInstanceDTO dag_rel_path: str bundle_info: BundleInfo start_date: datetime diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 5dfb3068466e2..1fc684489a8a3 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -51,7 +51,8 @@ if TYPE_CHECKING: from structlog.typing import FilteringBoundLogger - from airflow.sdk.api.datamodels._generated import BundleInfo, TaskInstance + from airflow.sdk._shared.workloads import TaskInstanceDTO + from airflow.sdk.api.datamodels._generated import BundleInfo from airflow.sdk.execution_time.comms import StartupDetails @@ -195,7 +196,7 @@ class DagParsingInfo(NamedTuple): class TaskExecutionInfo(NamedTuple): """Information needed for runtime task execution.""" - what: TaskInstance + what: TaskInstanceDTO dag_rel_path: str | os.PathLike[str] bundle_info: BundleInfo startup_details: StartupDetails @@ -259,7 +260,7 @@ def dag_parsing_runtime_cmd( def task_execution_runtime_cmd( cls, *, - what: TaskInstance, + what: TaskInstanceDTO, dag_file_path: str, bundle_path: str, bundle_info: BundleInfo, @@ -296,7 +297,7 @@ def run_dag_parsing(cls, *, path: str, bundle_name: str, bundle_path: str) -> No def run_task_execution( cls, *, - what: TaskInstance, + what: TaskInstanceDTO, dag_rel_path: str | os.PathLike[str], bundle_info: BundleInfo, startup_details: StartupDetails, diff --git a/task-sdk/src/airflow/sdk/execution_time/supervisor.py b/task-sdk/src/airflow/sdk/execution_time/supervisor.py index 30bcad10bce00..822e9700da81b 100644 --- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py +++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py @@ -167,6 +167,7 @@ from structlog.typing import FilteringBoundLogger, WrappedLogger from typing_extensions import Self + from airflow._shared.workloads import TaskInstanceDTO from airflow.executors.workloads import BundleInfo from airflow.sdk.bases.secrets_backend import BaseSecretsBackend from airflow.sdk.definitions.connection import Connection @@ -1203,7 +1204,7 @@ class ActivitySubprocess(WatchedSubprocess): def start( # type: ignore[override] cls, *, - what: TaskInstance, + what: TaskInstanceDTO, dag_rel_path: str | os.PathLike[str], bundle_info, client: Client, @@ -1232,7 +1233,7 @@ def start( # type: ignore[override] def _on_child_started( self, *, - ti: TaskInstance, + ti: TaskInstanceDTO, dag_rel_path: str | os.PathLike[str], bundle_info, sentry_integration: str, @@ -2216,7 +2217,7 @@ def _configure_logging(log_path: str, client: Client) -> tuple[FilteringBoundLog def supervise_task( *, - ti: TaskInstance, + ti: TaskInstanceDTO, bundle_info: BundleInfo, dag_rel_path: str | os.PathLike[str], token: str, diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index e2e14b43b2917..1f9a9c1030e17 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -30,7 +30,7 @@ from datetime import datetime, timedelta, timezone from itertools import product from pathlib import Path -from typing import TYPE_CHECKING, Annotated, Any, Literal +from typing import TYPE_CHECKING, Annotated, Any, Literal, cast from urllib.parse import quote import attrs @@ -146,6 +146,7 @@ from pendulum.datetime import DateTime from structlog.typing import FilteringBoundLogger as Logger + from airflow._shared.workloads import TaskInstanceDTO from airflow.sdk.definitions._internal.abstractoperator import AbstractOperator from airflow.sdk.definitions.context import Context from airflow.sdk.definitions.retry_policy import RetryDecision @@ -2124,7 +2125,7 @@ def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> ) return functools.partial( coordinator_cls.run_task_execution, - what=startup_details.ti, + what=cast("TaskInstanceDTO", startup_details.ti), dag_rel_path=startup_details.dag_rel_path, bundle_info=startup_details.bundle_info, startup_details=startup_details, diff --git a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py index 11d22865c0dd0..952e85ba97915 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py +++ b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py @@ -52,6 +52,7 @@ from task_sdk import FAKE_BUNDLE, make_client from uuid6 import uuid7 +from airflow._shared.workloads import TaskInstanceDTO from airflow.executors.workloads import BundleInfo from airflow.sdk import DAG, BaseOperator, timezone from airflow.sdk.api import client as sdk_client @@ -642,8 +643,16 @@ def subprocess_main(): proc = ActivitySubprocess.start( dag_rel_path=os.devnull, bundle_info=FAKE_BUNDLE, - what=TaskInstance( - id=ti_id, task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7() + what=TaskInstanceDTO( + id=ti_id, + task_id="b", + dag_id="c", + run_id="d", + try_number=1, + dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=sdk_client.Client(base_url="", dry_run=True, token=""), target=subprocess_main, @@ -679,8 +688,16 @@ def _on_child_started(self, *args, **kwargs): proc = ActivitySubprocess.start( dag_rel_path=os.devnull, bundle_info=FAKE_BUNDLE, - what=TaskInstance( - id=ti_id, task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7() + what=TaskInstanceDTO( + id=ti_id, + task_id="b", + dag_id="c", + run_id="d", + try_number=1, + dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=sdk_client.Client(base_url="", dry_run=True, token=""), target=subprocess_main, diff --git a/uv.lock b/uv.lock index 108cad880e51b..4baf294c7c047 100644 --- a/uv.lock +++ b/uv.lock @@ -98,6 +98,7 @@ apache-airflow-shared-serialization = false apache-airflow-scripts = false apache-airflow-providers-exasol = false apache-airflow-providers-mongo = false +apache-airflow-shared-workloads = false apache-airflow-providers-apprise = false apache-airflow-providers-apache-impala = false apache-airflow-ctl = false @@ -287,6 +288,7 @@ members = [ "apache-airflow-shared-state", "apache-airflow-shared-template-rendering", "apache-airflow-shared-timezones", + "apache-airflow-shared-workloads", "apache-airflow-task-sdk", "apache-airflow-task-sdk-integration-tests", "docker-stack", @@ -1514,6 +1516,7 @@ dev = [ { name = "apache-airflow-shared-state" }, { name = "apache-airflow-shared-template-rendering" }, { name = "apache-airflow-shared-timezones" }, + { name = "apache-airflow-shared-workloads" }, { name = "apache-airflow-task-sdk", extra = ["all"] }, { name = "apache-airflow-task-sdk-integration-tests" }, ] @@ -1820,6 +1823,7 @@ dev = [ { name = "apache-airflow-shared-state", editable = "shared/state" }, { name = "apache-airflow-shared-template-rendering", editable = "shared/template_rendering" }, { name = "apache-airflow-shared-timezones", editable = "shared/timezones" }, + { name = "apache-airflow-shared-workloads", editable = "shared/workloads" }, { name = "apache-airflow-task-sdk", extras = ["all"], editable = "task-sdk" }, { name = "apache-airflow-task-sdk-integration-tests", editable = "task-sdk-integration-tests" }, ] @@ -8650,6 +8654,25 @@ requires-dist = [ dev = [{ name = "apache-airflow-devel-common", editable = "devel-common" }] mypy = [{ name = "apache-airflow-devel-common", extras = ["mypy"], editable = "devel-common" }] +[[package]] +name = "apache-airflow-shared-workloads" +version = "0.0" +source = { editable = "shared/workloads" } +dependencies = [ + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "apache-airflow-devel-common" }, +] + +[package.metadata] +requires-dist = [{ name = "pydantic", specifier = ">=2.10.0" }] + +[package.metadata.requires-dev] +dev = [{ name = "apache-airflow-devel-common", editable = "devel-common" }] + [[package]] name = "apache-airflow-task-sdk" source = { editable = "task-sdk" } From 9f0acdf4bda4434922cb0838ae1c0c19ece4fa34 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 22 Apr 2026 14:16:33 +0800 Subject: [PATCH 23/75] Add [workers/queue_to_runtime_mapping] --- .../src/airflow/config_templates/config.yml | 14 ++++++ .../tests/airflow_e2e_tests/conftest.py | 1 + .../src/tests_common/pytest_plugin.py | 9 ++-- .../src/airflow_shared/workloads/__init__.py | 2 +- .../airflow/sdk/execution_time/coordinator.py | 38 ++++++++++++++- .../airflow/sdk/execution_time/task_runner.py | 47 +++++++++++++++---- .../execution_time/test_task_runner.py | 25 ++++++++-- 7 files changed, 116 insertions(+), 20 deletions(-) diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml index a6f3de25e2196..a12781aa3bef6 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -2004,6 +2004,20 @@ workers: type: integer example: ~ default: "60" + queue_to_runtime_mapping: + description: | + JSON mapping of queue names to runtime coordinator names. + + When a task's ``language`` field is not set, this mapping is checked + to route the task to a non-Python runtime coordinator based on its + queue. This is useful when queues are used as environment or + isolation identifiers (e.g. ``foo``, ``bar``). + + Example: ``{"foo": "java", "bar": "java", "go-queue": "go"}`` + version_added: 3.1.7 + type: string + example: '{"foo": "java", "bar": "java", "go-queue": "go"}' + default: "{{}}" api_auth: description: Settings relating to authentication on the Airflow APIs options: diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py index 95952e4ae80f0..e4280744c38b9 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py @@ -234,6 +234,7 @@ def _setup_xcom_object_storage_integration(dot_env_file, tmp_dir): os.environ["ENV_FILE_PATH"] = str(dot_env_file) +<<<<<<< HEAD def spin_up_airflow_environment(tmp_path_factory: pytest.TempPathFactory): tmp_dir = tmp_path_factory.mktemp("breeze-airflow-e2e-tests") diff --git a/devel-common/src/tests_common/pytest_plugin.py b/devel-common/src/tests_common/pytest_plugin.py index 6f47d98fd649d..dbfc7946f4d01 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -2519,7 +2519,6 @@ def execute(self, context): from uuid6 import uuid7 from airflow.sdk import DAG - from airflow.sdk.api.datamodels._generated import TaskInstance from airflow.sdk.execution_time.comms import BundleInfo, StartupDetails from airflow.timetables.base import TimeRestriction @@ -2547,6 +2546,7 @@ def _create_task_instance( should_retry: bool | None = None, max_tries: int | None = None, ) -> RuntimeTaskInstance: + from airflow.sdk._shared.workloads import TaskInstanceDTO from airflow.sdk.api.datamodels._generated import DagRun, DagRunState, TIRunContext from airflow.utils.types import DagRunType @@ -2624,14 +2624,17 @@ def _create_task_instance( } startup_details = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=ti_id, task_id=task.task_id, dag_id=dag_id, run_id=run_id, try_number=try_number, - map_index=map_index, + map_index=map_index if map_index is not None else -1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), dag_rel_path="", bundle_info=BundleInfo(name="anything", version="any"), diff --git a/shared/workloads/src/airflow_shared/workloads/__init__.py b/shared/workloads/src/airflow_shared/workloads/__init__.py index 3fc5ebe5277c1..b248de114a648 100644 --- a/shared/workloads/src/airflow_shared/workloads/__init__.py +++ b/shared/workloads/src/airflow_shared/workloads/__init__.py @@ -39,7 +39,7 @@ class TaskInstanceDTO(BaseModel): priority_weight: int executor_config: dict | None = Field(default=None, exclude=True) - language: str | None = None + sdk: str | None = None parent_context_carrier: dict | None = None context_carrier: dict | None = None diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 1fc684489a8a3..1995561064071 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -429,4 +429,40 @@ def _runtime_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskEx _bridge(supervisor_comm, runtime_comm, runtime_logs, read_stderr, proc, log) -__all__ = ["BaseRuntimeCoordinator"] +class QueueToRuntimeCoordinatorMapper: + """ + Map queue names to runtime coordinator names. + + Users often use queues as environment/isolation identifiers (e.g. ``"java-11"``, + ``"java-12"``). This mapper lets them reuse existing queue assignments to route + tasks to the correct runtime coordinator without requiring the ``sdk`` field + on every operator. + + The mapping is read from the ``[workers] queue_to_runtime_mapping`` + configuration option, which is a JSON dict of ``queue_name -> runtime_name``. + + Example configuration:: + + [workers] + queue_to_runtime_mapping = {"java-11": "java", "java-12": "java"} + """ + + def __init__(self, mapping: dict[str, str]) -> None: + self._mapping = mapping + + @classmethod + def from_config(cls) -> QueueToRuntimeCoordinatorMapper: + """Load the queue-to-runtime mapping from airflow configuration.""" + from airflow.sdk.configuration import conf + + mapping = conf.getjson("workers", "queue_to_runtime_mapping", fallback={}) + if not isinstance(mapping, dict): + return cls({}) + return cls(mapping) + + def resolve(self, queue: str) -> str | None: + """Return the runtime coordinator name for *queue*, or ``None`` if unmapped.""" + return self._mapping.get(queue) + + +__all__ = ["BaseRuntimeCoordinator", "QueueToRuntimeCoordinatorMapper"] diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 1f9a9c1030e17..4128aefb591ba 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -30,7 +30,7 @@ from datetime import datetime, timedelta, timezone from itertools import product from pathlib import Path -from typing import TYPE_CHECKING, Annotated, Any, Literal, cast +from typing import TYPE_CHECKING, Annotated, Any, Literal from urllib.parse import quote import attrs @@ -146,7 +146,6 @@ from pendulum.datetime import DateTime from structlog.typing import FilteringBoundLogger as Logger - from airflow._shared.workloads import TaskInstanceDTO from airflow.sdk.definitions._internal.abstractoperator import AbstractOperator from airflow.sdk.definitions.context import Context from airflow.sdk.definitions.retry_policy import RetryDecision @@ -2097,14 +2096,35 @@ def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> """ Check provider-registered runtime coordinators for a runtime-specific entrypoint. - If the task's ``sdk`` field matches a coordinator's ``runtime_name``, - return a no-arg callable that bridges fd 0 to the runtime subprocess. - Otherwise return ``None`` to fall through to the standard Python - execution path. + Resolution order: + + 1. If the task's ``sdk`` field is set, match it directly against + coordinator ``runtime_name`` values. + 2. Otherwise, consult the ``[workers] queue_to_runtime_mapping`` + configuration to see if the task's ``queue`` maps to a runtime name. + + Returns a no-arg callable that bridges fd 0 to the runtime subprocess, + or ``None`` to fall through to the standard Python execution path. """ sdk = startup_details.ti.sdk + + # Fallback: resolve runtime name from queue mapping when sdk is not set. if sdk is None: - return None + from airflow.sdk.execution_time.coordinator import QueueToRuntimeCoordinatorMapper + + log.debug( + "No sdk specified for task, attempting to resolve runtime from queue mapping", + queue=startup_details.ti.queue, + task_id=startup_details.ti.task_id, + ) + sdk = QueueToRuntimeCoordinatorMapper.from_config().resolve(startup_details.ti.queue) + if sdk is None: + log.debug( + "No runtime found for task queue, using standard Python execution path", + queue=startup_details.ti.queue, + task_id=startup_details.ti.task_id, + ) + return None import functools @@ -2125,7 +2145,7 @@ def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> ) return functools.partial( coordinator_cls.run_task_execution, - what=cast("TaskInstanceDTO", startup_details.ti), + what=startup_details.ti, dag_rel_path=startup_details.dag_rel_path, bundle_info=startup_details.bundle_info, startup_details=startup_details, @@ -2166,8 +2186,15 @@ def main(): # startup message as a ResendLoggingFD response. if os.environ.pop("_AIRFLOW_FORK_EXEC", None) == "1": reinit_supervisor_comms() - span_ctx_mgr = _make_task_span(msg=startup_details) - span = stack.enter_context(span_ctx_mgr) + # Check if a provider-registered runtime coordinator should + # handle this task (e.g. Java, Go) instead of the standard + # Python execution path. + runtime_entrypoint = _resolve_runtime_entrypoint(startup_details, log) + if runtime_entrypoint is not None: + runtime_entrypoint() + return + span = _make_task_span(msg=startup_details) + stack.enter_context(span) ti, context, log = startup(msg=startup_details) except AirflowRescheduleException as reschedule: log.warning("Rescheduling task during startup, marking task as UP_FOR_RESCHEDULE") diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py index 4ba821e537e56..dbac25fcf1678 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py +++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py @@ -201,13 +201,16 @@ def execute(self, context): def test_parse(test_dags_dir: Path, make_ti_context): """Test that checks parsing of a basic dag with an un-mocked parse.""" what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="a", dag_id="super_basic", run_id="c", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), dag_rel_path="super_basic.py", bundle_info=BundleInfo(name="my-bundle", version=None), @@ -248,13 +251,16 @@ def test_parse_dag_bag(mock_dagbag, test_dags_dir: Path, make_ti_context): mock_dag.task_dict = {"a": mock_task} what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="a", dag_id="super_basic", run_id="c", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), dag_rel_path="super_basic.py", bundle_info=BundleInfo(name="my-bundle", version=None), @@ -308,13 +314,16 @@ def test_parse_dag_bag(mock_dagbag, test_dags_dir: Path, make_ti_context): def test_parse_not_found(test_dags_dir: Path, make_ti_context, dag_id, task_id, expected_error): """Check for nice error messages on dag not found.""" what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id=task_id, dag_id=dag_id, run_id="c", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), dag_rel_path="super_basic.py", bundle_info=BundleInfo(name="my-bundle", version=None), @@ -354,13 +363,16 @@ def test_parse_not_found_does_not_reschedule_when_max_attempts_reached(test_dags and should surface as a hard failure (SystemExit in the task runner process). """ what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="a", dag_id="madeup_dag_id", run_id="c", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), dag_rel_path="super_basic.py", bundle_info=BundleInfo(name="my-bundle", version=None), @@ -697,13 +709,16 @@ def test_parse_module_in_bundle_root(tmp_path: Path, make_ti_context): dag1_path.write_text(textwrap.dedent(dag1_code)) what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="a", dag_id="dag_name", run_id="c", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), dag_rel_path="path_test.py", bundle_info=BundleInfo(name="my-bundle", version=None), From de8e5a5902fd470455b4e81d229169f98b6370ec Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 20:29:29 +0800 Subject: [PATCH 24/75] Remove the sdk field from stub operator and respect [workers/queue_to_runtime_mapping] config --- .../execution_api/datamodels/taskinstance.py | 1 - .../execution_api/versions/v2026_07_01.py | 30 ------- .../src/airflow/executors/workloads/task.py | 3 +- ...12_3_3_0_add_sdk_field_to_task_instance.py | 50 ----------- .../src/airflow/models/taskinstance.py | 3 - .../tests/airflow_e2e_tests/conftest.py | 1 - .../providers/standard/decorators/stub.py | 6 -- .../src/airflow_shared/workloads/__init__.py | 2 - .../airflow/sdk/api/datamodels/_generated.py | 1 - task-sdk/src/airflow/sdk/bases/operator.py | 3 - .../airflow/sdk/execution_time/coordinator.py | 3 +- .../airflow/sdk/execution_time/task_runner.py | 84 ++++++++++++------- 12 files changed, 56 insertions(+), 131 deletions(-) delete mode 100644 airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_07_01.py delete mode 100644 airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_sdk_field_to_task_instance.py diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py index c1f3ca6ae72bb..a0d9739080118 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py @@ -256,7 +256,6 @@ class TaskInstance(BaseModel): map_index: int = -1 hostname: str | None = None context_carrier: dict | None = None - sdk: str | None = None class AssetReferenceAssetEventDagRun(StrictBaseModel): diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_07_01.py b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_07_01.py deleted file mode 100644 index 0534365dea2bf..0000000000000 --- a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_07_01.py +++ /dev/null @@ -1,30 +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 cadwyn import VersionChange, schema - -from airflow.api_fastapi.execution_api.datamodels.taskinstance import TaskInstance - - -class AddSdkField(VersionChange): - """Add the `sdk` field to the TaskInstance model.""" - - description = __doc__ - - instructions_to_migrate_to_previous_version = (schema(TaskInstance).field("sdk").didnt_exist,) diff --git a/airflow-core/src/airflow/executors/workloads/task.py b/airflow-core/src/airflow/executors/workloads/task.py index 5309eed1a42f8..4c563f34351d8 100644 --- a/airflow-core/src/airflow/executors/workloads/task.py +++ b/airflow-core/src/airflow/executors/workloads/task.py @@ -18,6 +18,7 @@ from __future__ import annotations +import uuid from pathlib import Path from typing import TYPE_CHECKING, Literal @@ -56,8 +57,6 @@ class BaseTaskInstanceDTO(BaseModel): priority_weight: int executor_config: dict | None = Field(default=None, exclude=True) - sdk: str | None = None - parent_context_carrier: dict | None = None context_carrier: dict | None = None diff --git a/airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_sdk_field_to_task_instance.py b/airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_sdk_field_to_task_instance.py deleted file mode 100644 index 2d535c102bf58..0000000000000 --- a/airflow-core/src/airflow/migrations/versions/0112_3_3_0_add_sdk_field_to_task_instance.py +++ /dev/null @@ -1,50 +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. - -""" -Add sdk field to task instance. - -Revision ID: 7d3c6395b7f6 -Revises: 9fabad868fdb -Create Date: 2026-04-15 05:57:22.353951 - -""" - -from __future__ import annotations - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision = "7d3c6395b7f6" -down_revision = "9fabad868fdb" -branch_labels = None -depends_on = None -airflow_version = "3.3.0" - - -def upgrade(): - """Apply add sdk field to task instance.""" - with op.batch_alter_table("task_instance", schema=None) as batch_op: - batch_op.add_column(sa.Column("sdk", sa.String(length=64), nullable=True)) - - -def downgrade(): - """Unapply add sdk field to task instance.""" - with op.batch_alter_table("task_instance", schema=None) as batch_op: - batch_op.drop_column("sdk") diff --git a/airflow-core/src/airflow/models/taskinstance.py b/airflow-core/src/airflow/models/taskinstance.py index bcac80095faa3..9cc8d6a627140 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -578,7 +578,6 @@ class TaskInstance(Base, LoggingMixin, BaseWorkload): priority_weight: Mapped[int | None] = mapped_column(Integer, nullable=True) operator: Mapped[str | None] = mapped_column(String(1000), nullable=True) custom_operator_name: Mapped[str | None] = mapped_column(String(1000), nullable=True) - sdk: Mapped[str | None] = mapped_column(String(64), nullable=True) queued_dttm: Mapped[datetime | None] = mapped_column(UtcDateTime, nullable=True) scheduled_dttm: Mapped[datetime | None] = mapped_column(UtcDateTime, nullable=True) queued_by_job_id: Mapped[int | None] = mapped_column(Integer, nullable=True) @@ -766,7 +765,6 @@ def insert_mapping( "executor_config": task.executor_config, "operator": task.task_type, "custom_operator_name": getattr(task, "operator_name", None), - "sdk": getattr(task, "sdk", None), "map_index": map_index, "_task_display_property_value": task.task_display_name, "dag_version_id": dag_version_id, @@ -945,7 +943,6 @@ def refresh_from_task(self, task: Operator, pool_override: str | None = None) -> self.executor = task.executor self.executor_config = task.executor_config self.operator = task.task_type - self.sdk = getattr(task, "sdk", None) op_name = getattr(task, "operator_name", None) self.custom_operator_name = op_name if isinstance(op_name, str) else "" # Re-apply cluster policy here so that task default do not overload previous data diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py index e4280744c38b9..95952e4ae80f0 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py @@ -234,7 +234,6 @@ def _setup_xcom_object_storage_integration(dot_env_file, tmp_dir): os.environ["ENV_FILE_PATH"] = str(dot_env_file) -<<<<<<< HEAD def spin_up_airflow_environment(tmp_path_factory: pytest.TempPathFactory): tmp_dir = tmp_path_factory.mktemp("breeze-airflow-e2e-tests") diff --git a/providers/standard/src/airflow/providers/standard/decorators/stub.py b/providers/standard/src/airflow/providers/standard/decorators/stub.py index 8feecbc51a29f..a5e63d925f795 100644 --- a/providers/standard/src/airflow/providers/standard/decorators/stub.py +++ b/providers/standard/src/airflow/providers/standard/decorators/stub.py @@ -78,7 +78,6 @@ def stub( python_callable: Callable | None = None, queue: str | None = None, executor: str | None = None, - sdk: str | None = None, **kwargs, ) -> TaskDecorator: """ @@ -86,16 +85,11 @@ def stub( Stub tasks exist in the Dag graph only, but the execution must happen in an external environment via the Task Execution Interface. - - :param sdk: The SDK runtime that should execute this task (e.g. ``"java"``). - When set, the worker routes the task to the matching runtime coordinator - instead of executing it as Python. """ return task_decorator_factory( decorated_operator_class=_StubOperator, python_callable=python_callable, queue=queue, executor=executor, - sdk=sdk, **kwargs, ) diff --git a/shared/workloads/src/airflow_shared/workloads/__init__.py b/shared/workloads/src/airflow_shared/workloads/__init__.py index b248de114a648..1a30b11c4b6af 100644 --- a/shared/workloads/src/airflow_shared/workloads/__init__.py +++ b/shared/workloads/src/airflow_shared/workloads/__init__.py @@ -39,8 +39,6 @@ class TaskInstanceDTO(BaseModel): priority_weight: int executor_config: dict | None = Field(default=None, exclude=True) - sdk: str | None = None - parent_context_carrier: dict | None = None context_carrier: dict | None = None diff --git a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py index 359e3e175df72..2d999111eb01b 100644 --- a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py +++ b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py @@ -512,7 +512,6 @@ class TaskInstance(BaseModel): map_index: Annotated[int | None, Field(title="Map Index")] = -1 hostname: Annotated[str | None, Field(title="Hostname")] = None context_carrier: Annotated[dict[str, Any] | None, Field(title="Context Carrier")] = None - sdk: Annotated[str | None, Field(title="Sdk")] = None class BundleInfo(BaseModel): diff --git a/task-sdk/src/airflow/sdk/bases/operator.py b/task-sdk/src/airflow/sdk/bases/operator.py index 1490123bbc25e..8d6de54eb6d3d 100644 --- a/task-sdk/src/airflow/sdk/bases/operator.py +++ b/task-sdk/src/airflow/sdk/bases/operator.py @@ -905,7 +905,6 @@ def say_hello_world(**context): max_active_tis_per_dagrun: int | None = None executor: str | None = None executor_config: dict | None = None - sdk: str | None = None do_xcom_push: bool = True multiple_outputs: bool = False inlets: list[Any] = field(default_factory=list) @@ -1064,7 +1063,6 @@ def __init__( max_active_tis_per_dagrun: int | None = None, executor: str | None = None, executor_config: dict | None = None, - sdk: str | None = None, do_xcom_push: bool = True, multiple_outputs: bool = False, inlets: Any | None = None, @@ -1144,7 +1142,6 @@ def __init__( self.end_date = timezone.convert_to_utc(end_date) self.executor = executor self.executor_config = executor_config or {} - self.sdk = sdk self.run_as_user = run_as_user # TODO: # self.retries = parse_retries(retries) diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 1995561064071..4af6e5542076c 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -435,8 +435,7 @@ class QueueToRuntimeCoordinatorMapper: Users often use queues as environment/isolation identifiers (e.g. ``"java-11"``, ``"java-12"``). This mapper lets them reuse existing queue assignments to route - tasks to the correct runtime coordinator without requiring the ``sdk`` field - on every operator. + tasks to the correct runtime coordinator. The mapping is read from the ``[workers] queue_to_runtime_mapping`` configuration option, which is a JSON dict of ``queue_name -> runtime_name``. diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 4128aefb591ba..3da3a09decefb 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -2098,49 +2098,72 @@ def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> Resolution order: - 1. If the task's ``sdk`` field is set, match it directly against - coordinator ``runtime_name`` values. - 2. Otherwise, consult the ``[workers] queue_to_runtime_mapping`` - configuration to see if the task's ``queue`` maps to a runtime name. + 1. **Queue mapping** -- the ``[workers] queue_to_runtime_mapping`` config maps + the task's ``queue`` to a runtime coordinator name (e.g. ``"java-queue" -> "java"``). + Used by the python-stub pattern where users set ``queue="java-queue"`` explicitly. + 2. **DAG file extension** -- if no queue mapping matches, the DAG file's extension + (e.g. ``.jar``) is compared against each coordinator's ``file_extension`` attribute. + Used by the pure-Java (or pure-) pattern where the entire DAG is authored + in a non-Python language. Returns a no-arg callable that bridges fd 0 to the runtime subprocess, or ``None`` to fall through to the standard Python execution path. """ - sdk = startup_details.ti.sdk + import functools - # Fallback: resolve runtime name from queue mapping when sdk is not set. - if sdk is None: - from airflow.sdk.execution_time.coordinator import QueueToRuntimeCoordinatorMapper + from airflow.sdk.execution_time.coordinator import QueueToRuntimeCoordinatorMapper + from airflow.sdk.providers_manager_runtime import ProvidersManagerTaskRuntime + + coordinators = ProvidersManagerTaskRuntime().runtime_coordinators + + # Step 1: queue-to-runtime mapping. + queue = startup_details.ti.queue + runtime_name = QueueToRuntimeCoordinatorMapper.from_config().resolve(queue) + if runtime_name is not None: + for coordinator_cls in coordinators: + if not hasattr(coordinator_cls, "run_task_execution"): + continue + if getattr(coordinator_cls, "runtime_name", None) != runtime_name: + continue - log.debug( - "No sdk specified for task, attempting to resolve runtime from queue mapping", - queue=startup_details.ti.queue, - task_id=startup_details.ti.task_id, - ) - sdk = QueueToRuntimeCoordinatorMapper.from_config().resolve(startup_details.ti.queue) - if sdk is None: log.debug( - "No runtime found for task queue, using standard Python execution path", - queue=startup_details.ti.queue, + "Resolved runtime-specific entrypoint for task via queue mapping", + coordinator=coordinator_cls, + runtime=runtime_name, + queue=queue, task_id=startup_details.ti.task_id, ) - return None - - import functools + return functools.partial( + coordinator_cls.run_task_execution, + what=startup_details.ti, + dag_rel_path=startup_details.dag_rel_path, + bundle_info=startup_details.bundle_info, + startup_details=startup_details, + ) - from airflow.sdk.providers_manager_runtime import ProvidersManagerTaskRuntime + log.warning( + "No runtime coordinator found for runtime", + runtime=runtime_name, + queue=queue, + task_id=startup_details.ti.task_id, + ) + return None - for coordinator_cls in ProvidersManagerTaskRuntime().runtime_coordinators: - if not hasattr(coordinator_cls, "run_task_execution"): + # Step 2: DAG file extension fallback (pure- DAGs). + dag_rel_path = startup_details.dag_rel_path + for coordinator_cls in coordinators: + # TODO: Use `can_handle_dag_file` method instead of file_extension attribute for better maintainability. + ext = getattr(coordinator_cls, "file_extension", None) + if not ext or not dag_rel_path.endswith(ext): continue - - if getattr(coordinator_cls, "runtime_name", None) != sdk: + if not hasattr(coordinator_cls, "run_task_execution"): continue log.debug( - "Resolved runtime-specific entrypoint for task", + "Resolved runtime-specific entrypoint for task via DAG file extension", coordinator=coordinator_cls, - sdk=sdk, + runtime=getattr(coordinator_cls, "runtime_name", None), + dag_rel_path=dag_rel_path, task_id=startup_details.ti.task_id, ) return functools.partial( @@ -2151,9 +2174,10 @@ def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> startup_details=startup_details, ) - log.warning( - "No runtime coordinator found for sdk", - sdk=sdk, + log.debug( + "No runtime coordinator matched, using standard Python execution path", + queue=queue, + dag_rel_path=dag_rel_path, task_id=startup_details.ti.task_id, ) return None From 87053271069b96121a6aaf98b6de7df26e574230 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 20:52:27 +0800 Subject: [PATCH 25/75] Rename `[workers] queue_to_runtime_mapping` to `[sdk] queue_to_sdk` --- airflow-core/src/airflow/config_templates/config.yml | 7 +++++-- task-sdk/src/airflow/sdk/execution_time/coordinator.py | 8 ++++---- task-sdk/src/airflow/sdk/execution_time/task_runner.py | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml index a12781aa3bef6..f9629dfd87187 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -2004,9 +2004,12 @@ workers: type: integer example: ~ default: "60" - queue_to_runtime_mapping: +sdk: + description: Settings for non-Python SDK runtime coordination + options: + queue_to_sdk: description: | - JSON mapping of queue names to runtime coordinator names. + JSON mapping of queue names to SDK runtime coordinator names. When a task's ``language`` field is not set, this mapping is checked to route the task to a non-Python runtime coordinator based on its diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 4af6e5542076c..8e54ddcf8ce4c 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -437,13 +437,13 @@ class QueueToRuntimeCoordinatorMapper: ``"java-12"``). This mapper lets them reuse existing queue assignments to route tasks to the correct runtime coordinator. - The mapping is read from the ``[workers] queue_to_runtime_mapping`` + The mapping is read from the ``[sdk] queue_to_sdk`` configuration option, which is a JSON dict of ``queue_name -> runtime_name``. Example configuration:: - [workers] - queue_to_runtime_mapping = {"java-11": "java", "java-12": "java"} + [sdk] + queue_to_sdk = {"java-11": "java", "java-12": "java"} """ def __init__(self, mapping: dict[str, str]) -> None: @@ -454,7 +454,7 @@ def from_config(cls) -> QueueToRuntimeCoordinatorMapper: """Load the queue-to-runtime mapping from airflow configuration.""" from airflow.sdk.configuration import conf - mapping = conf.getjson("workers", "queue_to_runtime_mapping", fallback={}) + mapping = conf.getjson("sdk", "queue_to_sdk", fallback={}) if not isinstance(mapping, dict): return cls({}) return cls(mapping) diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 3da3a09decefb..726d8827c7a04 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -2098,7 +2098,7 @@ def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> Resolution order: - 1. **Queue mapping** -- the ``[workers] queue_to_runtime_mapping`` config maps + 1. **Queue mapping** -- the ``[sdk] queue_to_sdk`` config maps the task's ``queue`` to a runtime coordinator name (e.g. ``"java-queue" -> "java"``). Used by the python-stub pattern where users set ``queue="java-queue"`` explicitly. 2. **DAG file extension** -- if no queue mapping matches, the DAG file's extension From b8adf3a2f7012b580cbc5ac557fadf8d7ff905d8 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Fri, 24 Apr 2026 19:32:49 +0800 Subject: [PATCH 26/75] Simplify coordinator-related names (#1569) Tweak coordinator class names, attribute names, and method names to be shorter and avoid the term 'runtime'. --- .../src/airflow/dag_processing/manager.py | 2 +- .../src/airflow/dag_processing/processor.py | 7 +-- airflow-core/src/airflow/models/dagcode.py | 2 +- .../src/airflow/provider.yaml.schema.json | 4 +- .../src/airflow/provider_info.schema.json | 4 +- airflow-core/src/airflow/providers_manager.py | 30 +++++----- .../unit/always/test_providers_manager.py | 6 +- providers/sdk/java/provider.yaml | 4 +- .../airflow/providers/sdk/java/coordinator.py | 10 ++-- .../providers/sdk/java/get_provider_info.py | 2 +- .../tests/unit/sdk/java/test_java_provider.py | 8 +-- task-sdk/.pre-commit-config.yaml | 1 + .../airflow/sdk/execution_time/coordinator.py | 51 +++++++--------- .../airflow/sdk/execution_time/task_runner.py | 19 +++--- .../airflow/sdk/providers_manager_runtime.py | 28 ++++----- .../execution_time/test_coordinator.py | 60 +++++++++---------- .../test_providers_manager_runtime.py | 6 +- 17 files changed, 119 insertions(+), 125 deletions(-) diff --git a/airflow-core/src/airflow/dag_processing/manager.py b/airflow-core/src/airflow/dag_processing/manager.py index 5234ad910a8d5..c80552bca890a 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -876,7 +876,7 @@ def _get_runtime_file_extensions(self) -> tuple[str, ...]: from airflow.providers_manager import ProvidersManager extensions: list[str] = [] - for coordinator_cls in ProvidersManager().runtime_coordinators: + for coordinator_cls in ProvidersManager().coordinators: extensions.append(coordinator_cls.file_extension) self._runtime_file_extensions = tuple(extensions) return self._runtime_file_extensions diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index 90f1caf5f5cb7..cc77833cf7a77 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -94,8 +94,6 @@ from airflow.utils.state import TaskInstanceState if TYPE_CHECKING: - from socket import socket - from structlog.typing import FilteringBoundLogger from airflow.api_fastapi.execution_api.app import InProcessExecutionAPI @@ -104,6 +102,7 @@ from airflow.sdk.definitions.context import Context from airflow.sdk.definitions.dag import DAG from airflow.sdk.definitions.mappedoperator import MappedOperator + from airflow.sdk.execution_time.supervisor import SelectorCallback from airflow.typing_compat import Self @@ -609,7 +608,7 @@ def _resolve_processor_target( """ from airflow.providers_manager import ProvidersManager - for coordinator_cls in ProvidersManager().runtime_coordinators: + for coordinator_cls in ProvidersManager().coordinators: try: log.debug( "Checking runtime coordinator %s for file %s", @@ -665,7 +664,7 @@ def _get_target_loggers(self) -> tuple[FilteringBoundLogger, ...]: def _create_log_forwarder( self, loggers: tuple[FilteringBoundLogger, ...], name: str, log_level: int = logging.INFO - ) -> Callable[[socket], bool]: + ) -> SelectorCallback: return super()._create_log_forwarder(loggers, name.replace("task.", "dag_processor.", 1), log_level) def _handle_request(self, msg: ToManager, log: FilteringBoundLogger, req_id: int) -> None: diff --git a/airflow-core/src/airflow/models/dagcode.py b/airflow-core/src/airflow/models/dagcode.py index 90b84a98404fb..528859f4cd311 100644 --- a/airflow-core/src/airflow/models/dagcode.py +++ b/airflow-core/src/airflow/models/dagcode.py @@ -122,7 +122,7 @@ def get_code_from_file(fileloc): # Try from runtime coordinator first (classes are pre-loaded by ProvidersManager) from airflow.providers_manager import ProvidersManager - for coordinator_cls in ProvidersManager().runtime_coordinators: + for coordinator_cls in ProvidersManager().coordinators: # TODO: Perhaps the `can_handle_dag_file` interface should just accept `path` only? # Or maybe we can have different granularity for this. that 1 with bundle + path, another with just path if coordinator_cls.can_handle_dag_file("", fileloc): diff --git a/airflow-core/src/airflow/provider.yaml.schema.json b/airflow-core/src/airflow/provider.yaml.schema.json index 50336f05cb945..1c41b906289cf 100644 --- a/airflow-core/src/airflow/provider.yaml.schema.json +++ b/airflow-core/src/airflow/provider.yaml.schema.json @@ -624,9 +624,9 @@ } } }, - "runtime-coordinators": { + "coordinators": { "type": "array", - "description": "Runtime Coordinator class names (BaseRuntimeCoordinator subclasses)", + "description": "Runtime Coordinator class names (BaseCoordinator subclasses)", "items": { "type": "string" } diff --git a/airflow-core/src/airflow/provider_info.schema.json b/airflow-core/src/airflow/provider_info.schema.json index 45cc92cba276f..92601fc58af74 100644 --- a/airflow-core/src/airflow/provider_info.schema.json +++ b/airflow-core/src/airflow/provider_info.schema.json @@ -447,9 +447,9 @@ } } }, - "runtime-coordinators": { + "coordinators": { "type": "array", - "description": "Runtime Coordinator class names (BaseRuntimeCoordinator subclasses)", + "description": "Runtime Coordinator class names (BaseCoordinator subclasses)", "items": { "type": "string" } diff --git a/airflow-core/src/airflow/providers_manager.py b/airflow-core/src/airflow/providers_manager.py index 123fe502273fc..8945589b4b046 100644 --- a/airflow-core/src/airflow/providers_manager.py +++ b/airflow-core/src/airflow/providers_manager.py @@ -41,7 +41,7 @@ if TYPE_CHECKING: from airflow.cli.cli_config import CLICommand - from airflow.sdk.execution_time.coordinator import BaseRuntimeCoordinator + from airflow.sdk.execution_time.coordinator import BaseCoordinator log = logging.getLogger(__name__) @@ -449,7 +449,7 @@ def __init__(self): ) # Set of plugins contained in providers self._plugins_set: set[PluginInfo] = set() - self._runtime_coordinators: list[type[BaseRuntimeCoordinator]] = [] + self._coordinators: list[type[BaseCoordinator]] = [] self._init_airflow_core_hooks() self._runtime_manager = None @@ -627,11 +627,11 @@ def initialize_providers_configuration(self): self.initialize_providers_list() self._discover_config() - @provider_info_cache("runtime_coordinators") - def initialize_providers_runtime_coordinators(self): + @provider_info_cache("coordinators") + def initialize_providers_coordinators(self): """Lazy initialization of providers runtime coordinators.""" self.initialize_providers_list() - self._discover_runtime_coordinators() + self._discover_coordinators() @provider_info_cache("plugins") def initialize_providers_plugins(self): @@ -1288,18 +1288,18 @@ def _discover_config(self) -> None: if provider.data.get("config"): self._provider_configs[provider_package] = provider.data.get("config") # type: ignore[assignment] - def _discover_runtime_coordinators(self) -> None: - """Retrieve and pre-load all runtime coordinators defined in the providers.""" + def _discover_coordinators(self) -> None: + """Retrieve and pre-load all coordinators defined in the providers.""" seen: set[str] = set() for provider_package, provider in self._provider_dict.items(): - for coordinator_class_path in provider.data.get("runtime-coordinators", []): + for coordinator_class_path in provider.data.get("coordinators", []): if coordinator_class_path in seen: continue coordinator_cls = _correctness_check(provider_package, coordinator_class_path, provider) if coordinator_cls: seen.add(coordinator_class_path) - self._runtime_coordinators.append(coordinator_cls) - self._runtime_coordinators = sorted(self._runtime_coordinators, key=lambda c: c.__qualname__) + self._coordinators.append(coordinator_cls) + self._coordinators = sorted(self._coordinators, key=lambda c: c.__qualname__) def _discover_plugins(self) -> None: """Retrieve all plugins defined in the providers.""" @@ -1499,10 +1499,10 @@ def db_managers(self) -> list[str]: return sorted(self._db_manager_class_name_set) @property - def runtime_coordinators(self) -> list[type[BaseRuntimeCoordinator]]: - """Returns pre-loaded runtime coordinator classes available in providers.""" - self.initialize_providers_runtime_coordinators() - return self._runtime_coordinators + def coordinators(self) -> list[type[BaseCoordinator]]: + """Returns pre-loaded coordinator classes available in providers.""" + self.initialize_providers_coordinators() + return self._coordinators @property def filesystem_module_names(self) -> list[str]: @@ -1575,7 +1575,7 @@ def _cleanup(self): self._trigger_info_set.clear() self._notification_info_set.clear() self._plugins_set.clear() - self._runtime_coordinators.clear() + self._coordinators.clear() self._cli_command_functions_set.clear() self._cli_command_provider_name_set.clear() diff --git a/airflow-core/tests/unit/always/test_providers_manager.py b/airflow-core/tests/unit/always/test_providers_manager.py index 9d69ce3ebc24b..b13930c98d1c2 100644 --- a/airflow-core/tests/unit/always/test_providers_manager.py +++ b/airflow-core/tests/unit/always/test_providers_manager.py @@ -259,7 +259,7 @@ def test_dialects(self): assert dialect_class_names == ["default", "mssql", "postgresql"] @patch("airflow.providers_manager.import_string") - def test_runtime_coordinators(self, mock_import_string): + def test_coordinators(self, mock_import_string): class ACoordinator: pass @@ -275,7 +275,7 @@ class ZCoordinator: providers_manager._provider_dict["apache-airflow-providers-sdk-java"] = ProviderInfo( version="0.0.1", data={ - "runtime-coordinators": [ + "coordinators": [ "airflow.providers.sdk.java.coordinator.ZCoordinator", "airflow.providers.sdk.java.coordinator.ACoordinator", "airflow.providers.sdk.java.coordinator.ZCoordinator", @@ -284,7 +284,7 @@ class ZCoordinator: ) with patch.object(providers_manager, "initialize_providers_list"): - assert providers_manager.runtime_coordinators == [ACoordinator, ZCoordinator] + assert providers_manager.coordinators == [ACoordinator, ZCoordinator] class TestWithoutCheckProviderManager: diff --git a/providers/sdk/java/provider.yaml b/providers/sdk/java/provider.yaml index c93b45a4da6aa..d10f841962034 100644 --- a/providers/sdk/java/provider.yaml +++ b/providers/sdk/java/provider.yaml @@ -54,5 +54,5 @@ config: example: ~/airflow/java-bundles default: "" -runtime-coordinators: - - airflow.providers.sdk.java.coordinator.JavaRuntimeCoordinator +coordinators: + - airflow.providers.sdk.java.coordinator.JavaCoordinator diff --git a/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py b/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py index e23740549104a..a4d32020261b9 100644 --- a/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py +++ b/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py @@ -26,16 +26,16 @@ from typing import TYPE_CHECKING from airflow.providers.sdk.java.bundle_scanner import BundleScanner, read_dag_code -from airflow.sdk.execution_time.coordinator import BaseRuntimeCoordinator +from airflow.sdk.execution_time.coordinator import BaseCoordinator if TYPE_CHECKING: from airflow.sdk.api.datamodels._generated import BundleInfo, TaskInstance -class JavaRuntimeCoordinator(BaseRuntimeCoordinator): +class JavaCoordinator(BaseCoordinator): """Coordinator that launches a JVM subprocess for DAG parsing and task execution.""" - runtime_name = "java" + sdk = "java" file_extension = ".jar" @classmethod @@ -54,7 +54,7 @@ def get_code_from_file(cls, fileloc: str) -> str: return code @classmethod - def dag_parsing_runtime_cmd( + def dag_parsing_cmd( cls, *, dag_file_path: str, @@ -80,7 +80,7 @@ def dag_parsing_runtime_cmd( ] @classmethod - def task_execution_runtime_cmd( + def task_execution_cmd( cls, *, what: TaskInstance, diff --git a/providers/sdk/java/src/airflow/providers/sdk/java/get_provider_info.py b/providers/sdk/java/src/airflow/providers/sdk/java/get_provider_info.py index 48fd8238c8a5f..89df45102b732 100644 --- a/providers/sdk/java/src/airflow/providers/sdk/java/get_provider_info.py +++ b/providers/sdk/java/src/airflow/providers/sdk/java/get_provider_info.py @@ -43,5 +43,5 @@ def get_provider_info(): }, } }, - "runtime-coordinators": ["airflow.providers.sdk.java.coordinator.JavaRuntimeCoordinator"], + "coordinators": ["airflow.providers.sdk.java.coordinator.JavaCoordinator"], } diff --git a/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py b/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py index 94c7c1794197e..fe2a61ef18ae3 100644 --- a/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py +++ b/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py @@ -17,7 +17,7 @@ # under the License. from __future__ import annotations -from airflow.providers.sdk.java.coordinator import JavaRuntimeCoordinator +from airflow.providers.sdk.java.coordinator import JavaCoordinator from airflow.providers.sdk.java.get_provider_info import get_provider_info @@ -33,11 +33,11 @@ def test_get_provider_info_exposes_java_runtime_components(): "tags": ["software"], } ], - "runtime-coordinators": [ - "airflow.providers.sdk.java.coordinator.JavaRuntimeCoordinator", + "coordinators": [ + "airflow.providers.sdk.java.coordinator.JavaCoordinator", ], } def test_java_provider_entrypoints_are_importable(): - assert JavaRuntimeCoordinator.runtime_name == "java" + assert JavaCoordinator.sdk == "java" diff --git a/task-sdk/.pre-commit-config.yaml b/task-sdk/.pre-commit-config.yaml index f9a224f60d483..470a52cb380db 100644 --- a/task-sdk/.pre-commit-config.yaml +++ b/task-sdk/.pre-commit-config.yaml @@ -44,6 +44,7 @@ repos: ^src/airflow/sdk/definitions/dag\.py$| ^src/airflow/sdk/definitions/_internal/types\.py$| ^src/airflow/sdk/execution_time/schema/__init__\.py$| + ^src/airflow/sdk/execution_time/coordinator\.py$| ^src/airflow/sdk/execution_time/execute_workload\.py$| ^src/airflow/sdk/execution_time/secrets_masker\.py$| ^src/airflow/sdk/execution_time/callback_supervisor\.py$| diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 8e54ddcf8ce4c..c3d0594bebe14 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -18,15 +18,15 @@ """ Runtime coordinator for non-Python DAG file processing and task execution. -Provides :class:`BaseRuntimeCoordinator`, the base class for +Provides :class:`BaseCoordinator`, the base class for SDK-specific coordinators that bridge subprocess I/O between the Airflow supervisor and an external-SDK runtime (Java, Go, Rust, etc.). -The coordinator's :meth:`~BaseRuntimeCoordinator.run_dag_parsing` method +The coordinator's :meth:`~BaseCoordinator.run_dag_parsing` method handles the full lifecycle: 1. Creates TCP servers for comm and logs channels. -2. Calls :meth:`~BaseRuntimeCoordinator.dag_parsing_runtime_cmd` (provided +2. Calls :meth:`~BaseCoordinator.dag_parsing_cmd` (provided by the subclass) to obtain the subprocess command. 3. Spawns the subprocess and accepts TCP connections from it. 4. Runs a selector-based bridge that transparently forwards bytes @@ -50,6 +50,7 @@ if TYPE_CHECKING: from structlog.typing import FilteringBoundLogger + from typing_extensions import Self from airflow.sdk._shared.workloads import TaskInstanceDTO from airflow.sdk.api.datamodels._generated import BundleInfo @@ -166,23 +167,23 @@ def on_close(sock: socket.socket) -> None: sock.close() -class BaseRuntimeCoordinator: +class BaseCoordinator: """ Base coordinator for runtime-specific DAG file processing and task execution. Providers register subclasses in their ``provider.yaml`` under - ``runtime-coordinators``. Both :class:`ProvidersManager` (airflow-core) + ``coordinators``. Both :class:`ProvidersManager` (airflow-core) and :class:`ProvidersManagerTaskRuntime` (task-sdk) discover registered coordinators through this single extension point. Subclasses represent a specific SDK runtime (Java, Go, etc.) and only need to implement :meth:`can_handle_dag_file`, - :meth:`dag_parsing_runtime_cmd` and :meth:`task_execution_runtime_cmd`. + :meth:`dag_parsing_cmd` and :meth:`task_execution_cmd`. The base class owns the entire bridge lifecycle: TCP servers, subprocess management, selector-based I/O loop, and cleanup. """ - runtime_name: str + sdk: str file_extension: str class DagParsingInfo(NamedTuple): @@ -233,7 +234,7 @@ def get_code_from_file(cls, fileloc: str) -> str: raise NotImplementedError @classmethod - def dag_parsing_runtime_cmd( + def dag_parsing_cmd( cls, *, dag_file_path: str, @@ -257,7 +258,7 @@ def dag_parsing_runtime_cmd( raise NotImplementedError @classmethod - def task_execution_runtime_cmd( + def task_execution_cmd( cls, *, what: TaskInstanceDTO, @@ -320,7 +321,7 @@ def _runtime_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskEx bidirectional comms socket to the supervisor. The method: 1. Creates TCP servers for comm and logs. - 2. Calls :meth:`dag_parsing_runtime_cmd` or :meth:`task_execution_runtime_cmd` to get the command. + 2. Calls :meth:`dag_parsing_cmd` or :meth:`task_execution_cmd` to get the command. 3. Spawns the subprocess with ``stdin=/dev/null`` and stderr captured via a socketpair. 4. Runs the selector-based bridge until the subprocess exits. @@ -342,7 +343,7 @@ def _runtime_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskEx log = structlog.get_logger(logger_name="task") log.info( "Starting runtime subprocess", - runtime=cls.runtime_name, + sdk=cls.sdk, mode=entrypoint_info.mode, ) @@ -365,7 +366,7 @@ def _runtime_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskEx bundle_version_lock: contextlib.AbstractContextManager = contextlib.nullcontext() if isinstance(entrypoint_info, cls.DagParsingInfo): - cmd = cls.dag_parsing_runtime_cmd( + cmd = cls.dag_parsing_cmd( dag_file_path=entrypoint_info.dag_file_path, bundle_name=entrypoint_info.bundle_name, bundle_path=entrypoint_info.bundle_path, @@ -373,22 +374,16 @@ def _runtime_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskEx logs_addr=logs_addr, ) elif isinstance(entrypoint_info, cls.TaskExecutionInfo): - from pathlib import Path - - # import from core now will raise static check error from `check-core-imports` check - # We should support ignore label for the above static check - # directly commit for now from airflow.dag_processing.bundles.base import BundleVersionLock from airflow.sdk.execution_time.task_runner import resolve_bundle bundle_instance = resolve_bundle(entrypoint_info.bundle_info, log) - resolved_bundle_path = str(bundle_instance.path) - resolved_dag_file_path = os.fspath(Path(bundle_instance.path, entrypoint_info.dag_rel_path)) + resolved_dag_file_path = bundle_instance.path / entrypoint_info.dag_rel_path - cmd = cls.task_execution_runtime_cmd( + cmd = cls.task_execution_cmd( what=entrypoint_info.what, - dag_file_path=resolved_dag_file_path, - bundle_path=resolved_bundle_path, + dag_file_path=os.fspath(resolved_dag_file_path), + bundle_path=os.fspath(bundle_instance.path), bundle_info=entrypoint_info.bundle_info, comm_addr=comm_addr, logs_addr=logs_addr, @@ -429,16 +424,16 @@ def _runtime_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskEx _bridge(supervisor_comm, runtime_comm, runtime_logs, read_stderr, proc, log) -class QueueToRuntimeCoordinatorMapper: +class QueueToCoordinatorMapper: """ - Map queue names to runtime coordinator names. + Map queue names to coordinator names. Users often use queues as environment/isolation identifiers (e.g. ``"java-11"``, ``"java-12"``). This mapper lets them reuse existing queue assignments to route - tasks to the correct runtime coordinator. + tasks to the correct coordinator. The mapping is read from the ``[sdk] queue_to_sdk`` - configuration option, which is a JSON dict of ``queue_name -> runtime_name``. + configuration option, which is a JSON dict of ``queue -> sdk``. Example configuration:: @@ -450,7 +445,7 @@ def __init__(self, mapping: dict[str, str]) -> None: self._mapping = mapping @classmethod - def from_config(cls) -> QueueToRuntimeCoordinatorMapper: + def from_config(cls) -> Self: """Load the queue-to-runtime mapping from airflow configuration.""" from airflow.sdk.configuration import conf @@ -464,4 +459,4 @@ def resolve(self, queue: str) -> str | None: return self._mapping.get(queue) -__all__ = ["BaseRuntimeCoordinator", "QueueToRuntimeCoordinatorMapper"] +__all__ = ["BaseCoordinator", "QueueToCoordinatorMapper"] diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 726d8827c7a04..35e326dca68b4 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -2111,25 +2111,24 @@ def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> """ import functools - from airflow.sdk.execution_time.coordinator import QueueToRuntimeCoordinatorMapper + from airflow.sdk.execution_time.coordinator import QueueToCoordinatorMapper from airflow.sdk.providers_manager_runtime import ProvidersManagerTaskRuntime - coordinators = ProvidersManagerTaskRuntime().runtime_coordinators + coordinators = ProvidersManagerTaskRuntime().coordinators # Step 1: queue-to-runtime mapping. queue = startup_details.ti.queue - runtime_name = QueueToRuntimeCoordinatorMapper.from_config().resolve(queue) - if runtime_name is not None: + if (sdk := QueueToCoordinatorMapper.from_config().resolve(queue)) is not None: for coordinator_cls in coordinators: if not hasattr(coordinator_cls, "run_task_execution"): continue - if getattr(coordinator_cls, "runtime_name", None) != runtime_name: + if getattr(coordinator_cls, "sdk", None) != sdk: continue log.debug( - "Resolved runtime-specific entrypoint for task via queue mapping", + "Resolved sdk-specific entrypoint for task via queue mapping", coordinator=coordinator_cls, - runtime=runtime_name, + sdk=sdk, queue=queue, task_id=startup_details.ti.task_id, ) @@ -2142,8 +2141,8 @@ def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> ) log.warning( - "No runtime coordinator found for runtime", - runtime=runtime_name, + "No coordinator found for sdk", + sdk=sdk, queue=queue, task_id=startup_details.ti.task_id, ) @@ -2162,7 +2161,7 @@ def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> log.debug( "Resolved runtime-specific entrypoint for task via DAG file extension", coordinator=coordinator_cls, - runtime=getattr(coordinator_cls, "runtime_name", None), + sdk=getattr(coordinator_cls, "sdk", None), dag_rel_path=dag_rel_path, task_id=startup_details.ti.task_id, ) diff --git a/task-sdk/src/airflow/sdk/providers_manager_runtime.py b/task-sdk/src/airflow/sdk/providers_manager_runtime.py index b15c477f40779..63c8c97f816ef 100644 --- a/task-sdk/src/airflow/sdk/providers_manager_runtime.py +++ b/task-sdk/src/airflow/sdk/providers_manager_runtime.py @@ -51,7 +51,7 @@ from airflow.sdk import BaseHook from airflow.sdk.bases.decorator import TaskDecorator from airflow.sdk.definitions.asset import Asset - from airflow.sdk.execution_time.coordinator import BaseRuntimeCoordinator + from airflow.sdk.execution_time.coordinator import BaseCoordinator log = structlog.getLogger(__name__) @@ -151,7 +151,7 @@ def __init__(self): # Keeps dict of hooks keyed by connection type. They are lazy evaluated at access time self._hooks_lazy_dict: LazyDictWithCache[str, HookInfo | Callable] = LazyDictWithCache() self._plugins_set: set[PluginInfo] = set() - self._runtime_coordinators: list[type[BaseRuntimeCoordinator]] = [] + self._coordinators: list[type[BaseCoordinator]] = [] self._provider_schema_validator = _create_provider_info_schema_validator() self._init_airflow_core_hooks() # Populated by initialize_provider_configs(); holds provider-contributed config sections. @@ -222,11 +222,11 @@ def initialize_providers_taskflow_decorator(self): self.initialize_providers_list() self._discover_taskflow_decorators() - @provider_info_cache("runtime_coordinators") - def initialize_providers_runtime_coordinators(self): + @provider_info_cache("coordinators") + def initialize_providers_coordinators(self): """Lazy initialization of providers runtime coordinators.""" self.initialize_providers_list() - self._discover_runtime_coordinators() + self._discover_coordinators() @provider_info_cache("provider_configs") def initialize_provider_configs(self): @@ -472,18 +472,18 @@ def _import_hook( connection_testable=hasattr(hook_class, "test_connection"), ) - def _discover_runtime_coordinators(self) -> None: - """Retrieve and pre-load all runtime coordinators defined in the providers.""" + def _discover_coordinators(self) -> None: + """Retrieve and pre-load all coordinators defined in the providers.""" seen: set[str] = set() for provider_package, provider in self._provider_dict.items(): - for coordinator_class_path in provider.data.get("runtime-coordinators", []): + for coordinator_class_path in provider.data.get("coordinators", []): if coordinator_class_path in seen: continue coordinator_cls = _correctness_check(provider_package, coordinator_class_path, provider) if coordinator_cls: seen.add(coordinator_class_path) - self._runtime_coordinators.append(coordinator_cls) - self._runtime_coordinators = sorted(self._runtime_coordinators, key=lambda c: c.__qualname__) + self._coordinators.append(coordinator_cls) + self._coordinators = sorted(self._coordinators, key=lambda c: c.__qualname__) def _discover_filesystems(self) -> None: """Retrieve all filesystems defined in the providers.""" @@ -633,10 +633,10 @@ def plugins(self) -> list[PluginInfo]: return sorted(self._plugins_set, key=lambda x: x.plugin_class) @property - def runtime_coordinators(self) -> list[type[BaseRuntimeCoordinator]]: + def coordinators(self) -> list[type[BaseCoordinator]]: """Returns pre-loaded runtime coordinator classes available in providers.""" - self.initialize_providers_runtime_coordinators() - return self._runtime_coordinators + self.initialize_providers_coordinators() + return self._coordinators @property def provider_configs(self) -> list[tuple[str, dict[str, Any]]]: @@ -670,7 +670,7 @@ def _cleanup(self): self._asset_uri_handlers.clear() self._asset_factories.clear() self._asset_to_openlineage_converters.clear() - self._runtime_coordinators.clear() + self._coordinators.clear() self._provider_configs.clear() # Imported lazily to preserve SDK conf lazy initialization and avoid a configuration/runtime cycle. diff --git a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py index a069e33cd4536..783b46afad23e 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py +++ b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py @@ -25,7 +25,7 @@ import pytest from airflow.sdk.execution_time.coordinator import ( - BaseRuntimeCoordinator, + BaseCoordinator, _bridge, _send_startup_details, _start_server, @@ -156,17 +156,17 @@ def test_real_socket_roundtrip(self): server.close() -class TestBaseRuntimeCoordinatorDefaults: +class TestBaseCoordinatorDefaults: def test_can_handle_dag_file_returns_false(self): - assert BaseRuntimeCoordinator.can_handle_dag_file("bundle", "/path/to/dag.py") is False + assert BaseCoordinator.can_handle_dag_file("bundle", "/path/to/dag.py") is False def test_get_code_from_file_raises_not_implemented(self): with pytest.raises(NotImplementedError): - BaseRuntimeCoordinator.get_code_from_file("/path/to/dag.jar") + BaseCoordinator.get_code_from_file("/path/to/dag.jar") - def test_dag_parsing_runtime_cmd_raises_not_implemented(self): + def test_dag_parsing_cmd_raises_not_implemented(self): with pytest.raises(NotImplementedError): - BaseRuntimeCoordinator.dag_parsing_runtime_cmd( + BaseCoordinator.dag_parsing_cmd( dag_file_path="/dag.jar", bundle_name="b", bundle_path="/path", @@ -174,9 +174,9 @@ def test_dag_parsing_runtime_cmd_raises_not_implemented(self): logs_addr="127.0.0.1:1235", ) - def test_task_execution_runtime_cmd_raises_not_implemented(self): + def test_task_execution_cmd_raises_not_implemented(self): with pytest.raises(NotImplementedError): - BaseRuntimeCoordinator.task_execution_runtime_cmd( + BaseCoordinator.task_execution_cmd( what=MagicMock(), dag_file_path="/dag.jar", bundle_path="/path", @@ -188,7 +188,7 @@ def test_task_execution_runtime_cmd_raises_not_implemented(self): class TestCoordinatorNamedTuples: def test_dag_parsing_info_defaults(self): - info = BaseRuntimeCoordinator.DagParsingInfo( + info = BaseCoordinator.DagParsingInfo( dag_file_path="/dag.jar", bundle_name="my-bundle", bundle_path="/bundles/my-bundle", @@ -202,7 +202,7 @@ def test_task_execution_info_defaults(self): mock_ti = MagicMock() mock_bundle = MagicMock() mock_startup = MagicMock() - info = BaseRuntimeCoordinator.TaskExecutionInfo( + info = BaseCoordinator.TaskExecutionInfo( what=mock_ti, dag_rel_path="dags/example.jar", bundle_info=mock_bundle, @@ -319,9 +319,9 @@ def test_bridge_closes_all_sockets(self): class TestRunDagParsing: - @patch.object(BaseRuntimeCoordinator, "_runtime_subprocess_entrypoint") + @patch.object(BaseCoordinator, "_runtime_subprocess_entrypoint") def test_run_dag_parsing_creates_dag_parsing_info(self, mock_entrypoint): - BaseRuntimeCoordinator.run_dag_parsing( + BaseCoordinator.run_dag_parsing( path="/bundles/my-bundle/dags/example.jar", bundle_name="my-bundle", bundle_path="/bundles/my-bundle", @@ -329,7 +329,7 @@ def test_run_dag_parsing_creates_dag_parsing_info(self, mock_entrypoint): mock_entrypoint.assert_called_once() info = mock_entrypoint.call_args[0][0] - assert isinstance(info, BaseRuntimeCoordinator.DagParsingInfo) + assert isinstance(info, BaseCoordinator.DagParsingInfo) assert info.dag_file_path == "/bundles/my-bundle/dags/example.jar" assert info.bundle_name == "my-bundle" assert info.bundle_path == "/bundles/my-bundle" @@ -337,13 +337,13 @@ def test_run_dag_parsing_creates_dag_parsing_info(self, mock_entrypoint): class TestRunTaskExecution: - @patch.object(BaseRuntimeCoordinator, "_runtime_subprocess_entrypoint") + @patch.object(BaseCoordinator, "_runtime_subprocess_entrypoint") def test_run_task_execution_creates_task_execution_info(self, mock_entrypoint): mock_ti = MagicMock() mock_bundle_info = MagicMock() mock_startup = MagicMock() - BaseRuntimeCoordinator.run_task_execution( + BaseCoordinator.run_task_execution( what=mock_ti, dag_rel_path="dags/example.jar", bundle_info=mock_bundle_info, @@ -352,7 +352,7 @@ def test_run_task_execution_creates_task_execution_info(self, mock_entrypoint): mock_entrypoint.assert_called_once() info = mock_entrypoint.call_args[0][0] - assert isinstance(info, BaseRuntimeCoordinator.TaskExecutionInfo) + assert isinstance(info, BaseCoordinator.TaskExecutionInfo) assert info.what is mock_ti assert info.dag_rel_path == "dags/example.jar" assert info.bundle_info is mock_bundle_info @@ -362,8 +362,8 @@ def test_run_task_execution_creates_task_execution_info(self, mock_entrypoint): class TestRuntimeSubprocessEntrypoint: def test_unknown_entrypoint_info_type_raises(self): - class TestCoordinator(BaseRuntimeCoordinator): - runtime_name = "test" + class TestCoordinator(BaseCoordinator): + sdk = "test" file_extension = ".test" # Needs a 'mode' attribute (accessed during logging) but must not be @@ -402,15 +402,15 @@ def test_dag_parsing_flow(self, mock_dup, mock_start_server, mock_popen, mock_se # Mock supervisor_comm created from os.dup(0) supervisor_comm = MagicMock(spec=socket.socket) - class TestCoordinator(BaseRuntimeCoordinator): - runtime_name = "test" + class TestCoordinator(BaseCoordinator): + sdk = "test" file_extension = ".test" @classmethod - def dag_parsing_runtime_cmd(cls, **kwargs): + def dag_parsing_cmd(cls, **kwargs): return ["test-runtime", "--parse", kwargs["dag_file_path"]] - info = BaseRuntimeCoordinator.DagParsingInfo( + info = BaseCoordinator.DagParsingInfo( dag_file_path="/dag.test", bundle_name="test-bundle", bundle_path="/bundles/test-bundle", @@ -494,15 +494,15 @@ def test_task_execution_flow( mock_bundle_info.version = "v1" mock_startup = MagicMock() - class TestCoordinator(BaseRuntimeCoordinator): - runtime_name = "test" + class TestCoordinator(BaseCoordinator): + sdk = "test" file_extension = ".test" @classmethod - def task_execution_runtime_cmd(cls, **kwargs): + def task_execution_cmd(cls, **kwargs): return ["test-runtime", "--execute", kwargs["dag_file_path"]] - info = BaseRuntimeCoordinator.TaskExecutionInfo( + info = BaseCoordinator.TaskExecutionInfo( what=mock_ti, dag_rel_path="dags/example.test", bundle_info=mock_bundle_info, @@ -555,15 +555,15 @@ def test_sets_process_context_env_var(self, mock_dup, mock_start_server, mock_po read_stderr = MagicMock(spec=socket.socket) child_stderr.fileno.return_value = 10 - class TestCoordinator(BaseRuntimeCoordinator): - runtime_name = "test" + class TestCoordinator(BaseCoordinator): + sdk = "test" file_extension = ".test" @classmethod - def dag_parsing_runtime_cmd(cls, **kwargs): + def dag_parsing_cmd(cls, **kwargs): return ["echo", "test"] - info = BaseRuntimeCoordinator.DagParsingInfo( + info = BaseCoordinator.DagParsingInfo( dag_file_path="/dag.test", bundle_name="b", bundle_path="/path", diff --git a/task-sdk/tests/task_sdk/test_providers_manager_runtime.py b/task-sdk/tests/task_sdk/test_providers_manager_runtime.py index edea4fd35ede0..6e775f790be89 100644 --- a/task-sdk/tests/task_sdk/test_providers_manager_runtime.py +++ b/task-sdk/tests/task_sdk/test_providers_manager_runtime.py @@ -244,7 +244,7 @@ def test_already_initialized_provider_configs_emits_deprecation_warning(self): pm.already_initialized_provider_configs @patch("airflow.sdk.providers_manager_runtime.import_string") - def test_runtime_coordinators(self, mock_import_string): + def test_coordinators(self, mock_import_string): class ACoordinator: pass @@ -259,7 +259,7 @@ class ZCoordinator: providers_manager._provider_dict["apache-airflow-providers-sdk-java"] = ProviderInfo( version="0.0.1", data={ - "runtime-coordinators": [ + "coordinators": [ "airflow.providers.sdk.java.coordinator.ZCoordinator", "airflow.providers.sdk.java.coordinator.ACoordinator", "airflow.providers.sdk.java.coordinator.ZCoordinator", @@ -268,7 +268,7 @@ class ZCoordinator: ) with patch.object(providers_manager, "initialize_providers_list"): - assert providers_manager.runtime_coordinators == [ACoordinator, ZCoordinator] + assert providers_manager.coordinators == [ACoordinator, ZCoordinator] def test_initialize_provider_configs_can_reload_sdk_conf(self): from airflow.sdk.configuration import conf From 9055d008f5060597593c9d2934354dba07302ad6 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 15:09:55 +0800 Subject: [PATCH 27/75] CI: Add mypy and unit tests for shared/workloads --- shared/workloads/.pre-commit-config.yaml | 31 ++++++++ shared/workloads/tests/conftest.py | 22 ++++++ shared/workloads/tests/workloads/__init__.py | 17 +++++ .../tests/workloads/test_task_instance_dto.py | 56 ++++++++++++++ uv.lock | 73 +------------------ 5 files changed, 127 insertions(+), 72 deletions(-) create mode 100644 shared/workloads/.pre-commit-config.yaml create mode 100644 shared/workloads/tests/conftest.py create mode 100644 shared/workloads/tests/workloads/__init__.py create mode 100644 shared/workloads/tests/workloads/test_task_instance_dto.py diff --git a/shared/workloads/.pre-commit-config.yaml b/shared/workloads/.pre-commit-config.yaml new file mode 100644 index 0000000000000..7ce635750ca3c --- /dev/null +++ b/shared/workloads/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +# 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. +--- +default_stages: [pre-commit, pre-push] +minimum_prek_version: '0.3.4' +default_language_version: + python: python3 +repos: + - repo: local + hooks: + - id: mypy-shared-workloads + name: Run mypy for shared-workloads + language: python + entry: ../../scripts/ci/prek/run_mypy_full_dist_local_venv_or_breeze_in_ci.py shared/workloads + pass_filenames: false + files: ^.*\.py$ + require_serial: true diff --git a/shared/workloads/tests/conftest.py b/shared/workloads/tests/conftest.py new file mode 100644 index 0000000000000..8b61b1b99b90d --- /dev/null +++ b/shared/workloads/tests/conftest.py @@ -0,0 +1,22 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import os + +os.environ["_AIRFLOW__AS_LIBRARY"] = "true" diff --git a/shared/workloads/tests/workloads/__init__.py b/shared/workloads/tests/workloads/__init__.py new file mode 100644 index 0000000000000..217e5db960782 --- /dev/null +++ b/shared/workloads/tests/workloads/__init__.py @@ -0,0 +1,17 @@ +# +# 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. diff --git a/shared/workloads/tests/workloads/test_task_instance_dto.py b/shared/workloads/tests/workloads/test_task_instance_dto.py new file mode 100644 index 0000000000000..4788386dd6861 --- /dev/null +++ b/shared/workloads/tests/workloads/test_task_instance_dto.py @@ -0,0 +1,56 @@ +# +# 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. +"""Tests for shared TaskInstanceDTO.""" + +from __future__ import annotations + +import uuid + +import pytest + +from airflow_shared.workloads import TaskInstanceDTO + + +@pytest.fixture +def minimal_ti_kwargs(): + return { + "id": uuid.uuid4(), + "dag_version_id": uuid.uuid4(), + "task_id": "my_task", + "dag_id": "my_dag", + "run_id": "run_1", + "try_number": 1, + "pool_slots": 1, + "queue": "default", + "priority_weight": 1, + } + + +class TestTaskInstanceDTO: + """Test the shared TaskInstanceDTO model.""" + + def test_create_with_required_fields(self, minimal_ti_kwargs): + ti = TaskInstanceDTO(**minimal_ti_kwargs) + assert ti is not None + assert ti.task_id == "my_task" + assert ti.dag_id == "my_dag" + assert ti.run_id == "run_1" + assert ti.try_number == 1 + assert ti.pool_slots == 1 + assert ti.queue == "default" + assert ti.priority_weight == 1 diff --git a/uv.lock b/uv.lock index 4baf294c7c047..bca09add09083 100644 --- a/uv.lock +++ b/uv.lock @@ -80,8 +80,8 @@ apache-airflow-providers-salesforce = false apache-airflow-providers-ssh = false apache-airflow-providers-papermill = false apache-airflow-providers-google = false -apache-airflow-providers-vertica = false apache-airflow-providers-microsoft-psrp = false +apache-airflow-providers-vertica = false apache-airflow-providers-apache-hdfs = false apache-airflow-shared-template-rendering = false apache-airflow-mypy = false @@ -98,7 +98,6 @@ apache-airflow-shared-serialization = false apache-airflow-scripts = false apache-airflow-providers-exasol = false apache-airflow-providers-mongo = false -apache-airflow-shared-workloads = false apache-airflow-providers-apprise = false apache-airflow-providers-apache-impala = false apache-airflow-ctl = false @@ -153,7 +152,6 @@ apache-airflow-providers-smtp = false apache-airflow-providers-dingding = false apache-airflow-providers-apache-kylin = false apache-airflow-providers-cloudant = false -apache-airflow-providers-sdk-java = false apache-aurflow-docker-stack = false [manifest] @@ -251,7 +249,6 @@ members = [ "apache-airflow-providers-redis", "apache-airflow-providers-salesforce", "apache-airflow-providers-samba", - "apache-airflow-providers-sdk-java", "apache-airflow-providers-segment", "apache-airflow-providers-sendgrid", "apache-airflow-providers-sftp", @@ -288,7 +285,6 @@ members = [ "apache-airflow-shared-state", "apache-airflow-shared-template-rendering", "apache-airflow-shared-timezones", - "apache-airflow-shared-workloads", "apache-airflow-task-sdk", "apache-airflow-task-sdk-integration-tests", "docker-stack", @@ -1068,7 +1064,6 @@ all = [ { name = "apache-airflow-providers-redis" }, { name = "apache-airflow-providers-salesforce" }, { name = "apache-airflow-providers-samba" }, - { name = "apache-airflow-providers-sdk-java" }, { name = "apache-airflow-providers-segment" }, { name = "apache-airflow-providers-sendgrid" }, { name = "apache-airflow-providers-sftp" }, @@ -1388,9 +1383,6 @@ salesforce = [ samba = [ { name = "apache-airflow-providers-samba" }, ] -sdk-java = [ - { name = "apache-airflow-providers-sdk-java" }, -] segment = [ { name = "apache-airflow-providers-segment" }, ] @@ -1516,7 +1508,6 @@ dev = [ { name = "apache-airflow-shared-state" }, { name = "apache-airflow-shared-template-rendering" }, { name = "apache-airflow-shared-timezones" }, - { name = "apache-airflow-shared-workloads" }, { name = "apache-airflow-task-sdk", extra = ["all"] }, { name = "apache-airflow-task-sdk-integration-tests" }, ] @@ -1712,8 +1703,6 @@ requires-dist = [ { name = "apache-airflow-providers-salesforce", marker = "extra == 'salesforce'", editable = "providers/salesforce" }, { name = "apache-airflow-providers-samba", marker = "extra == 'all'", editable = "providers/samba" }, { name = "apache-airflow-providers-samba", marker = "extra == 'samba'", editable = "providers/samba" }, - { name = "apache-airflow-providers-sdk-java", marker = "extra == 'all'", editable = "providers/sdk/java" }, - { name = "apache-airflow-providers-sdk-java", marker = "extra == 'sdk-java'", editable = "providers/sdk/java" }, { name = "apache-airflow-providers-segment", marker = "extra == 'all'", editable = "providers/segment" }, { name = "apache-airflow-providers-segment", marker = "extra == 'segment'", editable = "providers/segment" }, { name = "apache-airflow-providers-sendgrid", marker = "extra == 'all'", editable = "providers/sendgrid" }, @@ -1823,7 +1812,6 @@ dev = [ { name = "apache-airflow-shared-state", editable = "shared/state" }, { name = "apache-airflow-shared-template-rendering", editable = "shared/template_rendering" }, { name = "apache-airflow-shared-timezones", editable = "shared/timezones" }, - { name = "apache-airflow-shared-workloads", editable = "shared/workloads" }, { name = "apache-airflow-task-sdk", extras = ["all"], editable = "task-sdk" }, { name = "apache-airflow-task-sdk-integration-tests", editable = "task-sdk-integration-tests" }, ] @@ -7205,46 +7193,6 @@ dev = [ ] docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "devel-common" }] -[[package]] -name = "apache-airflow-providers-sdk-java" -version = "0.1.0" -source = { editable = "providers/sdk/java" } -dependencies = [ - { name = "apache-airflow" }, -] - -[package.optional-dependencies] -common-compat = [ - { name = "apache-airflow-providers-common-compat" }, -] - -[package.dev-dependencies] -dev = [ - { name = "apache-airflow" }, - { name = "apache-airflow-devel-common" }, - { name = "apache-airflow-providers-common-compat" }, - { name = "apache-airflow-task-sdk" }, -] -docs = [ - { name = "apache-airflow-devel-common", extra = ["docs"] }, -] - -[package.metadata] -requires-dist = [ - { name = "apache-airflow", editable = "." }, - { name = "apache-airflow-providers-common-compat", marker = "extra == 'common-compat'", editable = "providers/common/compat" }, -] -provides-extras = ["common-compat"] - -[package.metadata.requires-dev] -dev = [ - { name = "apache-airflow", editable = "." }, - { name = "apache-airflow-devel-common", editable = "devel-common" }, - { name = "apache-airflow-providers-common-compat", editable = "providers/common/compat" }, - { name = "apache-airflow-task-sdk", editable = "task-sdk" }, -] -docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "devel-common" }] - [[package]] name = "apache-airflow-providers-segment" version = "3.9.4" @@ -8654,25 +8602,6 @@ requires-dist = [ dev = [{ name = "apache-airflow-devel-common", editable = "devel-common" }] mypy = [{ name = "apache-airflow-devel-common", extras = ["mypy"], editable = "devel-common" }] -[[package]] -name = "apache-airflow-shared-workloads" -version = "0.0" -source = { editable = "shared/workloads" } -dependencies = [ - { name = "pydantic" }, -] - -[package.dev-dependencies] -dev = [ - { name = "apache-airflow-devel-common" }, -] - -[package.metadata] -requires-dist = [{ name = "pydantic", specifier = ">=2.10.0" }] - -[package.metadata.requires-dev] -dev = [{ name = "apache-airflow-devel-common", editable = "devel-common" }] - [[package]] name = "apache-airflow-task-sdk" source = { editable = "task-sdk" } From 6f66928f08a9ee360141cfb2573e0690e03e2a78 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 15:15:28 +0800 Subject: [PATCH 28/75] CI: Fix DB migration and breeze images --- dev/breeze/doc/images/output_build-docs.svg | 4 +-- dev/breeze/doc/images/output_build-docs.txt | 2 +- ...release-management_add-back-references.svg | 4 +-- ...release-management_add-back-references.txt | 2 +- ...ement_generate-issue-content-providers.svg | 6 ++-- ...ement_generate-issue-content-providers.txt | 2 +- ...management_generate-providers-metadata.svg | 30 +++++++++++-------- ...management_generate-providers-metadata.txt | 2 +- ...agement_prepare-provider-distributions.svg | 6 ++-- ...agement_prepare-provider-distributions.txt | 2 +- ...agement_prepare-provider-documentation.svg | 6 ++-- ...agement_prepare-provider-documentation.txt | 2 +- ...output_release-management_publish-docs.svg | 4 +-- ...output_release-management_publish-docs.txt | 2 +- ...t_sbom_generate-providers-requirements.svg | 6 ++-- ...t_sbom_generate-providers-requirements.txt | 2 +- .../output_workflow-run_publish-docs.svg | 4 +-- .../output_workflow-run_publish-docs.txt | 2 +- 18 files changed, 46 insertions(+), 42 deletions(-) diff --git a/dev/breeze/doc/images/output_build-docs.svg b/dev/breeze/doc/images/output_build-docs.svg index 1858bbb097e91..2a0812c05163e 100644 --- a/dev/breeze/doc/images/output_build-docs.svg +++ b/dev/breeze/doc/images/output_build-docs.svg @@ -240,8 +240,8 @@ | hashicorp | helm-chart | http | imap | influxdb | informatica | jdbc | jenkins | keycloak | microsoft.azure |        microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai | openfaas | openlineage |  opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres | presto | qdrant | redis |    -salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh | standard |    -tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...             +salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh |    +standard | tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...  Build documents. diff --git a/dev/breeze/doc/images/output_build-docs.txt b/dev/breeze/doc/images/output_build-docs.txt index 54d8d4e3f39bb..247bee9e56ff6 100644 --- a/dev/breeze/doc/images/output_build-docs.txt +++ b/dev/breeze/doc/images/output_build-docs.txt @@ -1 +1 @@ -c5f2067ec852773089ed0ca7b8d1d533 +b4c249b4d1f7605a443774262109694a diff --git a/dev/breeze/doc/images/output_release-management_add-back-references.svg b/dev/breeze/doc/images/output_release-management_add-back-references.svg index f17f7f47ed43b..37e9086660253 100644 --- a/dev/breeze/doc/images/output_release-management_add-back-references.svg +++ b/dev/breeze/doc/images/output_release-management_add-back-references.svg @@ -155,8 +155,8 @@ | hashicorp | helm-chart | http | imap | influxdb | informatica | jdbc | jenkins | keycloak | microsoft.azure |        microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai | openfaas | openlineage |  opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres | presto | qdrant | redis |    -salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh | standard |    -tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...             +salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh |    +standard | tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...  Command to add back references for documentation to make it backward compatible. diff --git a/dev/breeze/doc/images/output_release-management_add-back-references.txt b/dev/breeze/doc/images/output_release-management_add-back-references.txt index ffc7eeea6018b..a43ec033fc2a6 100644 --- a/dev/breeze/doc/images/output_release-management_add-back-references.txt +++ b/dev/breeze/doc/images/output_release-management_add-back-references.txt @@ -1 +1 @@ -3df401aef0085547b08fe896a9a65381 +a44de0a6fcf0ad832e0b2a73a883f0a0 diff --git a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg index 8fe24cdf434e6..6566b6c97716f 100644 --- a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg +++ b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg @@ -149,9 +149,9 @@ github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins | keycloak |                microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai |         openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres |     -presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake |    -sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb |          -zendesk]...                                                                                                            +presto | qdrant | redis | salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp |     +snowflake | sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex |    +ydb | zendesk]...                                                                                                      Generates content for issue to test the release. diff --git a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt index c6189be26338f..0c327de82828f 100644 --- a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt +++ b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt @@ -1 +1 @@ -a85c889b710aa347eb6c47fc36b11720 +ee99c790838efb1d5e5a3b06e6c49846 diff --git a/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg b/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg index 867b9fedc0357..742e316f5a754 100644 --- a/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg +++ b/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg @@ -1,4 +1,4 @@ - + diff --git a/dev/breeze/doc/images/output_release-management_generate-providers-metadata.txt b/dev/breeze/doc/images/output_release-management_generate-providers-metadata.txt index 3615848d57819..6e5c7fd64de14 100644 --- a/dev/breeze/doc/images/output_release-management_generate-providers-metadata.txt +++ b/dev/breeze/doc/images/output_release-management_generate-providers-metadata.txt @@ -1 +1 @@ -fdfdca32a5248d3b91cb29e14cc538b4 +de007da2573c2e6066fd2b0d26d14874 diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.svg b/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.svg index 3661b47f2a46d..dbabcc063abf1 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.svg +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.svg @@ -197,9 +197,9 @@ github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins | keycloak |                microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai |         openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres |     -presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake |    -sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb |          -zendesk]...                                                                                                            +presto | qdrant | redis | salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp |     +snowflake | sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex |    +ydb | zendesk]...                                                                                                      Prepare sdist/whl distributions of Airflow Providers. Each provider directory is wiped with `git clean -fdx`  (preserving .venv, .idea, .vscode) before build to keep in-tree generated files out of the artifact. See dev/breeze  diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt b/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt index f10fd70bd89fa..aa4a21a6dcebd 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt @@ -1 +1 @@ -18d45fa2bec60ab0557f04fb4427b35e +71c54d02659478978d0aa40b2baf4fef diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg index c4454038e4a78..ae36d4fde676e 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg @@ -218,9 +218,9 @@ github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins | keycloak |                microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai |         openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres |     -presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake |    -sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb |          -zendesk]...                                                                                                            +presto | qdrant | redis | salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp |     +snowflake | sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex |    +ydb | zendesk]...                                                                                                      Prepare CHANGELOG, README and COMMITS information for providers. diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt index 5586a29b8136c..68e5927948e4f 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt @@ -1 +1 @@ -622441d283775edefeda685820e7169a +542fd516d5584cf5bc1b6aa945338a8c diff --git a/dev/breeze/doc/images/output_release-management_publish-docs.svg b/dev/breeze/doc/images/output_release-management_publish-docs.svg index d119da2013d75..291f8b0d144e3 100644 --- a/dev/breeze/doc/images/output_release-management_publish-docs.svg +++ b/dev/breeze/doc/images/output_release-management_publish-docs.svg @@ -194,8 +194,8 @@ | hashicorp | helm-chart | http | imap | influxdb | informatica | jdbc | jenkins | keycloak | microsoft.azure |        microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai | openfaas | openlineage |  opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres | presto | qdrant | redis |    -salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh | standard |    -tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...             +salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh |    +standard | tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...  Command to publish generated documentation to airflow-site diff --git a/dev/breeze/doc/images/output_release-management_publish-docs.txt b/dev/breeze/doc/images/output_release-management_publish-docs.txt index c73c7846664c8..487f7d9fef5f8 100644 --- a/dev/breeze/doc/images/output_release-management_publish-docs.txt +++ b/dev/breeze/doc/images/output_release-management_publish-docs.txt @@ -1 +1 @@ -4521ec02334b8909f66e82c460a69446 +6a7fed8b89fffc1e9d8856bf1a2d5f2d diff --git a/dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg b/dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg index fd62a65b513d4..ec6cd73739017 100644 --- a/dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg +++ b/dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg @@ -189,9 +189,9 @@ │| grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins | keycloak | │ │microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | │ │odbc | openai | openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill │ -│| pgvector | pinecone | postgres | presto | qdrant | redis | salesforce | samba | segment | │ -│sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh | standard | tableau | │ -│telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk)│ +│| pgvector | pinecone | postgres | presto | qdrant | redis | salesforce | samba | sdk.java | │ +│segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh | standard | │ +│tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk)│ │--provider-versionProvider version to generate the requirements for i.e `2.1.0`. `latest` is also a supported     │ │value to account for the most recent version of the provider (TEXT)│ │--force           Force update providers requirements even if they already exist.│ diff --git a/dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt b/dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt index a7761ea29d68a..8a0c324836340 100644 --- a/dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt +++ b/dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt @@ -1 +1 @@ -fa98bbcd73f9160c29eff1b6779a23bc +c4babe6a19ea7748ed3488c930187a8e diff --git a/dev/breeze/doc/images/output_workflow-run_publish-docs.svg b/dev/breeze/doc/images/output_workflow-run_publish-docs.svg index 511790e79d721..0b42e92e917d3 100644 --- a/dev/breeze/doc/images/output_workflow-run_publish-docs.svg +++ b/dev/breeze/doc/images/output_workflow-run_publish-docs.svg @@ -200,8 +200,8 @@ | hashicorp | helm-chart | http | imap | influxdb | informatica | jdbc | jenkins | keycloak | microsoft.azure |        microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai | openfaas | openlineage |  opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres | presto | qdrant | redis |    -salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh | standard |    -tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...             +salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh |    +standard | tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...  Trigger publish docs to S3 workflow diff --git a/dev/breeze/doc/images/output_workflow-run_publish-docs.txt b/dev/breeze/doc/images/output_workflow-run_publish-docs.txt index 6a433f7935a96..cbb67ce0a1df9 100644 --- a/dev/breeze/doc/images/output_workflow-run_publish-docs.txt +++ b/dev/breeze/doc/images/output_workflow-run_publish-docs.txt @@ -1 +1 @@ -6ff7091e58988c6273e51f372bb8a1a6 +a4876e7e49973aad884a0270de53885a From d8d520263af1bd2ddd608f94a2d709df17e5ec9a Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 15:35:59 +0800 Subject: [PATCH 29/75] CI: Fix failing items - Remove Java SDK setup in Dockerfile - add multi-language extras documentation - Update TaskInstanceDTO description, and adjust API version in generated files --- Dockerfile.ci | 24 ------------------- airflow-core/docs/extra-packages-ref.rst | 11 +++++++++ providers/sdk/java/docs/index.rst | 2 +- pyproject.toml | 4 ++-- .../execution_time/test_coordinator.py | 9 +++---- 5 files changed, 17 insertions(+), 33 deletions(-) diff --git a/Dockerfile.ci b/Dockerfile.ci index 1fb4ce4486ae1..a9d8b2e7f1dff 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1222,30 +1222,6 @@ function environment_initialization() { export AIRFLOW__SCHEDULER__GO_WORKER=True fi - if [[ ${JAVA_SDK=} == "true" ]]; then - echo - echo "${COLOR_BLUE}Setting up Java SDK${COLOR_RESET}" - echo - - # Install Java and the Java SDK provider - bash /opt/airflow/scripts/in_container/java_sdk_setup.sh - - # Set JAVA_HOME and PATH before Gradle build so gradlew can find java - export JAVA_HOME=/files/openjdk - export PATH=/files/openjdk/bin:${PATH} - - # Build both Java SDK bundles (stub + pure Java) - export JAVA_SDK_SRC_DIR=/opt/airflow/java-sdk - export BUNDLES_OUTPUT_DIR=/files/java-sdk-bundles - bash /opt/airflow/scripts/in_container/java_sdk_build.sh - - # Source the generated environment configuration - # shellcheck disable=SC1091 - source /files/java-sdk-bundles/java_sdk_env.sh - - echo "${COLOR_BLUE}Java SDK setup complete.${COLOR_RESET}" - fi - RUN_TESTS=${RUN_TESTS:="false"} CI=${CI:="false"} diff --git a/airflow-core/docs/extra-packages-ref.rst b/airflow-core/docs/extra-packages-ref.rst index 2646b0a7c3079..e576b59166dba 100644 --- a/airflow-core/docs/extra-packages-ref.rst +++ b/airflow-core/docs/extra-packages-ref.rst @@ -178,6 +178,17 @@ all the ``airflow`` packages together - similarly to what happened in Airflow 2. ``airflow-task-sdk`` separately, if you want to install providers, you need to install them separately as ``apache-airflow-providers-*`` distribution packages. +Multi-Language extras +===================== + +These are extras that add dependencies needed for integration with other languages runtimes. Currently we have only Java SDK related extra, but in the future we might add more extras related to other languages runtimes. + ++----------+------------------------------------------+------------------------------------------------------------------+ +| extra | install command | enables | ++==========+==========================================+==================================================================+ +| sdk.java | ``pip install apache-airflow[sdk.java]`` | JavaCoordinator for both dag processing and workload execution. | ++----------+------------------------------------------+------------------------------------------------------------------+ + Apache Software extras ====================== diff --git a/providers/sdk/java/docs/index.rst b/providers/sdk/java/docs/index.rst index 53b0719373c38..1104cb98fa866 100644 --- a/providers/sdk/java/docs/index.rst +++ b/providers/sdk/java/docs/index.rst @@ -56,7 +56,7 @@ The SDK: Java provider registers Java-specific task coordinator and DAG file pro apache-airflow-providers-sdk-java package ------------------------------------------------------- +----------------------------------------- Java SDK support for Apache Airflow runtime coordinators. diff --git a/pyproject.toml b/pyproject.toml index 3de5c2b45a418..b6b19605a41b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -339,7 +339,7 @@ apache-airflow = "airflow.__main__:main" "apache-airflow-providers-samba>=4.9.0" ] "sdk.java" = [ - "apache-airflow-providers-sdk-java>=0.1.0" # Set from local provider pyproject.toml + "apache-airflow-providers-sdk-java>=0.1.0" ] "segment" = [ "apache-airflow-providers-segment>=3.7.0" @@ -484,7 +484,7 @@ apache-airflow = "airflow.__main__:main" "apache-airflow-providers-redis>=4.0.0", "apache-airflow-providers-salesforce>=5.9.0", "apache-airflow-providers-samba>=4.9.0", - "apache-airflow-providers-sdk-java>=0.1.0", # Set from local provider pyproject.toml + "apache-airflow-providers-sdk-java>=0.1.0", "apache-airflow-providers-segment>=3.7.0", "apache-airflow-providers-sendgrid>=4.0.0", "apache-airflow-providers-sftp>=5.0.0", diff --git a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py index 783b46afad23e..5f4a878af8bef 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py +++ b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py @@ -17,6 +17,7 @@ # under the License. from __future__ import annotations +import contextlib import os import socket import subprocess @@ -245,10 +246,8 @@ def test_bridge_forwards_comm_bidirectionally(self): # If we got here without hanging, the bridge correctly processed all channels finally: for s in (sup_send, rt_send, log_send, stderr_send, sup_recv, rt_recv, log_recv, stderr_recv): - try: + with contextlib.suppress(OSError): s.close() - except OSError: - pass def test_bridge_drains_after_process_exit(self): """Verify _bridge drains remaining data after the subprocess exits.""" @@ -282,10 +281,8 @@ def test_bridge_drains_after_process_exit(self): stderr_local, stderr_remote, ): - try: + with contextlib.suppress(OSError): s.close() - except OSError: - pass def test_bridge_closes_all_sockets(self): """Verify _bridge closes all four sockets when done.""" From 9dd86d3cf9e71aa6852704ac1ebd0cb29aaef3c1 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 16:27:30 +0800 Subject: [PATCH 30/75] CI: Fix failing items - Update JavaCoordinator to use TaskInstanceDTO - add compatibility check for Airflow >= 3.3.0 --- .../sdk/java/src/airflow/providers/sdk/java/coordinator.py | 5 +++-- .../sdk/java/tests/unit/sdk/java/test_java_provider.py | 7 +++++++ scripts/in_container/install_airflow_and_providers.py | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py b/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py index a4d32020261b9..0ebd47b0d7c59 100644 --- a/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py +++ b/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py @@ -29,7 +29,8 @@ from airflow.sdk.execution_time.coordinator import BaseCoordinator if TYPE_CHECKING: - from airflow.sdk.api.datamodels._generated import BundleInfo, TaskInstance + from airflow._shared.workloads import TaskInstanceDTO + from airflow.sdk.api.datamodels._generated import BundleInfo class JavaCoordinator(BaseCoordinator): @@ -83,7 +84,7 @@ def dag_parsing_cmd( def task_execution_cmd( cls, *, - what: TaskInstance, + what: TaskInstanceDTO, dag_file_path: str, bundle_path: str, bundle_info: BundleInfo, diff --git a/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py b/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py index fe2a61ef18ae3..8615db6bb50f7 100644 --- a/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py +++ b/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py @@ -17,9 +17,16 @@ # under the License. from __future__ import annotations +import pytest + from airflow.providers.sdk.java.coordinator import JavaCoordinator from airflow.providers.sdk.java.get_provider_info import get_provider_info +from tests_common.test_utils.version_compat import AIRFLOW_V_3_3_PLUS + +if not AIRFLOW_V_3_3_PLUS: + pytest.skip("Coordinator is only compatible with Airflow >= 3.3.0", allow_module_level=True) + def test_get_provider_info_exposes_java_runtime_components(): assert get_provider_info() == { diff --git a/scripts/in_container/install_airflow_and_providers.py b/scripts/in_container/install_airflow_and_providers.py index 4f203e8c6c3f0..5e2c47ec58f19 100755 --- a/scripts/in_container/install_airflow_and_providers.py +++ b/scripts/in_container/install_airflow_and_providers.py @@ -1064,6 +1064,7 @@ def install_airflow_and_providers( "apache-airflow-providers-common-messaging", "apache-airflow-providers-git", "apache-airflow-providers-edge3", + "apache-airflow-providers-sdk-java", ] run_command( ["uv", "pip", "uninstall", *providers_to_uninstall_for_airflow_2], From cfd5873e279f0a5dc5303fc113987f8b152fd6ef Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 16:31:02 +0800 Subject: [PATCH 31/75] CI: Add compat for create_runtime_ti pytest fixture --- devel-common/src/tests_common/pytest_plugin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/devel-common/src/tests_common/pytest_plugin.py b/devel-common/src/tests_common/pytest_plugin.py index dbfc7946f4d01..0801cb2078e4e 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -36,6 +36,8 @@ import time_machine from _pytest.config.findpaths import ConfigValue +from tests_common.test_utils.version_compat import AIRFLOW_V_3_3_PLUS + if TYPE_CHECKING: from uuid import UUID @@ -2546,7 +2548,11 @@ def _create_task_instance( should_retry: bool | None = None, max_tries: int | None = None, ) -> RuntimeTaskInstance: - from airflow.sdk._shared.workloads import TaskInstanceDTO + if AIRFLOW_V_3_3_PLUS: + from airflow.sdk._shared.workloads import TaskInstanceDTO + else: + from airflow.executors.workloads.task import TaskInstanceDTO + from airflow.sdk.api.datamodels._generated import DagRun, DagRunState, TIRunContext from airflow.utils.types import DagRunType From a2a8244182cdea819364fb8dc0cb9df1d5a8a9d1 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 16:59:04 +0800 Subject: [PATCH 32/75] CI: Fix Java provider test to include configuration options --- .../src/tests_common/pytest_plugin.py | 4 ++-- .../tests/unit/sdk/java/test_java_provider.py | 24 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/devel-common/src/tests_common/pytest_plugin.py b/devel-common/src/tests_common/pytest_plugin.py index 0801cb2078e4e..dc6b297e7c62e 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -36,8 +36,6 @@ import time_machine from _pytest.config.findpaths import ConfigValue -from tests_common.test_utils.version_compat import AIRFLOW_V_3_3_PLUS - if TYPE_CHECKING: from uuid import UUID @@ -2548,6 +2546,8 @@ def _create_task_instance( should_retry: bool | None = None, max_tries: int | None = None, ) -> RuntimeTaskInstance: + from tests_common.test_utils.version_compat import AIRFLOW_V_3_3_PLUS + if AIRFLOW_V_3_3_PLUS: from airflow.sdk._shared.workloads import TaskInstanceDTO else: diff --git a/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py b/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py index 8615db6bb50f7..e0489ada7cc17 100644 --- a/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py +++ b/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py @@ -34,15 +34,23 @@ def test_get_provider_info_exposes_java_runtime_components(): "name": "SDK: Java", "description": "Java SDK support for Apache Airflow runtime coordinators.\n", "integrations": [ - { - "integration-name": "Java", - "external-doc-url": "https://openjdk.org/", - "tags": ["software"], - } - ], - "coordinators": [ - "airflow.providers.sdk.java.coordinator.JavaCoordinator", + {"integration-name": "Java", "external-doc-url": "https://openjdk.org/", "tags": ["software"]} ], + "config": { + "java": { + "description": "Options for the Java SDK provider.", + "options": { + "bundles_folder": { + "description": "Path to the directory containing Java DAG bundle JARs.\nWhen using Python stub DAGs that delegate task execution to Java,\nthe coordinator scans this directory to find the JAR bundle matching\nthe target dag_id. Each immediate subdirectory is treated as a\nseparate bundle home, and the directory itself is also checked\n(flat layout).\n", + "type": "string", + "version_added": None, + "example": "~/airflow/java-bundles", + "default": "", + } + }, + } + }, + "coordinators": ["airflow.providers.sdk.java.coordinator.JavaCoordinator"], } From 4cd16112abe6c23af97bd1006283c8ae442b2e93 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 18:58:42 +0800 Subject: [PATCH 33/75] CI: Fix self-review nits --- .../src/airflow/executors/base_executor.py | 3 +-- devel-common/src/tests_common/pytest_plugin.py | 5 ++--- providers/sdk/java/README.rst | 2 +- providers/sdk/java/docs/changelog.rst | 2 +- providers/sdk/java/docs/commits.rst | 2 +- providers/sdk/java/docs/index.rst | 6 +++--- providers/sdk/java/pyproject.toml | 14 +++++++++++++- .../src/airflow/providers/sdk/java/__init__.py | 4 ++-- .../airflow/providers/sdk/java/bundle_scanner.py | 6 +++--- 9 files changed, 27 insertions(+), 17 deletions(-) diff --git a/airflow-core/src/airflow/executors/base_executor.py b/airflow-core/src/airflow/executors/base_executor.py index 18011d72917fb..1d223c567616a 100644 --- a/airflow-core/src/airflow/executors/base_executor.py +++ b/airflow-core/src/airflow/executors/base_executor.py @@ -70,7 +70,6 @@ def get_execution_api_server_url(conf_source: AirflowConfigParser | ExecutorConf from sqlalchemy.orm import Session - from airflow._shared.workloads import TaskInstanceDTO from airflow.api_fastapi.auth.tokens import JWTGenerator from airflow.callbacks.base_callback_sink import BaseCallbackSink from airflow.callbacks.callback_requests import CallbackRequest @@ -655,7 +654,7 @@ def run_workload( from airflow.sdk.execution_time.supervisor import supervise_task return supervise_task( - ti=cast("TaskInstanceDTO", workload.ti), + ti=workload.ti, bundle_info=workload.bundle_info, dag_rel_path=workload.dag_rel_path, token=workload.token, diff --git a/devel-common/src/tests_common/pytest_plugin.py b/devel-common/src/tests_common/pytest_plugin.py index dc6b297e7c62e..faa53c986b18b 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -2551,7 +2551,7 @@ def _create_task_instance( if AIRFLOW_V_3_3_PLUS: from airflow.sdk._shared.workloads import TaskInstanceDTO else: - from airflow.executors.workloads.task import TaskInstanceDTO + from airflow.executors.workloads.task import TaskInstanceDTO # type: ignore[no-redef,assignment] from airflow.sdk.api.datamodels._generated import DagRun, DagRunState, TIRunContext from airflow.utils.types import DagRunType @@ -2636,8 +2636,7 @@ def _create_task_instance( dag_id=dag_id, run_id=run_id, try_number=try_number, - map_index=map_index if map_index is not None else -1, - dag_version_id=uuid7(), + map_index=map_index if map_index is not None else -1, dag_version_id=uuid7(), pool_slots=1, queue="default", priority_weight=1, diff --git a/providers/sdk/java/README.rst b/providers/sdk/java/README.rst index c183b5d7d6d65..ba3081bb6cb53 100644 --- a/providers/sdk/java/README.rst +++ b/providers/sdk/java/README.rst @@ -26,7 +26,7 @@ Package ``apache-airflow-providers-sdk-java`` Release: ``0.1.0`` -Java SDK support for Apache Airflow task coordinators and DAG file processors. +Java Coordinator Provider package diff --git a/providers/sdk/java/docs/changelog.rst b/providers/sdk/java/docs/changelog.rst index c6a923fee58d7..c5aa1ad337ef8 100644 --- a/providers/sdk/java/docs/changelog.rst +++ b/providers/sdk/java/docs/changelog.rst @@ -34,7 +34,7 @@ Changelog Features ~~~~~~~~ -* ``Add the initial Java provider with task coordinators and DAG file processors`` +* ``Add the initial Java coordinator interface`` .. Below changes are excluded from the changelog. Move them to appropriate section above if needed. Do not delete the lines(!): diff --git a/providers/sdk/java/docs/commits.rst b/providers/sdk/java/docs/commits.rst index bbd5ad0573136..6b84d751e94e3 100644 --- a/providers/sdk/java/docs/commits.rst +++ b/providers/sdk/java/docs/commits.rst @@ -26,7 +26,7 @@ Package apache-airflow-providers-sdk-java ------------------------------------------------------ -Java SDK support for Apache Airflow task coordinators and DAG file processors. +Java Coordinator This is detailed commit list of changes for versions provider package: ``sdk.java``. diff --git a/providers/sdk/java/docs/index.rst b/providers/sdk/java/docs/index.rst index 1104cb98fa866..7d4854e09b430 100644 --- a/providers/sdk/java/docs/index.rst +++ b/providers/sdk/java/docs/index.rst @@ -56,7 +56,7 @@ The SDK: Java provider registers Java-specific task coordinator and DAG file pro apache-airflow-providers-sdk-java package ------------------------------------------ +------------------------------------------------------ Java SDK support for Apache Airflow runtime coordinators. @@ -79,12 +79,12 @@ For the minimum Airflow version supported, see ``Requirements`` below. Requirements ------------ -The minimum Apache Airflow version supported by this provider distribution is ``3.0.0``. +The minimum Apache Airflow version supported by this provider distribution is ``3.3.0``. ================== ================== PIP package Version required ================== ================== -``apache-airflow`` ``>=3.0.0`` +``apache-airflow`` ``>=3.3.0`` ================== ================== Cross provider package dependencies diff --git a/providers/sdk/java/pyproject.toml b/providers/sdk/java/pyproject.toml index 6ed7ec4165a89..6baca6f81fdd4 100644 --- a/providers/sdk/java/pyproject.toml +++ b/providers/sdk/java/pyproject.toml @@ -59,7 +59,7 @@ requires-python = ">=3.10" # Make sure to run ``prek update-providers-dependencies --all-files`` # After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build`` dependencies = [ - "apache-airflow>=3.0.0", + "apache-airflow>=3.3.0", ] # The optional dependencies should be modified in place in the generated file @@ -117,3 +117,15 @@ provider_info = "airflow.providers.sdk.java.get_provider_info:get_provider_info" [tool.flit.module] name = "airflow.providers.sdk.java" + +# Explicit sdist contents so the build does not rely on VCS information +# (flit 4.0 makes --no-use-vcs the default — see https://github.com/pypa/flit/pull/782). +[tool.flit.sdist] +include = [ + "docs/", + "provider.yaml", + "src/airflow/__init__.py", + "src/airflow/providers/__init__.py", + "src/airflow/providers/sdk/__init__.py", + "tests/", +] diff --git a/providers/sdk/java/src/airflow/providers/sdk/java/__init__.py b/providers/sdk/java/src/airflow/providers/sdk/java/__init__.py index 0e177079fd3de..1c942bc68df44 100644 --- a/providers/sdk/java/src/airflow/providers/sdk/java/__init__.py +++ b/providers/sdk/java/src/airflow/providers/sdk/java/__init__.py @@ -32,8 +32,8 @@ __version__ = "0.1.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( - "3.0.0" + "3.3.0" ): raise RuntimeError( - f"The package `apache-airflow-providers-sdk-java:{__version__}` needs Apache Airflow 3.0.0+" + f"The package `apache-airflow-providers-sdk-java:{__version__}` needs Apache Airflow 3.3.0+" ) diff --git a/providers/sdk/java/src/airflow/providers/sdk/java/bundle_scanner.py b/providers/sdk/java/src/airflow/providers/sdk/java/bundle_scanner.py index 16712ec524152..87bbf518b8e5b 100644 --- a/providers/sdk/java/src/airflow/providers/sdk/java/bundle_scanner.py +++ b/providers/sdk/java/src/airflow/providers/sdk/java/bundle_scanner.py @@ -33,7 +33,7 @@ import yaml -_MANIFEST_PATH = "META-INF/MANIFEST.MF" +MANIFEST_PATH = "META-INF/MANIFEST.MF" METADATA_MANIFEST_KEY = "Airflow-Java-SDK-Metadata" SDK_VERSION_MANIFEST_KEY = "Airflow-Java-SDK-Version" DAG_CODE_MANIFEST_KEY = "Airflow-Java-SDK-Dag-Code" @@ -155,7 +155,7 @@ def _read_bundle_jar(jar_path: Path) -> tuple[str, set[str]] | None: try: with zipfile.ZipFile(jar_path) as zf: try: - with zf.open(_MANIFEST_PATH) as f: + with zf.open(MANIFEST_PATH) as f: manifest = email.message_from_binary_file(f) except KeyError: return None @@ -194,7 +194,7 @@ def read_dag_code(jar_path: Path) -> str | None: try: with zipfile.ZipFile(jar_path) as zf: try: - with zf.open(_MANIFEST_PATH) as f: + with zf.open(MANIFEST_PATH) as f: manifest = email.message_from_binary_file(f) except KeyError: return None From 45a0a7341dafbeef310b014051856683d0a926c2 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 18:59:55 +0800 Subject: [PATCH 34/75] Revert MappedOperator change --- devel-common/src/tests_common/pytest_plugin.py | 3 ++- task-sdk/src/airflow/sdk/definitions/mappedoperator.py | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/devel-common/src/tests_common/pytest_plugin.py b/devel-common/src/tests_common/pytest_plugin.py index faa53c986b18b..9911b07b8c14a 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -2636,7 +2636,8 @@ def _create_task_instance( dag_id=dag_id, run_id=run_id, try_number=try_number, - map_index=map_index if map_index is not None else -1, dag_version_id=uuid7(), + map_index=map_index if map_index is not None else -1, + dag_version_id=uuid7(), pool_slots=1, queue="default", priority_weight=1, diff --git a/task-sdk/src/airflow/sdk/definitions/mappedoperator.py b/task-sdk/src/airflow/sdk/definitions/mappedoperator.py index 4ee9e7b688dd8..0faa2ab6f1850 100644 --- a/task-sdk/src/airflow/sdk/definitions/mappedoperator.py +++ b/task-sdk/src/airflow/sdk/definitions/mappedoperator.py @@ -673,10 +673,6 @@ def executor(self) -> str | None: def executor_config(self) -> dict: return self.partial_kwargs.get("executor_config", {}) - @property - def sdk(self) -> str | None: - return self.partial_kwargs.get("sdk") - @property def inlets(self) -> list[Any]: return self.partial_kwargs.get("inlets", []) From 5e8664efd6a1ffe28e1682df325db8ece05adb76 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:14:44 +0800 Subject: [PATCH 35/75] CI: Fix failing items - Updated the Airflow issue template to include 'sdk-java' as an option. - Added unit tests for JavaCoordinator functionality. - Created a new test file for Java bundle scanning. - Updated uv.lock to reflect new dependency requirements for tomli. --- .../ISSUE_TEMPLATE/1-airflow_bug_report.yml | 1 + airflow-core/docs/extra-packages-ref.rst | 2 +- .../unit/sdk/java/test_bundle_scanner.py | 337 ++++++++++++++++++ .../tests/unit/sdk/java/test_coordinator.py | 242 +++++++++++++ 4 files changed, 581 insertions(+), 1 deletion(-) create mode 100644 providers/sdk/java/tests/unit/sdk/java/test_bundle_scanner.py create mode 100644 providers/sdk/java/tests/unit/sdk/java/test_coordinator.py diff --git a/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml b/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml index a9c8978740344..e5060dbe3671d 100644 --- a/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml @@ -192,6 +192,7 @@ body: - redis - salesforce - samba + - sdk-java - segment - sendgrid - sftp diff --git a/airflow-core/docs/extra-packages-ref.rst b/airflow-core/docs/extra-packages-ref.rst index e576b59166dba..9fb579c9b08ec 100644 --- a/airflow-core/docs/extra-packages-ref.rst +++ b/airflow-core/docs/extra-packages-ref.rst @@ -184,7 +184,7 @@ Multi-Language extras These are extras that add dependencies needed for integration with other languages runtimes. Currently we have only Java SDK related extra, but in the future we might add more extras related to other languages runtimes. +----------+------------------------------------------+------------------------------------------------------------------+ -| extra | install command | enables | +| extra | install command | enables | +==========+==========================================+==================================================================+ | sdk.java | ``pip install apache-airflow[sdk.java]`` | JavaCoordinator for both dag processing and workload execution. | +----------+------------------------------------------+------------------------------------------------------------------+ diff --git a/providers/sdk/java/tests/unit/sdk/java/test_bundle_scanner.py b/providers/sdk/java/tests/unit/sdk/java/test_bundle_scanner.py new file mode 100644 index 0000000000000..5c042036143da --- /dev/null +++ b/providers/sdk/java/tests/unit/sdk/java/test_bundle_scanner.py @@ -0,0 +1,337 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import os +import zipfile +from pathlib import Path + +import pytest +import yaml + +from airflow.providers.sdk.java.bundle_scanner import ( + DAG_CODE_MANIFEST_KEY, + MAIN_CLASS_MANIFEST_KEY, + MANIFEST_PATH, + METADATA_MANIFEST_KEY, + SDK_VERSION_MANIFEST_KEY, + BundleScanner, + ResolvedJarBundle, + _jar_files, + _normalize_bundle_home, + _parse_dag_ids_from_metadata, + _read_bundle_jar, + read_dag_code, +) + +METADATA_YAML_PATH = "META-INF/airflow-metadata.yaml" +DAG_CODE_PATH = "JavaExample.java" +TEST_MAIN_CLASS = "com.example.MyDag" +TEST_SDK_VERSION = "1.0.0" + + +def _make_manifest( + *, + main_class: str | None = TEST_MAIN_CLASS, + metadata_path: str | None = METADATA_YAML_PATH, + sdk_version: str | None = TEST_SDK_VERSION, + dag_code_path: str | None = None, +) -> str: + lines = ["Manifest-Version: 1.0"] + if main_class: + lines.append(f"{MAIN_CLASS_MANIFEST_KEY}: {main_class}") + if metadata_path: + lines.append(f"{METADATA_MANIFEST_KEY}: {metadata_path}") + if sdk_version: + lines.append(f"{SDK_VERSION_MANIFEST_KEY}: {sdk_version}") + if dag_code_path: + lines.append(f"{DAG_CODE_MANIFEST_KEY}: {dag_code_path}") + return "\n".join(lines) + "\n" + + +def _make_metadata_yaml(dag_ids: list[str]) -> str: + return yaml.dump({"dags": {dag_id: {} for dag_id in dag_ids}}) + + +def _create_bundle_jar( + jar_path: Path, + *, + dag_ids: list[str] | None = None, + main_class: str | None = TEST_MAIN_CLASS, + include_metadata: bool = True, + include_manifest: bool = True, + dag_code: str | None = None, +) -> Path: + """Create a minimal JAR (zip) file with Airflow Java SDK manifest attributes.""" + with zipfile.ZipFile(jar_path, "w") as zf: + if include_manifest: + dag_code_path = DAG_CODE_PATH if dag_code else None + manifest = _make_manifest( + main_class=main_class, + metadata_path=METADATA_YAML_PATH if include_metadata else None, + dag_code_path=dag_code_path, + ) + zf.writestr(MANIFEST_PATH, manifest) + + if include_metadata and dag_ids is not None: + zf.writestr(METADATA_YAML_PATH, _make_metadata_yaml(dag_ids)) + + if dag_code: + zf.writestr(DAG_CODE_PATH, dag_code) + return jar_path + + +class TestJarFiles: + def test_lists_jar_files_sorted(self, tmp_path: Path): + (tmp_path / "b.jar").touch() + (tmp_path / "a.jar").touch() + (tmp_path / "c.txt").touch() + result = _jar_files(tmp_path) + assert result == [tmp_path / "a.jar", tmp_path / "b.jar"] + + def test_returns_empty_for_nonexistent_directory(self, tmp_path: Path): + assert _jar_files(tmp_path / "nonexistent") == [] + + def test_returns_empty_for_directory_with_no_jars(self, tmp_path: Path): + (tmp_path / "readme.txt").touch() + assert _jar_files(tmp_path) == [] + + def test_ignores_jar_directories(self, tmp_path: Path): + (tmp_path / "fake.jar").mkdir() + assert _jar_files(tmp_path) == [] + + +class TestNormalizeBundleHome: + def test_jar_file_returns_parent(self, tmp_path: Path): + jar = tmp_path / "bundle.jar" + jar.touch() + assert _normalize_bundle_home(jar) == tmp_path.resolve() + + def test_dir_with_lib_containing_jars(self, tmp_path: Path): + lib = tmp_path / "lib" + lib.mkdir() + (lib / "dep.jar").touch() + assert _normalize_bundle_home(tmp_path) == lib.resolve() + + def test_dir_with_empty_lib(self, tmp_path: Path): + lib = tmp_path / "lib" + lib.mkdir() + assert _normalize_bundle_home(tmp_path) == tmp_path.resolve() + + def test_plain_directory(self, tmp_path: Path): + assert _normalize_bundle_home(tmp_path) == tmp_path.resolve() + + +class TestParseDagIdsFromMetadata: + def test_parses_dag_ids(self): + content = yaml.dump({"dags": {"dag_a": {}, "dag_b": {"key": "val"}}}) + assert _parse_dag_ids_from_metadata(content) == {"dag_a", "dag_b"} + + @pytest.mark.parametrize( + "yaml_content", + [ + pytest.param(yaml.dump({"other": 1}), id="missing_dags_key"), + pytest.param("just a string", id="non_dict"), + pytest.param(yaml.dump({"dags": {}}), id="empty_dags"), + ], + ) + def test_returns_empty_set(self, yaml_content): + assert _parse_dag_ids_from_metadata(yaml_content) == set() + + +class TestReadBundleJar: + def test_valid_jar(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "valid.jar", dag_ids=["my_dag"]) + result = _read_bundle_jar(jar) + assert result is not None + main_class, dag_ids = result + assert main_class == TEST_MAIN_CLASS + assert dag_ids == {"my_dag"} + + def test_returns_none_for_missing_manifest(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "no_manifest.jar", include_manifest=False) + assert _read_bundle_jar(jar) is None + + def test_returns_none_for_missing_metadata_key(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "no_meta.jar", include_metadata=False) + assert _read_bundle_jar(jar) is None + + def test_returns_none_for_missing_main_class(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "no_main.jar", dag_ids=["d"], main_class=None) + assert _read_bundle_jar(jar) is None + + def test_returns_none_for_missing_metadata_file(self, tmp_path: Path): + """Manifest references a metadata file that does not exist inside the JAR.""" + jar = tmp_path / "missing_meta_file.jar" + with zipfile.ZipFile(jar, "w") as zf: + manifest = _make_manifest(metadata_path="nonexistent.yaml") + zf.writestr(MANIFEST_PATH, manifest) + assert _read_bundle_jar(jar) is None + + def test_returns_none_for_bad_zip(self, tmp_path: Path): + bad = tmp_path / "bad.jar" + bad.write_text("not a zip file") + assert _read_bundle_jar(bad) is None + + def test_returns_none_for_empty_dag_ids(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "empty_dags.jar", dag_ids=[]) + assert _read_bundle_jar(jar) is None + + def test_multiple_dag_ids(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "multi.jar", dag_ids=["dag_1", "dag_2", "dag_3"]) + result = _read_bundle_jar(jar) + assert result is not None + _, dag_ids = result + assert dag_ids == {"dag_1", "dag_2", "dag_3"} + + +class TestReadDagCode: + def test_reads_embedded_dag_code(self, tmp_path: Path): + code = "public class MyDag {}" + jar = _create_bundle_jar(tmp_path / "with_code.jar", dag_ids=["d"], dag_code=code) + assert read_dag_code(jar) == code + + def test_returns_none_for_missing_dag_code_key(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "no_code.jar", dag_ids=["d"]) + assert read_dag_code(jar) is None + + def test_returns_none_for_missing_manifest(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "no_manifest.jar", include_manifest=False) + assert read_dag_code(jar) is None + + def test_returns_none_for_bad_zip(self, tmp_path: Path): + bad = tmp_path / "bad.jar" + bad.write_text("not a zip") + assert read_dag_code(bad) is None + + def test_returns_none_when_code_file_missing(self, tmp_path: Path): + """Manifest references a dag code file that does not exist inside the JAR.""" + jar = tmp_path / "broken_code.jar" + with zipfile.ZipFile(jar, "w") as zf: + manifest = _make_manifest(dag_code_path="missing_source.py") + zf.writestr(MANIFEST_PATH, manifest) + assert read_dag_code(jar) is None + + +class TestBundleScannerResolveJar: + def test_returns_main_class(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "bundle.jar", dag_ids=["d"]) + assert BundleScanner.resolve_jar(jar) == TEST_MAIN_CLASS + + def test_raises_for_invalid_jar(self, tmp_path: Path): + jar = tmp_path / "not_bundle.jar" + jar.write_text("not a zip") + with pytest.raises(FileNotFoundError, match="Not a valid Airflow Java SDK bundle"): + BundleScanner.resolve_jar(jar) + + +class TestBundleScannerCandidateHomes: + def test_nested_layout(self, tmp_path: Path): + sub_a = tmp_path / "bundle_a" + sub_a.mkdir() + (sub_a / "app.jar").touch() + + sub_b = tmp_path / "bundle_b" + sub_b.mkdir() + (sub_b / "app.jar").touch() + + scanner = BundleScanner(tmp_path) + homes = scanner._candidate_homes() + # Nested subdirs + the bundles_dir itself + assert len(homes) == 3 + assert sub_a.resolve() in homes + assert sub_b.resolve() in homes + assert tmp_path.resolve() in homes + + def test_flat_layout(self, tmp_path: Path): + (tmp_path / "app.jar").touch() + scanner = BundleScanner(tmp_path) + homes = scanner._candidate_homes() + # Only the directory itself (no subdirectories) + assert homes == [tmp_path.resolve()] + + def test_nested_with_lib_subdir(self, tmp_path: Path): + sub = tmp_path / "my_bundle" + sub.mkdir() + lib = sub / "lib" + lib.mkdir() + (lib / "dep.jar").touch() + + scanner = BundleScanner(tmp_path) + homes = scanner._candidate_homes() + # _normalize_bundle_home should redirect to lib/ + assert lib.resolve() in homes + + +class TestBundleScannerResolve: + def test_finds_matching_dag(self, tmp_path: Path): + bundle_dir = tmp_path / "my_bundle" + bundle_dir.mkdir() + _create_bundle_jar(bundle_dir / "app.jar", dag_ids=["target_dag"]) + + scanner = BundleScanner(tmp_path) + result = scanner.resolve("target_dag") + assert isinstance(result, ResolvedJarBundle) + assert result.main_class == TEST_MAIN_CLASS + assert str((bundle_dir / "app.jar").resolve()) in result.classpath + + def test_raises_when_no_match(self, tmp_path: Path): + bundle_dir = tmp_path / "my_bundle" + bundle_dir.mkdir() + _create_bundle_jar(bundle_dir / "app.jar", dag_ids=["other_dag"]) + + scanner = BundleScanner(tmp_path) + with pytest.raises(FileNotFoundError, match="No JAR bundle containing dag_id='missing'"): + scanner.resolve("missing") + + def test_classpath_includes_all_jars(self, tmp_path: Path): + bundle_dir = tmp_path / "my_bundle" + bundle_dir.mkdir() + _create_bundle_jar(bundle_dir / "app.jar", dag_ids=["my_dag"]) + # Create a dependency JAR (no SDK metadata, just a plain JAR) + with zipfile.ZipFile(bundle_dir / "dep.jar", "w") as zf: + zf.writestr("dummy.class", b"") + + scanner = BundleScanner(tmp_path) + result = scanner.resolve("my_dag") + parts = result.classpath.split(os.pathsep) + assert len(parts) == 2 + + def test_flat_layout_resolve(self, tmp_path: Path): + _create_bundle_jar(tmp_path / "app.jar", dag_ids=["flat_dag"]) + + scanner = BundleScanner(tmp_path) + result = scanner.resolve("flat_dag") + assert result.main_class == TEST_MAIN_CLASS + + def test_skips_non_bundle_jars(self, tmp_path: Path): + bundle_dir = tmp_path / "my_bundle" + bundle_dir.mkdir() + # Non-bundle JAR (no manifest) + with zipfile.ZipFile(bundle_dir / "plain.jar", "w") as zf: + zf.writestr("dummy.class", b"") + _create_bundle_jar(bundle_dir / "real.jar", dag_ids=["real_dag"]) + + scanner = BundleScanner(tmp_path) + result = scanner.resolve("real_dag") + assert result.main_class == TEST_MAIN_CLASS + + def test_empty_bundles_dir(self, tmp_path: Path): + scanner = BundleScanner(tmp_path) + with pytest.raises(FileNotFoundError): + scanner.resolve("any_dag") diff --git a/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py b/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py new file mode 100644 index 0000000000000..2c42574dd3ed0 --- /dev/null +++ b/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py @@ -0,0 +1,242 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import uuid +import zipfile +from pathlib import Path +from unittest.mock import patch + +import pytest +import yaml + +from airflow._shared.workloads import TaskInstanceDTO +from airflow.providers.sdk.java.bundle_scanner import ( + MAIN_CLASS_MANIFEST_KEY, + MANIFEST_PATH, + METADATA_MANIFEST_KEY, + SDK_VERSION_MANIFEST_KEY, +) +from airflow.providers.sdk.java.coordinator import JavaCoordinator +from airflow.sdk.api.datamodels._generated import BundleInfo + +from tests_common.test_utils.version_compat import AIRFLOW_V_3_3_PLUS + +if not AIRFLOW_V_3_3_PLUS: + pytest.skip("Coordinator is only compatible with Airflow >= 3.3.0", allow_module_level=True) + +METADATA_YAML_PATH = "META-INF/airflow-metadata.yaml" +DAG_CODE_PATH = "dag_source.py" +TEST_MAIN_CLASS = "com.example.MyBundle" + + +def _make_manifest( + *, + main_class: str | None = TEST_MAIN_CLASS, + metadata_path: str | None = METADATA_YAML_PATH, + dag_code_path: str | None = None, +) -> str: + lines = ["Manifest-Version: 1.0"] + if main_class: + lines.append(f"{MAIN_CLASS_MANIFEST_KEY}: {main_class}") + if metadata_path: + lines.append(f"{METADATA_MANIFEST_KEY}: {metadata_path}") + lines.append(f"{SDK_VERSION_MANIFEST_KEY}: 1.0.0") + if dag_code_path: + lines.append(f"Airflow-Java-SDK-Dag-Code: {dag_code_path}") + return "\n".join(lines) + "\n" + + +def _create_bundle_jar( + jar_path: Path, + *, + dag_ids: list[str] | None = None, + dag_code: str | None = None, +) -> Path: + with zipfile.ZipFile(jar_path, "w") as zf: + dag_code_path = DAG_CODE_PATH if dag_code else None + manifest = _make_manifest(dag_code_path=dag_code_path) + zf.writestr(MANIFEST_PATH, manifest) + if dag_ids is not None: + metadata = yaml.dump({"dags": {d: {} for d in dag_ids}}) + zf.writestr(METADATA_YAML_PATH, metadata) + if dag_code: + zf.writestr(DAG_CODE_PATH, dag_code) + return jar_path + + +def _make_ti(dag_id: str = "test_dag") -> TaskInstanceDTO: + return TaskInstanceDTO( + id=uuid.uuid4(), + dag_version_id=uuid.uuid4(), + task_id="task_1", + dag_id=dag_id, + run_id="run_1", + try_number=1, + map_index=-1, + pool_slots=1, + queue="default", + priority_weight=1, + ) + + +class TestJavaCoordinatorAttributes: + def test_sdk(self): + assert JavaCoordinator.sdk == "java" + + def test_file_extension(self): + assert JavaCoordinator.file_extension == ".jar" + + +class TestCanHandleDagFile: + def test_valid_jar_returns_true(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "valid.jar", dag_ids=["d"]) + assert JavaCoordinator.can_handle_dag_file("bundle", str(jar)) is True + + def test_non_jar_file_returns_false(self, tmp_path: Path): + py_file = tmp_path / "dag.py" + py_file.write_text("from airflow import DAG") + assert JavaCoordinator.can_handle_dag_file("bundle", str(py_file)) is False + + def test_missing_file_returns_false(self, tmp_path: Path): + assert JavaCoordinator.can_handle_dag_file("bundle", str(tmp_path / "missing.jar")) is False + + def test_bad_zip_returns_false(self, tmp_path: Path): + bad = tmp_path / "bad.jar" + bad.write_text("not a zip") + assert JavaCoordinator.can_handle_dag_file("bundle", str(bad)) is False + + def test_jar_without_sdk_manifest_returns_false(self, tmp_path: Path): + jar = tmp_path / "plain.jar" + with zipfile.ZipFile(jar, "w") as zf: + zf.writestr("dummy.class", b"") + assert JavaCoordinator.can_handle_dag_file("bundle", str(jar)) is False + + +class TestGetCodeFromFile: + def test_returns_embedded_code(self, tmp_path: Path): + code = "from airflow import DAG\ndag = DAG('my_dag')" + jar = _create_bundle_jar(tmp_path / "with_code.jar", dag_ids=["d"], dag_code=code) + assert JavaCoordinator.get_code_from_file(str(jar)) == code + + def test_raises_when_no_code(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "no_code.jar", dag_ids=["d"]) + with pytest.raises(FileNotFoundError, match="No DAG source code found in JAR"): + JavaCoordinator.get_code_from_file(str(jar)) + + +class TestDagParsingCmd: + def test_builds_java_command(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "app.jar", dag_ids=["d"]) + bundle_path = str(tmp_path) + cmd = JavaCoordinator.dag_parsing_cmd( + dag_file_path=str(jar), + bundle_name="my_bundle", + bundle_path=bundle_path, + comm_addr="localhost:1234", + logs_addr="localhost:5678", + ) + assert cmd == [ + "java", + "-classpath", + f"{bundle_path}/*", + TEST_MAIN_CLASS, + "--comm=localhost:1234", + "--logs=localhost:5678", + ] + + +class TestTaskExecutionCmd: + def test_pure_java_dag(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "app.jar", dag_ids=["test_dag"]) + bundle_path = str(tmp_path) + ti = _make_ti() + bundle_info = BundleInfo(name="my_bundle") + + cmd = JavaCoordinator.task_execution_cmd( + what=ti, + dag_file_path=str(jar), + bundle_path=bundle_path, + bundle_info=bundle_info, + comm_addr="localhost:1234", + logs_addr="localhost:5678", + ) + assert cmd == [ + "java", + "-classpath", + f"{bundle_path}/*", + TEST_MAIN_CLASS, + "--comm=localhost:1234", + "--logs=localhost:5678", + ] + + def test_python_stub_dag_with_bundles_folder(self, tmp_path: Path): + bundles_folder = tmp_path / "java_bundles" + bundle_sub = bundles_folder / "my_bundle" + bundle_sub.mkdir(parents=True) + _create_bundle_jar(bundle_sub / "app.jar", dag_ids=["stub_dag"]) + + ti = _make_ti(dag_id="stub_dag") + bundle_info = BundleInfo(name="my_bundle") + + with patch( + "airflow.providers.common.compat.sdk.conf.get", + return_value=str(bundles_folder), + ): + cmd = JavaCoordinator.task_execution_cmd( + what=ti, + dag_file_path="/dags/stub_dag.py", + bundle_path="/some/bundle/path", + bundle_info=bundle_info, + comm_addr="localhost:1234", + logs_addr="localhost:5678", + ) + + assert cmd == [ + "java", + "-classpath", + f"{bundles_folder}/my_bundle/app.jar", + TEST_MAIN_CLASS, + "--comm=localhost:1234", + "--logs=localhost:5678", + ] + + @pytest.mark.parametrize( + "config_value", + [ + pytest.param(None, id="none"), + pytest.param("", id="empty_string"), + ], + ) + def test_python_stub_dag_invalid_config_raises(self, config_value): + ti = _make_ti() + bundle_info = BundleInfo(name="my_bundle") + + with patch( + "airflow.providers.common.compat.sdk.conf.get", + return_value=config_value, + ): + with pytest.raises(ValueError, match="bundles_folder config must be set"): + JavaCoordinator.task_execution_cmd( + what=ti, + dag_file_path="/dags/stub_dag.py", + bundle_path="/some/bundle/path", + bundle_info=bundle_info, + comm_addr="localhost:1234", + logs_addr="localhost:5678", + ) From 31742c810540f1226b3618edace40bd26cd74cb9 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:24:57 +0800 Subject: [PATCH 36/75] CI: Fix Task SDK test_task_runner failures using TaskInstanceDTO Replace TaskInstance with TaskInstanceDTO in StartupDetails fixtures and add the required pool_slots, queue, and priority_weight fields. --- .../execution_time/test_task_runner.py | 57 +++++++++++++++---- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py index dbac25fcf1678..d1b6a0c39db09 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py +++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py @@ -427,13 +427,16 @@ def test_main_sends_reschedule_task_when_startup_reschedules( mock_comms_instance.socket = None mock_comms_decoder_cls.__getitem__.return_value.return_value = mock_comms_instance what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="my_task", dag_id="test_dag", run_id="test_run", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, context_carrier={}, ), dag_rel_path="", @@ -600,13 +603,16 @@ def test_task_span_is_child_of_dag_run_span(make_ti_context): # Step 3: build StartupDetails with ti.context_carrier = ti_carrier. what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="my_task", dag_id="test_dag", run_id="test_run", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, context_carrier=ti_carrier, ), dag_rel_path="", @@ -668,13 +674,16 @@ def test_task_span_no_parent_when_no_context_carrier(make_ti_context): provider.add_span_processor(SimpleSpanProcessor(in_mem_exporter)) what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="standalone_task", dag_id="test_dag", run_id="test_run", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, context_carrier=None, ), dag_rel_path="", @@ -1159,13 +1168,16 @@ def test_basic_templated_dag(mocked_parse, make_ti_context, mock_supervisor_comm ) what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="templated_task", dag_id="basic_templated_dag", run_id="c", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), bundle_info=FAKE_BUNDLE, dag_rel_path="", @@ -1275,13 +1287,16 @@ def execute(self, context): instant = timezone.datetime(2024, 12, 3, 10, 0) what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="templated_task", dag_id="basic_dag", run_id="c", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), dag_rel_path="", bundle_info=FAKE_BUNDLE, @@ -1323,13 +1338,16 @@ def execute(self, context): instant = timezone.datetime(2024, 12, 3, 10, 0) what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="impersonation_task", dag_id="basic_dag", run_id="c", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), dag_rel_path="", bundle_info=FAKE_BUNDLE, @@ -1371,13 +1389,16 @@ def execute(self, context): instant = timezone.datetime(2024, 12, 3, 10, 0) what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="impersonation_task", dag_id="basic_dag", run_id="c", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), dag_rel_path="", bundle_info=FAKE_BUNDLE, @@ -1411,13 +1432,16 @@ def execute(self, context): instant = timezone.datetime(2024, 12, 3, 10, 0) what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="impersonation_task", dag_id="basic_dag", run_id="c", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), dag_rel_path="", bundle_info=FAKE_BUNDLE, @@ -1584,8 +1608,16 @@ def test_dag_parsing_context(make_ti_context, mock_supervisor_comms, monkeypatch task_id = "conditional_task" what = StartupDetails( - ti=TaskInstance( - id=uuid7(), task_id=task_id, dag_id=dag_id, run_id="c", try_number=1, dag_version_id=uuid7() + ti=TaskInstanceDTO( + id=uuid7(), + task_id=task_id, + dag_id=dag_id, + run_id="c", + try_number=1, + dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), dag_rel_path="dag_parsing_context.py", bundle_info=BundleInfo(name="my-bundle", version=None), @@ -4120,13 +4152,16 @@ def execute(self, context): task_id="test_task_runner_calls_listeners", do_xcom_push=True, multiple_outputs=True ) what = StartupDetails( - ti=TaskInstance( + ti=TaskInstanceDTO( id=uuid7(), task_id="templated_task", dag_id="basic_dag", run_id="c", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), dag_rel_path="", bundle_info=FAKE_BUNDLE, From b20641e545f743c5fddcc6a0f8b3d4886b0c3491 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:28:27 +0800 Subject: [PATCH 37/75] CI: Skip non-JAR paths in JavaCoordinator.can_handle_dag_file DagCode.get_code_from_file probes every coordinator's can_handle_dag_file on each fileloc, including .py paths nested inside ZIP DAGs (e.g. test_zip.zip/test_zip.py). The Java coordinator opened these as JAR files, raising NotADirectoryError because the parent path is a ZIP file rather than a directory. Short-circuit on the .jar suffix and add NotADirectoryError to the suppressed exceptions for safety. --- .../sdk/java/src/airflow/providers/sdk/java/coordinator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py b/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py index 0ebd47b0d7c59..ba70714cc313a 100644 --- a/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py +++ b/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py @@ -42,7 +42,9 @@ class JavaCoordinator(BaseCoordinator): @classmethod def can_handle_dag_file(cls, bundle_name: str, path: str | os.PathLike[str]) -> bool: """Return ``True`` when *path* is a JAR with valid Airflow Java SDK manifest attributes.""" - with contextlib.suppress(FileNotFoundError, zipfile.BadZipFile, KeyError): + if not os.fspath(path).endswith(cls.file_extension): + return False + with contextlib.suppress(FileNotFoundError, NotADirectoryError, zipfile.BadZipFile, KeyError): return BundleScanner.resolve_jar(Path(path)) is not None return False From 9fade1b57beb15e97d3960a8c22b9756d8027779 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:31:07 +0800 Subject: [PATCH 38/75] CI: Drop literal Example: line from queue_to_sdk config description The config.yml description duplicated the example field as a literal "Example:" line in the description text. With --include-descriptions this rendered as "# Example:", which trips test_cli_show_config_shows_descriptions. The example is already in the dedicated example field, so remove the duplicate from the description. --- airflow-core/src/airflow/config_templates/config.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml index f9629dfd87187..5fba029a0a126 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -2015,8 +2015,6 @@ sdk: to route the task to a non-Python runtime coordinator based on its queue. This is useful when queues are used as environment or isolation identifiers (e.g. ``foo``, ``bar``). - - Example: ``{"foo": "java", "bar": "java", "go-queue": "go"}`` version_added: 3.1.7 type: string example: '{"foo": "java", "bar": "java", "go-queue": "go"}' From 2cf30ead60e5ffa76c6387c533615da9a4836960 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:38:56 +0800 Subject: [PATCH 39/75] CI: Skip sdk-java provider in compat tests for older Airflow apache-airflow-providers-sdk-java requires apache-airflow>=3.3.0, so installing it against the 2.11.1 / 3.0.6 / 3.1.8 / 3.2.1 compat targets fails dependency resolution. Add it to remove-providers for each older-Airflow row in PROVIDERS_COMPATIBILITY_TESTS_MATRIX. Also silence mypy no-redef on dev/registry tomli fallback imports, which now trip the mypy-dev hook because tomli is resolvable in the mypy environment after recent uv.lock updates. --- dev/breeze/src/airflow_breeze/global_constants.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dev/breeze/src/airflow_breeze/global_constants.py b/dev/breeze/src/airflow_breeze/global_constants.py index 80ea5e6cb7e3d..8be1ff9724ed9 100644 --- a/dev/breeze/src/airflow_breeze/global_constants.py +++ b/dev/breeze/src/airflow_breeze/global_constants.py @@ -789,25 +789,25 @@ def get_airflow_extras(): { "python-version": "3.10", "airflow-version": "2.11.1", - "remove-providers": "common.messaging edge3 fab git keycloak informatica common.ai opensearch", + "remove-providers": "common.messaging edge3 fab git keycloak informatica common.ai opensearch sdk.java", "run-unit-tests": "true", }, { "python-version": "3.10", "airflow-version": "3.0.6", - "remove-providers": "", + "remove-providers": "sdk.java", "run-unit-tests": "true", }, { "python-version": "3.10", "airflow-version": "3.1.8", - "remove-providers": "", + "remove-providers": "sdk.java", "run-unit-tests": "true", }, { "python-version": "3.10", "airflow-version": "3.2.1", - "remove-providers": "", + "remove-providers": "sdk.java", "run-unit-tests": "true", }, ] From f98bc1d706d518fd54b174254d25997ea602b482 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:47:00 +0800 Subject: [PATCH 40/75] CI: Fix MyPy Liskov violation in JavaCoordinator.task_execution_cmd Import TaskInstanceDTO from the same airflow.sdk._shared.workloads namespace that BaseCoordinator uses. The previous import via airflow._shared.workloads pointed at the same physical file via a symlink but mypy treated the two namespaces as distinct types, flagging the override as a Liskov violation. --- .../sdk/java/src/airflow/providers/sdk/java/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py b/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py index ba70714cc313a..e5f31b29f7575 100644 --- a/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py +++ b/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py @@ -29,7 +29,7 @@ from airflow.sdk.execution_time.coordinator import BaseCoordinator if TYPE_CHECKING: - from airflow._shared.workloads import TaskInstanceDTO + from airflow.sdk._shared.workloads import TaskInstanceDTO from airflow.sdk.api.datamodels._generated import BundleInfo From 372bc9c355928ced61903e5f681587634436afcc Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:55:36 +0800 Subject: [PATCH 41/75] CI: Fix sdk-java docs build warnings * Add 'sdk' to empty_subpackages in provider_conf so the autoapi- generated _api/airflow/providers/sdk/index.rst is excluded the same way the other namespace-only directories are. Without this, Sphinx warned that the document was not in any toctree. * Fix the relative include paths in security.rst and installing- providers-from-sources.rst. Nested providers (those under a namespace package like sdk/) sit one directory deeper than flat providers, so the include needs four ../ segments instead of three to reach devel-common/src/sphinx_exts/includes/. --- devel-common/src/docs/provider_conf.py | 2 +- providers/sdk/java/docs/installing-providers-from-sources.rst | 2 +- providers/sdk/java/docs/security.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/devel-common/src/docs/provider_conf.py b/devel-common/src/docs/provider_conf.py index 6bc9da15f5f61..b730e8f20a417 100644 --- a/devel-common/src/docs/provider_conf.py +++ b/devel-common/src/docs/provider_conf.py @@ -151,7 +151,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -empty_subpackages = ["apache", "atlassian", "common", "cncf", "dbt", "microsoft"] +empty_subpackages = ["apache", "atlassian", "common", "cncf", "dbt", "microsoft", "sdk"] exclude_patterns = [ "operators/_partials", "_api/airflow/index.rst", diff --git a/providers/sdk/java/docs/installing-providers-from-sources.rst b/providers/sdk/java/docs/installing-providers-from-sources.rst index a72b45ffaa6e8..fdbb17d017579 100644 --- a/providers/sdk/java/docs/installing-providers-from-sources.rst +++ b/providers/sdk/java/docs/installing-providers-from-sources.rst @@ -15,4 +15,4 @@ specific language governing permissions and limitations under the License. -.. include:: /../../../devel-common/src/sphinx_exts/includes/installing-providers-from-sources.rst +.. include:: /../../../../devel-common/src/sphinx_exts/includes/installing-providers-from-sources.rst diff --git a/providers/sdk/java/docs/security.rst b/providers/sdk/java/docs/security.rst index 15a0ebbb2d054..351ff007ebf2f 100644 --- a/providers/sdk/java/docs/security.rst +++ b/providers/sdk/java/docs/security.rst @@ -15,4 +15,4 @@ specific language governing permissions and limitations under the License. -.. include:: /../../../devel-common/src/sphinx_exts/includes/security.rst +.. include:: /../../../../devel-common/src/sphinx_exts/includes/security.rst From 570ac1bcf21b9e7324fb3414e1ff73507328b9dc Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 29 Apr 2026 16:44:31 +0800 Subject: [PATCH 42/75] CI: Update SDK Java configuration and documentation references --- .../src/airflow/config_templates/config.yml | 2 +- .../src/tests_common/pytest_plugin.py | 4 +++- .../sdk/java/docs/configurations-ref.rst | 19 +++++++++++++++++++ providers/sdk/java/docs/index.rst | 7 +++++++ 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 providers/sdk/java/docs/configurations-ref.rst diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml index 5fba029a0a126..b2e1e7eeef819 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -2018,7 +2018,7 @@ sdk: version_added: 3.1.7 type: string example: '{"foo": "java", "bar": "java", "go-queue": "go"}' - default: "{{}}" + default: ~ api_auth: description: Settings relating to authentication on the Airflow APIs options: diff --git a/devel-common/src/tests_common/pytest_plugin.py b/devel-common/src/tests_common/pytest_plugin.py index 9911b07b8c14a..457a31495e019 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -2551,7 +2551,9 @@ def _create_task_instance( if AIRFLOW_V_3_3_PLUS: from airflow.sdk._shared.workloads import TaskInstanceDTO else: - from airflow.executors.workloads.task import TaskInstanceDTO # type: ignore[no-redef,assignment] + from airflow.sdk.api.datamodels._generated import ( # type: ignore[no-redef,assignment] + TaskInstance as TaskInstanceDTO, + ) from airflow.sdk.api.datamodels._generated import DagRun, DagRunState, TIRunContext from airflow.utils.types import DagRunType diff --git a/providers/sdk/java/docs/configurations-ref.rst b/providers/sdk/java/docs/configurations-ref.rst new file mode 100644 index 0000000000000..ea8e668d75793 --- /dev/null +++ b/providers/sdk/java/docs/configurations-ref.rst @@ -0,0 +1,19 @@ + .. 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. + +.. include:: /../../../../devel-common/src/sphinx_exts/includes/providers-configurations-ref.rst +.. include:: /../../../../devel-common/src/sphinx_exts/includes/sections-and-options.rst diff --git a/providers/sdk/java/docs/index.rst b/providers/sdk/java/docs/index.rst index 7d4854e09b430..77e8b1e22d80e 100644 --- a/providers/sdk/java/docs/index.rst +++ b/providers/sdk/java/docs/index.rst @@ -29,6 +29,13 @@ The SDK: Java provider registers Java-specific task coordinator and DAG file pro Changelog Security +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Guides + + Configuration + .. toctree:: :hidden: :maxdepth: 1 From 91b2291e15d6655925b489be85dde9983f11dd9e Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 29 Apr 2026 21:13:09 +0800 Subject: [PATCH 43/75] CI: Update map_index handling and add fixture to restore process context in tests --- devel-common/src/tests_common/pytest_plugin.py | 2 +- .../task_sdk/execution_time/test_coordinator.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/devel-common/src/tests_common/pytest_plugin.py b/devel-common/src/tests_common/pytest_plugin.py index 457a31495e019..6873fd99ad60e 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -2638,7 +2638,7 @@ def _create_task_instance( dag_id=dag_id, run_id=run_id, try_number=try_number, - map_index=map_index if map_index is not None else -1, + map_index=map_index, # type: ignore[arg-type] dag_version_id=uuid7(), pool_slots=1, queue="default", diff --git a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py index 5f4a878af8bef..082cfaf6051b9 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py +++ b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py @@ -21,6 +21,7 @@ import os import socket import subprocess +from pathlib import Path from unittest.mock import MagicMock, patch import pytest @@ -358,6 +359,20 @@ def test_run_task_execution_creates_task_execution_info(self, mock_entrypoint): class TestRuntimeSubprocessEntrypoint: + @pytest.fixture(autouse=True) + def _restore_process_context_env(self): + """``_runtime_subprocess_entrypoint`` runs inside a forked child in production + and sets ``_AIRFLOW_PROCESS_CONTEXT`` for the runtime subprocess. When tests + invoke it in-process, the env var leaks into other tests — restore it.""" + old = os.environ.get("_AIRFLOW_PROCESS_CONTEXT") + try: + yield + finally: + if old is None: + os.environ.pop("_AIRFLOW_PROCESS_CONTEXT", None) + else: + os.environ["_AIRFLOW_PROCESS_CONTEXT"] = old + def test_unknown_entrypoint_info_type_raises(self): class TestCoordinator(BaseCoordinator): sdk = "test" @@ -476,7 +491,7 @@ def test_task_execution_flow( # Mock resolved bundle mock_bundle_instance = MagicMock() - mock_bundle_instance.path = "/resolved/bundles/test-bundle" + mock_bundle_instance.path = Path("/resolved/bundles/test-bundle") mock_resolve_bundle.return_value = mock_bundle_instance # BundleVersionLock as context manager From 9d05bed7bebd41ade37d8810a9ec22bdd3dba64a Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 30 Apr 2026 10:29:21 +0800 Subject: [PATCH 44/75] CI: Refactor map_index handling and update time travel decorators for timezone awareness in tests --- providers/sdk/java/tests/unit/sdk/java/test_coordinator.py | 6 +++--- task-sdk/tests/task_sdk/execution_time/test_task_runner.py | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py b/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py index 2c42574dd3ed0..f57472fc5e69a 100644 --- a/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py +++ b/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py @@ -169,7 +169,7 @@ def test_pure_java_dag(self, tmp_path: Path): bundle_info = BundleInfo(name="my_bundle") cmd = JavaCoordinator.task_execution_cmd( - what=ti, + what=ti, # type: ignore[arg-type] dag_file_path=str(jar), bundle_path=bundle_path, bundle_info=bundle_info, @@ -199,7 +199,7 @@ def test_python_stub_dag_with_bundles_folder(self, tmp_path: Path): return_value=str(bundles_folder), ): cmd = JavaCoordinator.task_execution_cmd( - what=ti, + what=ti, # type: ignore[arg-type] dag_file_path="/dags/stub_dag.py", bundle_path="/some/bundle/path", bundle_info=bundle_info, @@ -233,7 +233,7 @@ def test_python_stub_dag_invalid_config_raises(self, config_value): ): with pytest.raises(ValueError, match="bundles_folder config must be set"): JavaCoordinator.task_execution_cmd( - what=ti, + what=ti, # type: ignore[arg-type] dag_file_path="/dags/stub_dag.py", bundle_path="/some/bundle/path", bundle_info=bundle_info, diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py index d1b6a0c39db09..7ed02ce5e78d7 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py +++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py @@ -2256,8 +2256,10 @@ def execute(self, context): test_task_id = "pull_task" task = CustomOperator(task_id=test_task_id) - # In case of the specific map_index or None we should check it is passed to TI - extra_for_ti = {"map_index": map_indexes} if map_indexes in (1, None) else {} + # In case of the specific map_index we should check it is passed to TI. + # ``None`` is not a valid TaskInstanceDTO.map_index value, but xcom_pull's + # behaviour with ``map_indexes=None`` is independent of the TI's own map_index. + extra_for_ti = {"map_index": map_indexes} if isinstance(map_indexes, int) else {} runtime_ti = create_runtime_ti(task=task, **extra_for_ti) ser_value = BaseXCom.serialize_value(xcom_values) From 9a32fbfe9dcd609ac7c970288fc566584e16b16c Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 30 Apr 2026 13:25:28 +0800 Subject: [PATCH 45/75] CI: Replace TaskInstance with TaskInstanceDTO and add additional parameters in test_supervisor --- .../execution_time/test_supervisor.py | 108 ++++++++++++++---- 1 file changed, 86 insertions(+), 22 deletions(-) diff --git a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py index 952e85ba97915..4dfaa77dfd478 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py +++ b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py @@ -65,7 +65,6 @@ DagRunState, DagRunType, PreviousTIResponse, - TaskInstance, TaskInstanceState, ) from airflow.sdk.exceptions import AirflowRuntimeError, ErrorType, TaskAlreadyRunningError @@ -232,13 +231,16 @@ def test_supervise( """ Test that the supervisor validates server URL and dry_run parameter combinations correctly. """ - ti = TaskInstance( + ti = TaskInstanceDTO( id=uuid7(), task_id="async", dag_id="super_basic_deferred_run", run_id="d", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ) bundle_info = BundleInfo(name="my-bundle", version=None) @@ -325,13 +327,16 @@ def subprocess_main(): proc = ActivitySubprocess.start( dag_rel_path=os.devnull, bundle_info=FAKE_BUNDLE, - what=TaskInstance( + what=TaskInstanceDTO( id="4d828a62-a417-4936-a7a6-2b3fabacecab", task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=client_with_ti_start, target=subprocess_main, @@ -400,13 +405,16 @@ def subprocess_main(): proc = ActivitySubprocess.start( dag_rel_path=os.devnull, bundle_info=FAKE_BUNDLE, - what=TaskInstance( + what=TaskInstanceDTO( id="4d828a62-a417-4936-a7a6-2b3fabacecab", task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=client_with_ti_start, target=subprocess_main, @@ -497,13 +505,16 @@ def on_kill(self) -> None: proc = ActivitySubprocess.start( dag_rel_path=os.devnull, bundle_info=FAKE_BUNDLE, - what=TaskInstance( + what=TaskInstanceDTO( id=ti_id, task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=make_client(transport=httpx.MockTransport(handle_request)), target=subprocess_main, @@ -526,13 +537,16 @@ def subprocess_main(): proc = ActivitySubprocess.start( dag_rel_path=os.devnull, bundle_info=FAKE_BUNDLE, - what=TaskInstance( + what=TaskInstanceDTO( id="4d828a62-a417-4936-a7a6-2b3fabacecab", task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=client_with_ti_start, target=subprocess_main, @@ -561,8 +575,16 @@ def subprocess_main(): proc = ActivitySubprocess.start( dag_rel_path=os.devnull, bundle_info=FAKE_BUNDLE, - what=TaskInstance( - id=uuid7(), task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7() + what=TaskInstanceDTO( + id=uuid7(), + task_id="b", + dag_id="c", + run_id="d", + try_number=1, + dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=mock_client, target=subprocess_main, @@ -600,13 +622,16 @@ def test_resume_start_date_from_context(self, mocker, make_ti_context, start_dat proc = ActivitySubprocess.start( dag_rel_path=os.devnull, bundle_info=FAKE_BUNDLE, - what=TaskInstance( + what=TaskInstanceDTO( id=uuid7(), task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=mock_client, target=lambda: None, @@ -712,13 +737,16 @@ def test_run_simple_dag(self, test_dags_dir, captured_logs, time_machine, mocker time_machine.move_to(instant, tick=False) dagfile_path = test_dags_dir - ti = TaskInstance( + ti = TaskInstanceDTO( id=uuid7(), task_id="hello", dag_id="super_basic_run", run_id="c", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ) bundle_info = BundleInfo(name="my-bundle", version=None) @@ -753,13 +781,16 @@ def test_supervise_handles_deferred_task( """ instant = timezone.datetime(2024, 11, 7, 12, 34, 56, 0) - ti = TaskInstance( + ti = TaskInstanceDTO( id=uuid7(), task_id="async", dag_id="super_basic_deferred_run", run_id="d", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ) # Create a mock client to assert calls to the client @@ -880,8 +911,16 @@ def handle_request(request: httpx.Request) -> httpx.Response: proc = ActivitySubprocess.start( dag_rel_path=os.devnull, - what=TaskInstance( - id=ti_id, task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7() + what=TaskInstanceDTO( + id=ti_id, + task_id="b", + dag_id="c", + run_id="d", + try_number=1, + dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=make_client(transport=httpx.MockTransport(handle_request)), target=subprocess_main, @@ -958,8 +997,16 @@ def subprocess_main(): ActivitySubprocess.start( dag_rel_path=os.devnull, bundle_info=FAKE_BUNDLE, - what=TaskInstance( - id=ti_id, task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7() + what=TaskInstanceDTO( + id=ti_id, + task_id="b", + dag_id="c", + run_id="d", + try_number=1, + dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=make_client(transport=httpx.MockTransport(handle_request)), target=subprocess_main, @@ -1163,13 +1210,16 @@ def subprocess_main(): proc = ActivitySubprocess.start( dag_rel_path=os.devnull, bundle_info=FAKE_BUNDLE, - what=TaskInstance( + what=TaskInstanceDTO( id="4d828a62-a417-4936-a7a6-2b3fabacecab", task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=client_with_ti_start, target=subprocess_main, @@ -1320,8 +1370,16 @@ def _handler(sig, frame): proc = ActivitySubprocess.start( dag_rel_path=os.devnull, bundle_info=FAKE_BUNDLE, - what=TaskInstance( - id=ti_id, task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7() + what=TaskInstanceDTO( + id=ti_id, + task_id="b", + dag_id="c", + run_id="d", + try_number=1, + dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=client_with_ti_start, target=subprocess_main, @@ -3333,13 +3391,16 @@ def execute(self, context: Context): task.dag = DAG(dag_id="test_dag") # Create a simple TaskInstance datamodel to pass to the supervisor - ti = TaskInstance( + ti = TaskInstanceDTO( id=uuid7(), - task_id=task.task_id, + dag_version_id=uuid7(), dag_id="test_dag", + task_id=task.task_id, run_id="r", try_number=1, - dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ) # Patch the API client used by InProcessTestSupervisor to return a predictable TI context @@ -3756,13 +3817,16 @@ def subprocess_main(): proc = ActivitySubprocess.start( dag_rel_path=os.devnull, bundle_info=FAKE_BUNDLE, - what=TaskInstance( + what=TaskInstanceDTO( id="4d828a62-a417-4936-a7a6-2b3fabacecab", task_id="b", dag_id="c", run_id="d", try_number=1, dag_version_id=uuid7(), + pool_slots=1, + queue="default", + priority_weight=1, ), client=client_with_ti_start, target=subprocess_main, From 894ac55ee6a5b427a4b5f2fe13d4704a60b660fc Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 30 Apr 2026 14:37:46 +0800 Subject: [PATCH 46/75] CI: Update test cases to use -1 for map_index and add additional parameters in TestCommsDecoder --- task-sdk/tests/task_sdk/definitions/test_mappedoperator.py | 4 ++-- task-sdk/tests/task_sdk/definitions/test_xcom_arg.py | 2 +- task-sdk/tests/task_sdk/execution_time/test_comms.py | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/task-sdk/tests/task_sdk/definitions/test_mappedoperator.py b/task-sdk/tests/task_sdk/definitions/test_mappedoperator.py index 2b34fac6ea0f9..93c5cc19aed47 100644 --- a/task-sdk/tests/task_sdk/definitions/test_mappedoperator.py +++ b/task-sdk/tests/task_sdk/definitions/test_mappedoperator.py @@ -680,14 +680,14 @@ def mock_comms_response(msg): ("tg.t2", 0): ["a", "b"], ("tg.t2", 1): [4], ("tg.t2", 2): ["z"], - ("t3", None): [["a", "b"], [4], ["z"]], + ("t3", -1): [["a", "b"], [4], ["z"]], } # We hard-code the number of expansions here as the server is in charge of that. expansion_per_task_id = { "tg.t1": range(3), "tg.t2": range(3), - "t3": [None], + "t3": [-1], } for task in dag.tasks: for map_index in expansion_per_task_id[task.task_id]: diff --git a/task-sdk/tests/task_sdk/definitions/test_xcom_arg.py b/task-sdk/tests/task_sdk/definitions/test_xcom_arg.py index af487851b07cb..6014d26f2208b 100644 --- a/task-sdk/tests/task_sdk/definitions/test_xcom_arg.py +++ b/task-sdk/tests/task_sdk/definitions/test_xcom_arg.py @@ -344,7 +344,7 @@ def xcom_get(msg): mock_supervisor_comms.send.side_effect = xcom_get # Run "pull_one" and "pull_all". - assert run_ti(dag, "pull_all", None) == TaskInstanceState.SUCCESS + assert run_ti(dag, "pull_all", -1) == TaskInstanceState.SUCCESS assert all_results == ["a", "b", "c", 1, 2] states = [run_ti(dag, "pull_one", map_index) for map_index in range(5)] diff --git a/task-sdk/tests/task_sdk/execution_time/test_comms.py b/task-sdk/tests/task_sdk/execution_time/test_comms.py index 5c6d88439250c..37a91dd0ecc28 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_comms.py +++ b/task-sdk/tests/task_sdk/execution_time/test_comms.py @@ -86,6 +86,9 @@ def test_recv_StartupDetails(self): "run_id": "b", "dag_id": "c", "dag_version_id": uuid.UUID("4d828a62-a417-4936-a7a6-2b3fabacecab"), + "pool_slots": 1, + "queue": "default", + "priority_weight": 1, }, "ti_context": { "dag_run": { From 93725b9458b8ea00b63af92af2c59b7a0ee7dc6c Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 30 Apr 2026 18:58:24 +0800 Subject: [PATCH 47/75] Remove shared workloads dependency and refactor TaskInstanceDTO usage - Removed the shared workloads dependency from pyproject.toml and related files. - Deleted the workloads directory and its references in the codebase. - Refactored imports of TaskInstanceDTO to point to the new location in execution_time.workloads.task. - Introduced new files for TaskInstanceDTO and its base class in the execution_time module. - Updated tests to reflect the changes in TaskInstanceDTO imports. --- airflow-core/pyproject.toml | 2 - airflow-core/src/airflow/_shared/workloads | 1 - .../src/airflow/executors/base_executor.py | 3 +- .../src/airflow/executors/workloads/task.py | 2 - .../src/tests_common/pytest_plugin.py | 2 +- .../airflow/providers/sdk/java/coordinator.py | 2 +- .../tests/unit/sdk/java/test_coordinator.py | 2 +- pyproject.toml | 5 - shared/workloads/.gitignore | 1 - shared/workloads/.pre-commit-config.yaml | 31 - shared/workloads/pyproject.toml | 55 - .../src/airflow_shared/workloads/__init__.py | 46 - shared/workloads/tests/conftest.py | 22 - shared/workloads/tests/workloads/__init__.py | 17 - .../tests/workloads/test_task_instance_dto.py | 56 - shared/workloads/uv.lock | 2125 ----------------- task-sdk/pyproject.toml | 2 - task-sdk/src/airflow/sdk/_shared/workloads | 1 - .../src/airflow/sdk/execution_time/comms.py | 5 +- .../airflow/sdk/execution_time/coordinator.py | 2 +- .../airflow/sdk/execution_time/supervisor.py | 2 +- .../execution_time/test_supervisor.py | 2 +- .../execution_time/test_task_runner.py | 1 + 23 files changed, 13 insertions(+), 2374 deletions(-) delete mode 120000 airflow-core/src/airflow/_shared/workloads delete mode 100644 shared/workloads/.gitignore delete mode 100644 shared/workloads/.pre-commit-config.yaml delete mode 100644 shared/workloads/pyproject.toml delete mode 100644 shared/workloads/src/airflow_shared/workloads/__init__.py delete mode 100644 shared/workloads/tests/conftest.py delete mode 100644 shared/workloads/tests/workloads/__init__.py delete mode 100644 shared/workloads/tests/workloads/test_task_instance_dto.py delete mode 100644 shared/workloads/uv.lock delete mode 120000 task-sdk/src/airflow/sdk/_shared/workloads diff --git a/airflow-core/pyproject.toml b/airflow-core/pyproject.toml index f89c0f88d8155..664ff7d905f97 100644 --- a/airflow-core/pyproject.toml +++ b/airflow-core/pyproject.toml @@ -250,7 +250,6 @@ exclude = [ "../shared/serialization/src/airflow_shared/serialization" = "src/airflow/_shared/serialization" "../shared/state/src/airflow_shared/state" = "src/airflow/_shared/state" "../shared/timezones/src/airflow_shared/timezones" = "src/airflow/_shared/timezones" -"../shared/workloads/src/airflow_shared/workloads" = "src/airflow/_shared/workloads" "../shared/listeners/src/airflow_shared/listeners" = "src/airflow/_shared/listeners" "../shared/plugins_manager/src/airflow_shared/plugins_manager" = "src/airflow/_shared/plugins_manager" "../shared/providers_discovery/src/airflow_shared/providers_discovery" = "src/airflow/_shared/providers_discovery" @@ -340,7 +339,6 @@ shared_distributions = [ "apache-airflow-shared-serialization", "apache-airflow-shared-state", "apache-airflow-shared-timezones", - "apache-airflow-shared-workloads", "apache-airflow-shared-plugins-manager", "apache-airflow-shared-providers-discovery", ] diff --git a/airflow-core/src/airflow/_shared/workloads b/airflow-core/src/airflow/_shared/workloads deleted file mode 120000 index f25d9e16ea1d9..0000000000000 --- a/airflow-core/src/airflow/_shared/workloads +++ /dev/null @@ -1 +0,0 @@ -../../../../shared/workloads/src/airflow_shared/workloads \ No newline at end of file diff --git a/airflow-core/src/airflow/executors/base_executor.py b/airflow-core/src/airflow/executors/base_executor.py index 1d223c567616a..5080e75750a48 100644 --- a/airflow-core/src/airflow/executors/base_executor.py +++ b/airflow-core/src/airflow/executors/base_executor.py @@ -652,9 +652,10 @@ def run_workload( if isinstance(workload, ExecuteTask): from airflow.sdk.execution_time.supervisor import supervise_task + from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO as SDKTaskInstanceDTO return supervise_task( - ti=workload.ti, + ti=SDKTaskInstanceDTO.model_validate(workload.ti, from_attributes=True), bundle_info=workload.bundle_info, dag_rel_path=workload.dag_rel_path, token=workload.token, diff --git a/airflow-core/src/airflow/executors/workloads/task.py b/airflow-core/src/airflow/executors/workloads/task.py index 4c563f34351d8..9af3f33c10efd 100644 --- a/airflow-core/src/airflow/executors/workloads/task.py +++ b/airflow-core/src/airflow/executors/workloads/task.py @@ -28,8 +28,6 @@ from airflow.utils.state import TaskInstanceState if TYPE_CHECKING: - import uuid - from airflow.api_fastapi.auth.tokens import JWTGenerator from airflow.models.taskinstance import TaskInstance as TIModel from airflow.models.taskinstancekey import TaskInstanceKey diff --git a/devel-common/src/tests_common/pytest_plugin.py b/devel-common/src/tests_common/pytest_plugin.py index 6873fd99ad60e..defee35f0e30c 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -2549,7 +2549,7 @@ def _create_task_instance( from tests_common.test_utils.version_compat import AIRFLOW_V_3_3_PLUS if AIRFLOW_V_3_3_PLUS: - from airflow.sdk._shared.workloads import TaskInstanceDTO + from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO else: from airflow.sdk.api.datamodels._generated import ( # type: ignore[no-redef,assignment] TaskInstance as TaskInstanceDTO, diff --git a/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py b/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py index e5f31b29f7575..11833f166ce4b 100644 --- a/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py +++ b/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py @@ -29,8 +29,8 @@ from airflow.sdk.execution_time.coordinator import BaseCoordinator if TYPE_CHECKING: - from airflow.sdk._shared.workloads import TaskInstanceDTO from airflow.sdk.api.datamodels._generated import BundleInfo + from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO class JavaCoordinator(BaseCoordinator): diff --git a/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py b/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py index f57472fc5e69a..d5dc053a822bf 100644 --- a/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py +++ b/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py @@ -25,7 +25,6 @@ import pytest import yaml -from airflow._shared.workloads import TaskInstanceDTO from airflow.providers.sdk.java.bundle_scanner import ( MAIN_CLASS_MANIFEST_KEY, MANIFEST_PATH, @@ -34,6 +33,7 @@ ) from airflow.providers.sdk.java.coordinator import JavaCoordinator from airflow.sdk.api.datamodels._generated import BundleInfo +from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO from tests_common.test_utils.version_compat import AIRFLOW_V_3_3_PLUS diff --git a/pyproject.toml b/pyproject.toml index b6b19605a41b6..9934271e31d10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1351,7 +1351,6 @@ dev = [ "apache-airflow-shared-state", "apache-airflow-shared-template-rendering", "apache-airflow-shared-timezones", - "apache-airflow-shared-workloads", ] # To build docs: @@ -1533,7 +1532,6 @@ apache-airflow-shared-serialization = false apache-airflow-shared-state = false apache-airflow-shared-template-rendering = false apache-airflow-shared-timezones = false -apache-airflow-shared-workloads = false apache-airflow-task-sdk = false apache-airflow-task-sdk-integration-tests = false apache-aurflow-docker-stack = false @@ -1685,7 +1683,6 @@ apache-airflow-shared-serialization = false apache-airflow-shared-state = false apache-airflow-shared-template-rendering = false apache-airflow-shared-timezones = false -apache-airflow-shared-workloads = false apache-airflow-task-sdk = false apache-airflow-task-sdk-integration-tests = false apache-aurflow-docker-stack = false @@ -1731,7 +1728,6 @@ apache-airflow-shared-serialization = { workspace = true } apache-airflow-shared-state = { workspace = true } apache-airflow-shared-template-rendering = { workspace = true } apache-airflow-shared-timezones = { workspace = true } -apache-airflow-shared-workloads = { workspace = true } # Automatically generated provider workspace items (update_airflow_pyproject_toml.py) apache-airflow-providers-airbyte = { workspace = true } apache-airflow-providers-akeyless = { workspace = true } @@ -1870,7 +1866,6 @@ members = [ "shared/state", "shared/template_rendering", "shared/timezones", - "shared/workloads", # Automatically generated provider workspace members (update_airflow_pyproject_toml.py) "providers/airbyte", "providers/akeyless", diff --git a/shared/workloads/.gitignore b/shared/workloads/.gitignore deleted file mode 100644 index bff2d7629604d..0000000000000 --- a/shared/workloads/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.iml diff --git a/shared/workloads/.pre-commit-config.yaml b/shared/workloads/.pre-commit-config.yaml deleted file mode 100644 index 7ce635750ca3c..0000000000000 --- a/shared/workloads/.pre-commit-config.yaml +++ /dev/null @@ -1,31 +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. ---- -default_stages: [pre-commit, pre-push] -minimum_prek_version: '0.3.4' -default_language_version: - python: python3 -repos: - - repo: local - hooks: - - id: mypy-shared-workloads - name: Run mypy for shared-workloads - language: python - entry: ../../scripts/ci/prek/run_mypy_full_dist_local_venv_or_breeze_in_ci.py shared/workloads - pass_filenames: false - files: ^.*\.py$ - require_serial: true diff --git a/shared/workloads/pyproject.toml b/shared/workloads/pyproject.toml deleted file mode 100644 index 1a1c1dded04be..0000000000000 --- a/shared/workloads/pyproject.toml +++ /dev/null @@ -1,55 +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. - -[project] -name = "apache-airflow-shared-workloads" -description = "Shared workload data transfer objects for Airflow distributions" -version = "0.0" -classifiers = [ - "Private :: Do Not Upload", -] - -dependencies = [ - "pydantic>=2.10.0", -] - -[dependency-groups] -dev = [ - "apache-airflow-devel-common", -] - -[build-system] -requires = [ - "hatchling==1.29.0", - "packaging==26.0", - "pathspec==1.0.4", - "pluggy==1.6.0", - "tomli==2.4.1; python_version < '3.11'", - "trove-classifiers==2026.1.14.14", -] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["src/airflow_shared"] - -[tool.ruff] -extend = "../../pyproject.toml" -src = ["src"] - -[tool.ruff.lint.per-file-ignores] -# Ignore Doc rules et al for anything outside of tests -"!src/*" = ["D", "S101", "TRY002"] diff --git a/shared/workloads/src/airflow_shared/workloads/__init__.py b/shared/workloads/src/airflow_shared/workloads/__init__.py deleted file mode 100644 index 1a30b11c4b6af..0000000000000 --- a/shared/workloads/src/airflow_shared/workloads/__init__.py +++ /dev/null @@ -1,46 +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. -"""Shared workload data transfer objects for Airflow distributions.""" - -from __future__ import annotations - -import uuid - -from pydantic import BaseModel, Field - - -class TaskInstanceDTO(BaseModel): - """Schema for TaskInstance with minimal required fields needed for Executors and Task SDK.""" - - id: uuid.UUID - dag_version_id: uuid.UUID - task_id: str - dag_id: str - run_id: str - try_number: int - map_index: int = -1 - - pool_slots: int - queue: str - priority_weight: int - executor_config: dict | None = Field(default=None, exclude=True) - - parent_context_carrier: dict | None = None - context_carrier: dict | None = None - - -__all__ = ["TaskInstanceDTO"] diff --git a/shared/workloads/tests/conftest.py b/shared/workloads/tests/conftest.py deleted file mode 100644 index 8b61b1b99b90d..0000000000000 --- a/shared/workloads/tests/conftest.py +++ /dev/null @@ -1,22 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from __future__ import annotations - -import os - -os.environ["_AIRFLOW__AS_LIBRARY"] = "true" diff --git a/shared/workloads/tests/workloads/__init__.py b/shared/workloads/tests/workloads/__init__.py deleted file mode 100644 index 217e5db960782..0000000000000 --- a/shared/workloads/tests/workloads/__init__.py +++ /dev/null @@ -1,17 +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. diff --git a/shared/workloads/tests/workloads/test_task_instance_dto.py b/shared/workloads/tests/workloads/test_task_instance_dto.py deleted file mode 100644 index 4788386dd6861..0000000000000 --- a/shared/workloads/tests/workloads/test_task_instance_dto.py +++ /dev/null @@ -1,56 +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. -"""Tests for shared TaskInstanceDTO.""" - -from __future__ import annotations - -import uuid - -import pytest - -from airflow_shared.workloads import TaskInstanceDTO - - -@pytest.fixture -def minimal_ti_kwargs(): - return { - "id": uuid.uuid4(), - "dag_version_id": uuid.uuid4(), - "task_id": "my_task", - "dag_id": "my_dag", - "run_id": "run_1", - "try_number": 1, - "pool_slots": 1, - "queue": "default", - "priority_weight": 1, - } - - -class TestTaskInstanceDTO: - """Test the shared TaskInstanceDTO model.""" - - def test_create_with_required_fields(self, minimal_ti_kwargs): - ti = TaskInstanceDTO(**minimal_ti_kwargs) - assert ti is not None - assert ti.task_id == "my_task" - assert ti.dag_id == "my_dag" - assert ti.run_id == "run_1" - assert ti.try_number == 1 - assert ti.pool_slots == 1 - assert ti.queue == "default" - assert ti.priority_weight == 1 diff --git a/shared/workloads/uv.lock b/shared/workloads/uv.lock deleted file mode 100644 index 60097a87185e6..0000000000000 --- a/shared/workloads/uv.lock +++ /dev/null @@ -1,2125 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.12" - -[options] -exclude-newer = "2026-04-17T08:44:15.45316Z" -exclude-newer-span = "P4D" - -[options.exclude-newer-package] -apache-airflow-providers-informatica = false -apache-airflow-providers-amazon = false -apache-airflow-providers-elasticsearch = false -apache-airflow-providers-microsoft-winrm = false -apache-airflow-docker-tests = false -apache-airflow-providers = false -apache-airflow-providers-fab = false -apache-airflow-providers-openlineage = false -apache-airflow-providers-sftp = false -apache-airflow-e2e-tests = false -apache-airflow-shared-logging = false -apache-airflow-providers-apache-drill = false -apache-airflow-providers-pgvector = false -apache-airflow-providers-imap = false -apache-airflow-providers-qdrant = false -apache-airflow-providers-edge3 = false -apache-airflow-providers-neo4j = false -apache-airflow-providers-discord = false -apache-airflow-providers-opensearch = false -apache-airflow-providers-samba = false -apache-airflow-providers-arangodb = false -apache-airflow-providers-apache-spark = false -apache-airflow-providers-ftp = false -apache-airflow-helm-tests = false -apache-airflow-providers-jenkins = false -apache-airflow-shared-listeners = false -apache-airflow-providers-telegram = false -apache-airflow-shared-providers-discovery = false -apache-airflow-providers-celery = false -apache-airflow-providers-docker = false -apache-airflow-providers-sendgrid = false -apache-airflow-providers-common-ai = false -apache-airflow = false -apache-airflow-shared-observability = false -apache-airflow-dev = false -apache-airflow-providers-dbt-cloud = false -apache-airflow-providers-openfaas = false -apache-airflow-devel-common = false -apache-airflow-providers-apache-cassandra = false -apache-airflow-providers-asana = false -apache-airflow-providers-oracle = false -apache-airflow-providers-mysql = false -apache-airflow-providers-alibaba = false -apache-airflow-providers-microsoft-mssql = false -apache-airflow-providers-teradata = false -apache-airflow-providers-jdbc = false -apache-airflow-providers-common-io = false -apache-airflow-providers-cohere = false -apache-airflow-providers-pinecone = false -apache-airflow-providers-segment = false -apache-airflow-providers-redis = false -apache-airflow-shared-dagnode = false -apache-airflow-providers-apache-pinot = false -apache-airflow-providers-weaviate = false -apache-airflow-providers-salesforce = false -apache-airflow-providers-ssh = false -apache-airflow-providers-papermill = false -apache-airflow-providers-google = false -apache-airflow-providers-microsoft-psrp = false -apache-airflow-providers-vertica = false -apache-airflow-providers-apache-hdfs = false -apache-airflow-shared-template-rendering = false -apache-airflow-providers-http = false -apache-airflow-providers-slack = false -apache-airflow-providers-vespa = false -apache-airflow-providers-databricks = false -apache-airflow-providers-sqlite = false -apache-airflow-shared-module-loading = false -apache-airflow-providers-yandex = false -apache-airflow-shared-serialization = false -apache-airflow-scripts = false -apache-airflow-providers-exasol = false -apache-airflow-providers-mongo = false -apache-airflow-providers-apprise = false -apache-airflow-providers-apache-impala = false -apache-airflow-ctl = false -apache-airflow-providers-github = false -apache-airflow-providers-snowflake = false -apache-airflow-providers-zendesk = false -apache-airflow-providers-presto = false -apache-airflow-providers-airbyte = false -apache-airflow-providers-apache-hive = false -apache-airflow-kubernetes-tests = false -apache-airflow-providers-grpc = false -apache-airflow-providers-apache-druid = false -apache-airflow-providers-cncf-kubernetes = false -apache-airflow-providers-apache-flink = false -apache-airflow-providers-apache-pig = false -apache-airflow-providers-apache-tinkerpop = false -apache-airflow-shared-timezones = false -apache-airflow-providers-apache-iceberg = false -apache-airflow-breeze = false -apache-airflow-providers-opsgenie = false -apache-airflow-providers-apache-livy = false -apache-airflow-core = false -apache-airflow-providers-hashicorp = false -apache-airflow-providers-pagerduty = false -apache-airflow-providers-datadog = false -apache-airflow-providers-apache-kafka = false -apache-airflow-providers-influxdb = false -apache-airflow-providers-keycloak = false -apache-airflow-providers-trino = false -apache-airflow-providers-common-messaging = false -apache-airflow-providers-standard = false -apache-airflow-providers-languages-java = false -apache-airflow-providers-singularity = false -apache-airflow-providers-common-compat = false -apache-airflow-ctl-tests = false -apache-airflow-providers-tableau = false -apache-airflow-providers-common-sql = false -apache-airflow-shared-configuration = false -apache-airflow-providers-facebook = false -apache-airflow-providers-ydb = false -apache-airflow-providers-microsoft-azure = false -apache-airflow-shared-plugins-manager = false -apache-airflow-shared-secrets-backend = false -apache-airflow-shared-secrets-masker = false -apache-airflow-providers-git = false -apache-airflow-task-sdk = false -apache-airflow-providers-atlassian-jira = false -apache-airflow-providers-odbc = false -apache-airflow-providers-postgres = false -apache-airflow-providers-openai = false -apache-airflow-task-sdk-integration-tests = false -apache-airflow-providers-smtp = false -apache-airflow-providers-dingding = false -apache-airflow-providers-apache-kylin = false -apache-airflow-providers-cloudant = false -apache-aurflow-docker-stack = false - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.13.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, - { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, - { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, - { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, - { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, - { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, - { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, - { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, - { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, - { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, - { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, - { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, - { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, - { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, - { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, - { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, - { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, - { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, - { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, - { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, - { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, - { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, - { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, - { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, - { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, - { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, - { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, - { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, - { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, - { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, -] - -[[package]] -name = "aioresponses" -version = "0.7.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/03/532bbc645bdebcf3b6af3b25d46655259d66ce69abba7720b71ebfabbade/aioresponses-0.7.8.tar.gz", hash = "sha256:b861cdfe5dc58f3b8afac7b0a6973d5d7b2cb608dd0f6253d16b8ee8eaf6df11", size = 40253, upload-time = "2025-01-19T18:14:03.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b7/584157e43c98aa89810bc2f7099e7e01c728ecf905a66cf705106009228f/aioresponses-0.7.8-py2.py3-none-any.whl", hash = "sha256:b73bd4400d978855e55004b23a3a84cb0f018183bcf066a85ad392800b5b9a94", size = 12518, upload-time = "2025-01-19T18:13:59.633Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "apache-airflow-devel-common" -version = "0.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aioresponses" }, - { name = "black" }, - { name = "coverage" }, - { name = "filelock" }, - { name = "ipdb" }, - { name = "jmespath" }, - { name = "kgb" }, - { name = "mypy" }, - { name = "pdbr" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "pytest-custom-exit-code" }, - { name = "pytest-icdiff" }, - { name = "pytest-instafail" }, - { name = "pytest-mock" }, - { name = "pytest-rerunfailures" }, - { name = "pytest-timeouts" }, - { name = "pytest-unordered" }, - { name = "pytest-xdist" }, - { name = "requests-mock" }, - { name = "rich" }, - { name = "ruff" }, - { name = "semver" }, - { name = "time-machine" }, - { name = "types-aiofiles" }, - { name = "types-certifi" }, - { name = "types-croniter" }, - { name = "types-deprecated" }, - { name = "types-docutils" }, - { name = "types-markdown" }, - { name = "types-paramiko" }, - { name = "types-protobuf" }, - { name = "types-pymysql" }, - { name = "types-python-dateutil" }, - { name = "types-python-slugify" }, - { name = "types-pytz" }, - { name = "types-pyyaml" }, - { name = "types-redis" }, - { name = "types-requests" }, - { name = "types-setuptools" }, - { name = "types-tabulate" }, - { name = "types-toml" }, - { name = "wheel" }, - { name = "yamllint" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/cf/b1f97e6b6f2e57adda012429c541f1881610061e646945e75dfe7d811a9c/apache_airflow_devel_common-0.1.1.tar.gz", hash = "sha256:b066b2d3fdf940b4c2859af23036a7ef31f8e81694209b1ab0e056530e286232", size = 63846, upload-time = "2025-05-16T20:05:09.303Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/df/9f05b0e6fc132e59b5dec26f4ec610bc611b5f256495b7db25066f5b743b/apache_airflow_devel_common-0.1.1-py2.py3-none-any.whl", hash = "sha256:951ab206cf2f95768d08ad8c15b1cfaa7df404308c9e32998e3e16d217d2b032", size = 100909, upload-time = "2025-05-16T20:05:07.52Z" }, -] - -[[package]] -name = "apache-airflow-shared-workloads" -version = "0.0" -source = { editable = "." } -dependencies = [ - { name = "pydantic" }, -] - -[package.dev-dependencies] -dev = [ - { name = "apache-airflow-devel-common" }, -] - -[package.metadata] -requires-dist = [{ name = "pydantic", specifier = ">=2.10.0" }] - -[package.metadata.requires-dev] -dev = [{ name = "apache-airflow-devel-common" }] - -[[package]] -name = "asttokens" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, -] - -[[package]] -name = "attrs" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, -] - -[[package]] -name = "black" -version = "26.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pytokens" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, - { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, - { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, - { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, - { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, - { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, - { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, - { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, - { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, -] - -[[package]] -name = "certifi" -version = "2026.2.25" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, -] - -[[package]] -name = "click" -version = "8.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "coverage" -version = "7.13.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, - { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, - { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, - { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, - { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, - { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, - { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, - { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, - { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, - { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, - { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, - { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, - { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, - { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, - { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, - { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, - { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, - { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, - { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, - { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, - { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, - { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, - { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, - { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, -] - -[[package]] -name = "cryptography" -version = "46.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, - { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, - { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, - { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, - { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, - { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, - { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, - { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, - { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, - { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, - { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, - { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, - { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, - { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, -] - -[[package]] -name = "decorator" -version = "5.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, -] - -[[package]] -name = "execnet" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, -] - -[[package]] -name = "executing" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, -] - -[[package]] -name = "filelock" -version = "3.28.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/17/6e8890271880903e3538660a21d63a6c1fea969ac71d0d6b608b78727fa9/filelock-3.28.0.tar.gz", hash = "sha256:4ed1010aae813c4ee8d9c660e4792475ee60c4a0ba76073ceaf862bd317e3ca6", size = 56474, upload-time = "2026-04-14T22:54:33.625Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/21/2f728888c45033d34a417bfcd248ea2564c9e08ab1bfd301377cf05d5586/filelock-3.28.0-py3-none-any.whl", hash = "sha256:de9af6712788e7171df1b28b15eba2446c69721433fa427a9bee07b17820a9db", size = 39189, upload-time = "2026-04-14T22:54:32.037Z" }, -] - -[[package]] -name = "frozenlist" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, - { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, - { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, - { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, - { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, - { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, -] - -[[package]] -name = "icdiff" -version = "2.0.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/6d/41c8ff13b67c30ceb9325398ff6bb9fc4b77208c6c1d79db8d5913840bc6/icdiff-2.0.10.tar.gz", hash = "sha256:75a3de5c9af35ab45fb0504df59770c514a12c0d2b2c99e5f9c5c2429957e133", size = 16378, upload-time = "2026-02-08T15:03:26.76Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/4b/bb94514476624bfff977599e5ef3ac9aea603abb693639ed305b22ed1b35/icdiff-2.0.10-py3-none-any.whl", hash = "sha256:fd8ce592d94261c435e2a410f668a41ec3be4527878f8df01100797021d8edb0", size = 17103, upload-time = "2026-02-08T15:03:25.723Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - -[[package]] -name = "ipdb" -version = "0.13.13" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "decorator" }, - { name = "ipython" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/4c/b075da0092003d9a55cf2ecc1cae9384a1ca4f650d51b00fc59875fe76f6/ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4", size = 12130, upload-time = "2023-03-09T15:40:55.021Z" }, -] - -[[package]] -name = "ipython" -version = "9.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "ipython-pygments-lexers" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" }, -] - -[[package]] -name = "ipython-pygments-lexers" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, -] - -[[package]] -name = "jedi" -version = "0.19.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "parso" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, -] - -[[package]] -name = "jmespath" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, -] - -[[package]] -name = "kgb" -version = "7.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/00/9e56dee65ec791a92348fb54e8ced08c4c4db494b0f58cfb34737d087fb4/kgb-7.3.tar.gz", hash = "sha256:b8af7e79cb8b0df5a2ec596010b8e5d014845cfaa9203577b85b99d4df192927", size = 62922, upload-time = "2025-12-11T23:56:24.911Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/d6/1c81a1292fc50ad93d0b145f1c241ecb7d541fba4dcec7166e2e1d99f9cd/kgb-7.3-py2.py3-none-any.whl", hash = "sha256:0b300cd6d234a951f60e54ccda78c99a355393d6ae878d3d5925e726ae2f0450", size = 59662, upload-time = "2025-12-11T23:56:23.699Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, -] - -[[package]] -name = "matplotlib-inline" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "multidict" -version = "6.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, - { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, - { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, - { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, - { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, - { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, - { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, - { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, - { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, - { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, - { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, - { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, - { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, - { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, - { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, - { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, - { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, - { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, - { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, - { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, - { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, - { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, - { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, - { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, - { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, - { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, - { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, - { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, - { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, - { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, - { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, - { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, - { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, - { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, -] - -[[package]] -name = "mypy" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/1e/a587a862c766a755a58b62d8c00aed11b74a15dc415c1bf5da7b607b0efd/mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974", size = 2995901, upload-time = "2024-03-08T16:10:12.412Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/96/40f0f605b1d4e2ad1fb11d21988ce3a3e205886c0fcbd35c9789a214de9a/mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd", size = 10725390, upload-time = "2024-03-08T16:10:01.099Z" }, - { url = "https://files.pythonhosted.org/packages/d7/d2/072e40384b53051106b4fcf03537fb88e2a6ad0757d2ab7f6c8c2f188a69/mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6", size = 9731292, upload-time = "2024-03-08T16:08:48.463Z" }, - { url = "https://files.pythonhosted.org/packages/85/a5/b7dc7eb69eda899fd07e71403b51b598a1f4df0f452d1da5844374082bcd/mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185", size = 12455450, upload-time = "2024-03-08T16:08:57.375Z" }, - { url = "https://files.pythonhosted.org/packages/1c/1b/3e962a201d2f0f57c9fa1990e0dd6076f4f2f94954ab56e4a701ec3cc070/mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913", size = 12530368, upload-time = "2024-03-08T16:09:17.061Z" }, - { url = "https://files.pythonhosted.org/packages/72/1f/8b214b69d08cc5e4bd8c3769ac55a43318f3529362ea55e5957774b69924/mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6", size = 9319112, upload-time = "2024-03-08T16:09:07.961Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/0ba2eaedca52bf5276275e8489951c26206030b3d31bf06f00875ae75d5d/mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e", size = 2555887, upload-time = "2024-03-08T16:09:48.584Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - -[[package]] -name = "packaging" -version = "26.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, -] - -[[package]] -name = "parso" -version = "0.8.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, -] - -[[package]] -name = "pathspec" -version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, -] - -[[package]] -name = "pdbr" -version = "0.9.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyreadline3", marker = "sys_platform == 'win32'" }, - { name = "rich" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/37/47/1458f133732817d5e98e1323b454b032e70a07589c52562e8985e7add8e5/pdbr-0.9.7.tar.gz", hash = "sha256:8baba0b0dfe0ee2b758da4341507a71c20a6f6a3f92d9ee6bcd9650ae2e805bf", size = 15615, upload-time = "2026-01-05T08:36:11.166Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/9e/1ea57f0c7b13cc85f280eded3f24d0ee1f264709289ebc0804dc4608f34e/pdbr-0.9.7-py3-none-any.whl", hash = "sha256:b88c8df940897d01eb0944062b80cd4422e46903b2b910d281b215748029ba21", size = 16201, upload-time = "2026-01-05T08:36:09.942Z" }, -] - -[[package]] -name = "pexpect" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ptyprocess" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.9.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "pprintpp" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/1a/7737e7a0774da3c3824d654993cf57adc915cb04660212f03406334d8c0b/pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403", size = 17995, upload-time = "2018-07-01T01:42:34.87Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/d1/e4ed95fdd3ef13b78630280d9e9e240aeb65cc7c544ec57106149c3942fb/pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d", size = 16952, upload-time = "2018-07-01T01:42:36.496Z" }, -] - -[[package]] -name = "prompt-toolkit" -version = "3.0.52" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, -] - -[[package]] -name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, -] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, -] - -[[package]] -name = "pure-eval" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pydantic" -version = "2.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/6b/1353beb3d1cd5cf61cdec5b6f87a9872399de3bc5cae0b7ce07ff4de2ab0/pydantic-2.13.1.tar.gz", hash = "sha256:a0f829b279ddd1e39291133fe2539d2aa46cc6b150c1706a270ff0879e3774d2", size = 843746, upload-time = "2026-04-15T14:57:19.398Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/5a/2225f4c176dbfed0d809e848b50ef08f70e61daa667b7fa14b0d311ae44d/pydantic-2.13.1-py3-none-any.whl", hash = "sha256:9557ecc2806faaf6037f85b1fbd963d01e30511c48085f0d573650fdeaad378a", size = 471917, upload-time = "2026-04-15T14:57:17.277Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.46.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/93/f97a86a7eb28faa1d038af2fd5d6166418b4433659108a4c311b57128b2d/pydantic_core-2.46.1.tar.gz", hash = "sha256:d408153772d9f298098fb5d620f045bdf0f017af0d5cb6e309ef8c205540caa4", size = 471230, upload-time = "2026-04-15T14:49:34.52Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/fb/caaa8ee23861c170f07dbd58fc2be3a2c02a32637693cbb23eef02e84808/pydantic_core-2.46.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae8c8c5eb4c796944f3166f2f0dab6c761c2c2cc5bd20e5f692128be8600b9a4", size = 2119472, upload-time = "2026-04-15T14:49:45.946Z" }, - { url = "https://files.pythonhosted.org/packages/fa/61/bcffaa52894489ff89e5e1cdde67429914bf083c0db7296bef153020f786/pydantic_core-2.46.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:daba6f5f5b986aa0682623a1a4f8d1ecb0ec00ce09cfa9ca71a3b742bc383e3a", size = 1951230, upload-time = "2026-04-15T14:52:27.646Z" }, - { url = "https://files.pythonhosted.org/packages/f8/95/80d2f43a2a1a1e3220fd329d614aa5a39e0a75d24353a3aaf226e605f1c2/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0265f3a2460539ecc97817a80c7a23c458dd84191229b655522a2674f701f14e", size = 1976394, upload-time = "2026-04-15T14:50:32.742Z" }, - { url = "https://files.pythonhosted.org/packages/8d/31/2c5b1a207926b5fc1961a2d11da940129bc3841c36cc4df03014195b2966/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb16c0156c4b4e94aa3719138cc43c53d30ff21126b6a3af63786dcc0757b56e", size = 2068455, upload-time = "2026-04-15T14:50:01.286Z" }, - { url = "https://files.pythonhosted.org/packages/7d/36/c6aa07274359a51ac62895895325ce90107e811c6cea39d2617a99ef10d7/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b42d80fad8e4b283e1e4138f1142f0d038c46d137aad2f9824ad9086080dd41", size = 2239049, upload-time = "2026-04-15T14:53:02.216Z" }, - { url = "https://files.pythonhosted.org/packages/0a/3f/77cdd0db8bddc714842dfd93f737c863751cf02001c993341504f6b0cd53/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cced85896d5b795293bc36b7e2fb0347a36c828551b50cbba510510d928548c", size = 2318681, upload-time = "2026-04-15T14:50:04.539Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a3/09d929a40e6727274b0b500ad06e1b3f35d4f4665ae1c8ba65acbb17e9b5/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a641cb1e74b44c418adaf9f5f450670dbec53511f030d8cde8d8accb66edc363", size = 2096527, upload-time = "2026-04-15T14:53:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/89/ae/544c3a82456ebc254a9fcbe2715bab76c70acf9d291aaea24391147943e4/pydantic_core-2.46.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:191e7a122ab14eb12415fe3f92610fc06c7f1d2b4b9101d24d490d447ac92506", size = 2170407, upload-time = "2026-04-15T14:51:27.138Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ce/0dfd881c7af4c522f47b325707bd9a2cdcf4f40e4f2fd30df0e9a3e8d393/pydantic_core-2.46.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fe4ff660f7938b5d92f21529ce331b011aa35e481ab64b7cd03f52384e544bb", size = 2188578, upload-time = "2026-04-15T14:50:39.655Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e9/980ea2a6d5114dd1a62ecc5f56feb3d34555f33bd11043f042e5f7f0724a/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:18fcea085b3adc3868d8d19606da52d7a52d8bccd8e28652b0778dbe5e6a6660", size = 2188959, upload-time = "2026-04-15T14:52:42.243Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f1/595e0f50f4bfc56cde2fe558f2b0978f29f2865da894c6226231e17464a5/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e8e589e7c9466e022d79e13c5764c2239b2e5a7993ba727822b021234f89b56b", size = 2339973, upload-time = "2026-04-15T14:52:10.642Z" }, - { url = "https://files.pythonhosted.org/packages/49/44/be9f979a6ab6b8c36865ccd92c3a38a760c66055e1f384665f35525134c4/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f78eb3d4027963bdc9baccd177f02a98bf8714bc51fe17153d8b51218918b5bc", size = 2385228, upload-time = "2026-04-15T14:51:00.77Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d4/c826cd711787d240219f01d0d3ca116cb55516b8b95277820aa9c85e1882/pydantic_core-2.46.1-cp312-cp312-win32.whl", hash = "sha256:54fe30c20cab03844dc63bdc6ddca67f74a2eb8482df69c1e5f68396856241be", size = 1978828, upload-time = "2026-04-15T14:50:29.362Z" }, - { url = "https://files.pythonhosted.org/packages/22/05/8a1fcf8181be4c7a9cfc34e5fbf2d9c3866edc9dfd3c48d5401806e0a523/pydantic_core-2.46.1-cp312-cp312-win_amd64.whl", hash = "sha256:aea4e22ed4c53f2774221435e39969a54d2e783f4aee902cdd6c8011415de893", size = 2070015, upload-time = "2026-04-15T14:49:47.301Z" }, - { url = "https://files.pythonhosted.org/packages/61/d5/fea36ad2882b99c174ef4ffbc7ea6523f6abe26060fbc1f77d6441670232/pydantic_core-2.46.1-cp312-cp312-win_arm64.whl", hash = "sha256:f76fb49c34b4d66aa6e552ce9e852ea97a3a06301a9f01ae82f23e449e3a55f8", size = 2030176, upload-time = "2026-04-15T14:50:47.307Z" }, - { url = "https://files.pythonhosted.org/packages/ff/d2/bda39bad2f426cb5078e6ad28076614d3926704196efe0d7a2a19a99025d/pydantic_core-2.46.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:cdc8a5762a9c4b9d86e204d555444e3227507c92daba06259ee66595834de47a", size = 2119092, upload-time = "2026-04-15T14:49:50.392Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f3/69631e64d69cb3481494b2bddefe0ddd07771209f74e9106d066f9138c2a/pydantic_core-2.46.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba381dfe9c85692c566ecb60fa5a77a697a2a8eebe274ec5e4d6ec15fafad799", size = 1951400, upload-time = "2026-04-15T14:51:06.588Z" }, - { url = "https://files.pythonhosted.org/packages/53/1c/21cb3db6ae997df31be8e91f213081f72ffa641cb45c89b8a1986832b1f9/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1593d8de98207466dc070118322fef68307a0cc6a5625e7b386f6fdae57f9ab6", size = 1976864, upload-time = "2026-04-15T14:50:54.804Z" }, - { url = "https://files.pythonhosted.org/packages/91/9c/05c819f734318ce5a6ca24da300d93696c105af4adb90494ee571303afd8/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8262c74a1af5b0fdf795f5537f7145785a63f9fbf9e15405f547440c30017ed8", size = 2066669, upload-time = "2026-04-15T14:51:42.346Z" }, - { url = "https://files.pythonhosted.org/packages/cb/23/fadddf1c7f2f517f58731aea9b35c914e6005250f08dac9b8e53904cdbaa/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b88949a24182e83fbbb3f7ca9b7858d0d37b735700ea91081434b7d37b3b444", size = 2238737, upload-time = "2026-04-15T14:50:45.558Z" }, - { url = "https://files.pythonhosted.org/packages/23/07/0cd4f95cb0359c8b1ec71e89c3777e7932c8dfeb9cd54740289f310aaead/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8f3708cd55537aeaf3fd0ea55df0d68d0da51dcb07cbc8508745b34acc4c6e0", size = 2316258, upload-time = "2026-04-15T14:51:08.471Z" }, - { url = "https://files.pythonhosted.org/packages/0c/40/6fc24c3766a19c222a0d60d652b78f0283339d4cd4c173fab06b7ee76571/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f79292435fff1d4f0c18d9cfaf214025cc88e4f5104bfaed53f173621da1c743", size = 2097474, upload-time = "2026-04-15T14:49:56.543Z" }, - { url = "https://files.pythonhosted.org/packages/4b/af/f39795d1ce549e35d0841382b9c616ae211caffb88863147369a8d74fba9/pydantic_core-2.46.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a2e607aeb59cf4575bb364470288db3b9a1f0e7415d053a322e3e154c1a0802e", size = 2168383, upload-time = "2026-04-15T14:51:29.269Z" }, - { url = "https://files.pythonhosted.org/packages/e6/32/0d563f74582795779df6cc270c3fc220f49f4daf7860d74a5a6cda8491ff/pydantic_core-2.46.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec5ca190b75878a9f6ae1fc8f5eb678497934475aef3d93204c9fa01e97370b6", size = 2186182, upload-time = "2026-04-15T14:50:19.097Z" }, - { url = "https://files.pythonhosted.org/packages/5c/07/1c10d5ce312fc4cf86d1e50bdcdbb8ef248409597b099cab1b4bb3a093f7/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:1f80535259dcdd517d7b8ca588d5ca24b4f337228e583bebedf7a3adcdf5f721", size = 2187859, upload-time = "2026-04-15T14:49:22.974Z" }, - { url = "https://files.pythonhosted.org/packages/92/01/e1f62d4cb39f0913dbf5c95b9b119ef30ddba9493dff8c2b012f0cdd67dc/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:24820b3c82c43df61eca30147e42853e6c127d8b868afdc0c162df829e011eb4", size = 2338372, upload-time = "2026-04-15T14:49:53.316Z" }, - { url = "https://files.pythonhosted.org/packages/44/ed/218dfeea6127fb1781a6ceca241ec6edf00e8a8933ff331af2215975a534/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f12794b1dd8ac9fb66619e0b3a0427189f5d5638e55a3de1385121a9b7bf9b39", size = 2384039, upload-time = "2026-04-15T14:53:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/6c/1e/011e763cd059238249fbd5780e0f8d0b04b47f86c8925e22784f3e5fc977/pydantic_core-2.46.1-cp313-cp313-win32.whl", hash = "sha256:9bc09aed935cdf50f09e908923f9efbcca54e9244bd14a5a0e2a6c8d2c21b4e9", size = 1977943, upload-time = "2026-04-15T14:52:17.969Z" }, - { url = "https://files.pythonhosted.org/packages/8c/06/b559a490d3ed106e9b1777b8d5c8112dd8d31716243cd662616f66c1f8ea/pydantic_core-2.46.1-cp313-cp313-win_amd64.whl", hash = "sha256:fac2d6c8615b8b42bee14677861ba09d56ee076ba4a65cfb9c3c3d0cc89042f2", size = 2068729, upload-time = "2026-04-15T14:53:07.288Z" }, - { url = "https://files.pythonhosted.org/packages/9f/52/32a198946e2e19508532aa9da02a61419eb15bd2d96bab57f810f2713e31/pydantic_core-2.46.1-cp313-cp313-win_arm64.whl", hash = "sha256:f978329f12ace9f3cb814a5e44d98bbeced2e36f633132bafa06d2d71332e33e", size = 2029550, upload-time = "2026-04-15T14:52:22.707Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2b/6793fe89ab66cb2d3d6e5768044eab80bba1d0fae8fd904d0a1574712e17/pydantic_core-2.46.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9917cb61effac7ec0f448ef491ec7584526d2193be84ff981e85cbf18b68c42a", size = 2118110, upload-time = "2026-04-15T14:50:52.947Z" }, - { url = "https://files.pythonhosted.org/packages/d2/87/e9a905ddfcc2fd7bd862b340c02be6ab1f827922822d425513635d0ac774/pydantic_core-2.46.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e749679ca9f8a9d0bff95fb7f6b57bb53f2207fa42ffcc1ec86de7e0029ab89", size = 1948645, upload-time = "2026-04-15T14:51:55.577Z" }, - { url = "https://files.pythonhosted.org/packages/15/23/26e67f86ed62ac9d6f7f3091ee5220bf14b5ac36fb811851d601365ef896/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2ecacee70941e233a2dad23f7796a06f86cc10cc2fbd1c97c7dd5b5a79ffa4f", size = 1977576, upload-time = "2026-04-15T14:49:37.58Z" }, - { url = "https://files.pythonhosted.org/packages/b8/78/813c13c0de323d4de54ee2e6fdd69a0271c09ac8dd65a8a000931aa487a5/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:647d0a2475b8ed471962eed92fa69145b864942f9c6daa10f95ac70676637ae7", size = 2060358, upload-time = "2026-04-15T14:51:40.087Z" }, - { url = "https://files.pythonhosted.org/packages/09/5e/4caf2a15149271fbd2b4d968899a450853c800b85152abcf54b11531417f/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac9cde61965b0697fce6e6cc372df9e1ad93734828aac36e9c1c42a22ad02897", size = 2235980, upload-time = "2026-04-15T14:50:34.535Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c1/a2cdabb5da6f5cb63a3558bcafffc20f790fa14ccffbefbfb1370fadc93f/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a2eb0864085f8b641fb3f54a2fb35c58aff24b175b80bc8a945050fcde03204", size = 2316800, upload-time = "2026-04-15T14:52:46.999Z" }, - { url = "https://files.pythonhosted.org/packages/76/fd/19d711e4e9331f9d77f222bffc202bf30ea0d74f6419046376bb82f244c8/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b83ce9fede4bc4fb649281d9857f06d30198b8f70168f18b987518d713111572", size = 2101762, upload-time = "2026-04-15T14:49:24.278Z" }, - { url = "https://files.pythonhosted.org/packages/dc/64/ce95625448e1a4e219390a2923fd594f3fa368599c6b42ac71a5df7238c9/pydantic_core-2.46.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:cb33192753c60f269d2f4a1db8253c95b0df6e04f2989631a8cc1b0f4f6e2e92", size = 2167737, upload-time = "2026-04-15T14:50:41.637Z" }, - { url = "https://files.pythonhosted.org/packages/ad/31/413572d03ca3e73b408f00f54418b91a8be6401451bc791eaeff210328e5/pydantic_core-2.46.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:96611d51f953f87e1ae97637c01ee596a08b7f494ea00a5afb67ea6547b9f53b", size = 2185658, upload-time = "2026-04-15T14:51:46.799Z" }, - { url = "https://files.pythonhosted.org/packages/36/09/e4f581353bdf3f0c7de8a8b27afd14fc761da29d78146376315a6fedc487/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9b176fa55f9107db5e6c86099aa5bfd934f1d3ba6a8b43f714ddeebaed3f42b7", size = 2184154, upload-time = "2026-04-15T14:52:49.629Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a4/d0d52849933f5a4bf1ad9d8da612792f96469b37e286a269e3ee9c60bbb1/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:79a59f63a4ce4f3330e27e6f3ce281dd1099453b637350e97d7cf24c207cd120", size = 2332379, upload-time = "2026-04-15T14:49:55.009Z" }, - { url = "https://files.pythonhosted.org/packages/30/93/25bfb08fdbef419f73290e573899ce938a327628c34e8f3a4bafeea30126/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:f200fce071808a385a314b7343f5e3688d7c45746be3d64dc71ee2d3e2a13268", size = 2377964, upload-time = "2026-04-15T14:51:59.649Z" }, - { url = "https://files.pythonhosted.org/packages/15/36/b777766ff83fef1cf97473d64764cd44f38e0d8c269ed06faace9ae17666/pydantic_core-2.46.1-cp314-cp314-win32.whl", hash = "sha256:3a07eccc0559fb9acc26d55b16bf8ebecd7f237c74a9e2c5741367db4e6d8aff", size = 1976450, upload-time = "2026-04-15T14:51:57.665Z" }, - { url = "https://files.pythonhosted.org/packages/7b/4b/4cd19d2437acfc18ca166db5a2067040334991eb862c4ecf2db098c91fbf/pydantic_core-2.46.1-cp314-cp314-win_amd64.whl", hash = "sha256:1706d270309ac7d071ffe393988c471363705feb3d009186e55d17786ada9622", size = 2067750, upload-time = "2026-04-15T14:49:38.941Z" }, - { url = "https://files.pythonhosted.org/packages/7f/a0/490751c0ef8f5b27aae81731859aed1508e72c1a9b5774c6034269db773b/pydantic_core-2.46.1-cp314-cp314-win_arm64.whl", hash = "sha256:22d4e7457ade8af06528012f382bc994a97cc2ce6e119305a70b3deff1e409d6", size = 2021109, upload-time = "2026-04-15T14:50:27.728Z" }, - { url = "https://files.pythonhosted.org/packages/36/3a/2a018968245fffd25d5f1972714121ad309ff2de19d80019ad93494844f9/pydantic_core-2.46.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:607ff9db0b7e2012e7eef78465e69f9a0d7d1c3e7c6a84cf0c4011db0fcc3feb", size = 2111548, upload-time = "2026-04-15T14:52:08.273Z" }, - { url = "https://files.pythonhosted.org/packages/77/5b/4103b6192213217e874e764e5467d2ff10d8873c1147d01fa432ac281880/pydantic_core-2.46.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cda3eacaea13bd02a1bea7e457cc9fc30b91c5a91245cef9b215140f80dd78c", size = 1926745, upload-time = "2026-04-15T14:50:03.045Z" }, - { url = "https://files.pythonhosted.org/packages/c3/70/602a667cf4be4bec6c3334512b12ae4ea79ce9bfe41dc51be1fd34434453/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9493279cdc7997fe19e5ed9b41f30cbc3806bd4722adb402fedb6f6d41bd72a", size = 1965922, upload-time = "2026-04-15T14:51:12.555Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/06a89ce5323e755b7d2812189f9706b87aaebe49b34d247b380502f7992c/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3644e5e10059999202355b6c6616e624909e23773717d8f76deb8a6e2a72328c", size = 2043221, upload-time = "2026-04-15T14:51:18.995Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6e/b1d9ad907d9d76964903903349fd2e33c87db4b993cc44713edcad0fc488/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ad6c9de57683e26c92730991960c0c3571b8053263b042de2d3e105930b2767", size = 2243655, upload-time = "2026-04-15T14:50:10.718Z" }, - { url = "https://files.pythonhosted.org/packages/ef/73/787abfaad51174641abb04c8aa125322279b40ad7ce23c495f5a69f76554/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:557ebaa27c7617e7088002318c679a8ce685fa048523417cd1ca52b7f516d955", size = 2295976, upload-time = "2026-04-15T14:53:09.694Z" }, - { url = "https://files.pythonhosted.org/packages/56/0b/b7c5a631b6d5153d4a1ea4923b139aea256dc3bd99c8e6c7b312c7733146/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cd37e39b22b796ba0298fe81e9421dd7b65f97acfbb0fb19b33ffdda7b9a7b4", size = 2103439, upload-time = "2026-04-15T14:50:08.32Z" }, - { url = "https://files.pythonhosted.org/packages/2a/3f/952ee470df69e5674cdec1cbde22331adf643b5cc2ff79f4292d80146ee4/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:6689443b59714992e67d62505cdd2f952d6cf1c14cc9fd9aeec6719befc6f23b", size = 2132871, upload-time = "2026-04-15T14:50:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/e3/8b/1dea3b1e683c60c77a60f710215f90f486755962aa8939dbcb7c0f975ac3/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f32c41ca1e3456b5dd691827b7c1433c12d5f0058cc186afbb3615bc07d97b8", size = 2168658, upload-time = "2026-04-15T14:52:24.897Z" }, - { url = "https://files.pythonhosted.org/packages/67/97/32ae283810910d274d5ba9f48f856f5f2f612410b78b249f302d297816f5/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:88cd1355578852db83954dc36e4f58f299646916da976147c20cf6892ba5dc43", size = 2171184, upload-time = "2026-04-15T14:52:34.854Z" }, - { url = "https://files.pythonhosted.org/packages/a2/57/c9a855527fe56c2072070640221f53095b0b19eaf651f3c77643c9cabbe3/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:a170fefdb068279a473cc9d34848b85e61d68bfcc2668415b172c5dfc6f213bf", size = 2316573, upload-time = "2026-04-15T14:52:12.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/14c39ffc7399819c5448007c7bcb4e6da5669850cfb7dcbb727594290b48/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:556a63ff1006934dba4eed7ea31b58274c227e29298ec398e4275eda4b905e95", size = 2378340, upload-time = "2026-04-15T14:51:02.619Z" }, - { url = "https://files.pythonhosted.org/packages/01/55/a37461fbb29c053ea4e62cfc5c2d56425cb5efbef8316e63f6d84ae45718/pydantic_core-2.46.1-cp314-cp314t-win32.whl", hash = "sha256:3b146d8336a995f7d7da6d36e4a779b7e7dff2719ac00a1eb8bd3ded00bec87b", size = 1960843, upload-time = "2026-04-15T14:52:06.103Z" }, - { url = "https://files.pythonhosted.org/packages/22/d7/97e1221197d17a27f768363f87ec061519eeeed15bbd315d2e9d1429ff03/pydantic_core-2.46.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f1bc856c958e6fe9ec071e210afe6feb695f2e2e81fd8d2b102f558d364c4c17", size = 2048696, upload-time = "2026-04-15T14:52:52.154Z" }, - { url = "https://files.pythonhosted.org/packages/19/d5/4eac95255c7d35094b46a32ec1e4d80eac94729c694726ee1d69948bd5f0/pydantic_core-2.46.1-cp314-cp314t-win_arm64.whl", hash = "sha256:21a5bfd8a1aa4de60494cdf66b0c912b1495f26a8899896040021fbd6038d989", size = 2022343, upload-time = "2026-04-15T14:49:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/f4/97/95de673a1356a88b2efdaa120eb6af357a81555c35f6809a7a1423ff7aef/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:5f9107a24a4bc00293434dfa95cf8968751ad0dd703b26ea83a75a56f7326041", size = 2107564, upload-time = "2026-04-15T14:50:49.14Z" }, - { url = "https://files.pythonhosted.org/packages/00/fc/a7c16d85211ea9accddc693b7d049f20b0c06440d9264d1e1c074394ee6c/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:2b1801ba99876984d0a03362782819238141c4d0f3f67f69093663691332fc35", size = 1939925, upload-time = "2026-04-15T14:50:36.188Z" }, - { url = "https://files.pythonhosted.org/packages/2e/23/87841169d77820ddabeb81d82002c95dcb82163846666d74f5bdeeaec750/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7fd82a91a20ed6d54fa8c91e7a98255b1ff45bf09b051bfe7fe04eb411e232e", size = 1995313, upload-time = "2026-04-15T14:50:22.538Z" }, - { url = "https://files.pythonhosted.org/packages/ea/96/b46609359a354fa9cd336fc5d93334f1c358b756cc81e4b397347a88fa6f/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f135bf07c92c93def97008bc4496d16934da9efefd7204e5f22a2c92523cb1f", size = 2151197, upload-time = "2026-04-15T14:51:22.925Z" }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, -] - -[[package]] -name = "pyreadline3" -version = "3.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, -] - -[[package]] -name = "pytest" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, -] - -[[package]] -name = "pytest-asyncio" -version = "0.25.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/18/82fcb4ee47d66d99f6cd1efc0b11b2a25029f303c599a5afda7c1bca4254/pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609", size = 53298, upload-time = "2024-12-13T06:12:44.53Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/56/2ee0cab25c11d4e38738a2a98c645a8f002e2ecf7b5ed774c70d53b92bb1/pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3", size = 19245, upload-time = "2024-12-13T06:12:41.805Z" }, -] - -[[package]] -name = "pytest-cov" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage" }, - { name = "pluggy" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, -] - -[[package]] -name = "pytest-custom-exit-code" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/9d/e1eb0af5e96a5c34f59b9aa69dfb680764420fe60f2ec28cfbc5339f99f8/pytest-custom_exit_code-0.3.0.tar.gz", hash = "sha256:51ffff0ee2c1ddcc1242e2ddb2a5fd02482717e33a2326ef330e3aa430244635", size = 3633, upload-time = "2019-08-07T09:45:15.781Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/a0/effb6cbbccfd1c106c572d3d619b3418d71093afb4cd4f91f51e6a1799d2/pytest_custom_exit_code-0.3.0-py3-none-any.whl", hash = "sha256:6e0ce6e57ce3a583cb7e5023f7d1021e19dfec22be41d9ad345bae2fc61caf3b", size = 4055, upload-time = "2019-08-07T09:45:13.767Z" }, -] - -[[package]] -name = "pytest-icdiff" -version = "0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "icdiff" }, - { name = "pprintpp" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/0c/66e1e2590e98f4428e374a3b6448dc086a908d15b1e24b914539d13b7ac4/pytest-icdiff-0.9.tar.gz", hash = "sha256:13aede616202e57fcc882568b64589002ef85438046f012ac30a8d959dac8b75", size = 7110, upload-time = "2023-12-05T11:18:30.192Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/e1/cafe1edf7a30be6fa1bbbf43f7af12b34682eadcf19eb6e9f7352062c422/pytest_icdiff-0.9-py3-none-any.whl", hash = "sha256:efee0da3bd1b24ef2d923751c5c547fbb8df0a46795553fba08ef57c3ca03d82", size = 4994, upload-time = "2023-12-05T11:18:28.572Z" }, -] - -[[package]] -name = "pytest-instafail" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/86/bd/e0ba6c3cd20b9aa445f0af229f3a9582cce589f083537978a23e6f14e310/pytest-instafail-0.5.0.tar.gz", hash = "sha256:33a606f7e0c8e646dc3bfee0d5e3a4b7b78ef7c36168cfa1f3d93af7ca706c9e", size = 5849, upload-time = "2023-03-31T17:17:32.161Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/c0/c32dc39fc172e684fdb3d30169843efb65c067be1e12689af4345731126e/pytest_instafail-0.5.0-py3-none-any.whl", hash = "sha256:6855414487e9e4bb76a118ce952c3c27d3866af15487506c4ded92eb72387819", size = 4176, upload-time = "2023-03-31T17:17:30.065Z" }, -] - -[[package]] -name = "pytest-mock" -version = "3.15.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, -] - -[[package]] -name = "pytest-rerunfailures" -version = "16.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/04/71e9520551fc8fe2cf5c1a1842e4e600265b0815f2016b7c27ec85688682/pytest_rerunfailures-16.1.tar.gz", hash = "sha256:c38b266db8a808953ebd71ac25c381cb1981a78ff9340a14bcb9f1b9bff1899e", size = 30889, upload-time = "2025-10-10T07:06:01.238Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/54/60eabb34445e3db3d3d874dc1dfa72751bfec3265bd611cb13c8b290adea/pytest_rerunfailures-16.1-py3-none-any.whl", hash = "sha256:5d11b12c0ca9a1665b5054052fcc1084f8deadd9328962745ef6b04e26382e86", size = 14093, upload-time = "2025-10-10T07:06:00.019Z" }, -] - -[[package]] -name = "pytest-timeouts" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/47/9a8d7fa18736d69d218e303345dfe21755e956fd863b0f043ef854ff84a6/pytest-timeouts-1.2.1.tar.gz", hash = "sha256:390351afc7ecb422ea0ec38081e0acd91cad416b383944a9a3358087de50c2fb", size = 5002, upload-time = "2019-09-21T06:03:25.167Z" } - -[[package]] -name = "pytest-unordered" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bd/3e/6ec9ec74551804c9e005d5b3cbe1fd663f03ed3bd4bdb1ce764c3d334d8e/pytest_unordered-0.7.0.tar.gz", hash = "sha256:0f953a438db00a9f6f99a0f4727f2d75e72dd93319b3d548a97ec9db4903a44f", size = 7930, upload-time = "2025-06-03T12:56:04.289Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/95/ae2875e19472797e9672b65412858ab6639d8e55defd9859241e5ff80d02/pytest_unordered-0.7.0-py3-none-any.whl", hash = "sha256:486b26d24a2d3b879a275c3d16d14eda1bd9c32aafddbb17b98ac755daba7584", size = 6210, upload-time = "2025-06-03T12:36:06.66Z" }, -] - -[[package]] -name = "pytest-xdist" -version = "3.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "execnet" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, -] - -[[package]] -name = "pytokens" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, - { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, - { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, - { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, - { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, - { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, - { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, - { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, - { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, - { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, - { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, - { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, - { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, - { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "requests" -version = "2.33.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, -] - -[[package]] -name = "requests-mock" -version = "1.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901, upload-time = "2024-03-29T03:54:29.446Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" }, -] - -[[package]] -name = "rich" -version = "15.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, -] - -[[package]] -name = "ruff" -version = "0.11.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511, upload-time = "2025-03-21T13:31:17.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146, upload-time = "2025-03-21T13:30:26.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092, upload-time = "2025-03-21T13:30:37.949Z" }, - { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082, upload-time = "2025-03-21T13:30:39.962Z" }, - { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818, upload-time = "2025-03-21T13:30:42.551Z" }, - { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251, upload-time = "2025-03-21T13:30:45.196Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566, upload-time = "2025-03-21T13:30:47.516Z" }, - { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721, upload-time = "2025-03-21T13:30:49.56Z" }, - { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274, upload-time = "2025-03-21T13:30:52.055Z" }, - { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284, upload-time = "2025-03-21T13:30:54.24Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861, upload-time = "2025-03-21T13:30:56.757Z" }, - { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560, upload-time = "2025-03-21T13:30:58.881Z" }, - { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091, upload-time = "2025-03-21T13:31:01.45Z" }, - { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133, upload-time = "2025-03-21T13:31:04.013Z" }, - { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514, upload-time = "2025-03-21T13:31:06.166Z" }, - { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835, upload-time = "2025-03-21T13:31:10.7Z" }, - { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713, upload-time = "2025-03-21T13:31:13.148Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990, upload-time = "2025-03-21T13:31:15.206Z" }, -] - -[[package]] -name = "semver" -version = "3.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, -] - -[[package]] -name = "stack-data" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "asttokens" }, - { name = "executing" }, - { name = "pure-eval" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, -] - -[[package]] -name = "time-machine" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/fc/37b02f6094dbb1f851145330460532176ed2f1dc70511a35828166c41e52/time_machine-3.2.0.tar.gz", hash = "sha256:a4ddd1cea17b8950e462d1805a42b20c81eb9aafc8f66b392dd5ce997e037d79", size = 14804, upload-time = "2025-12-17T23:33:02.599Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/8b/080c8eedcd67921a52ba5bd0e075362062509ab63c86fc1a0442fad241a6/time_machine-3.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc4bee5b0214d7dc4ebc91f4a4c600f1a598e9b5606ac751f42cb6f6740b1dbb", size = 19255, upload-time = "2025-12-17T23:31:58.057Z" }, - { url = "https://files.pythonhosted.org/packages/66/17/0e5291e9eb705bf8a5a1305f826e979af307bbeb79def4ddbf4b3f9a81e0/time_machine-3.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ca036304b4460ae2fdc1b52dd8b1fa7cf1464daa427fc49567413c09aa839c1", size = 15360, upload-time = "2025-12-17T23:31:59.048Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/9ab87b71d2e2b62463b9b058b7ae7ac09fb57f8fcd88729dec169d304340/time_machine-3.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5442735b41d7a2abc2f04579b4ca6047ed4698a8338a4fec92c7c9423e7938cb", size = 33029, upload-time = "2025-12-17T23:32:00.413Z" }, - { url = "https://files.pythonhosted.org/packages/4b/26/b5ca19da6f25ea905b3e10a0ea95d697c1aeba0404803a43c68f1af253e6/time_machine-3.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:97da3e971e505cb637079fb07ab0bcd36e33279f8ecac888ff131f45ef1e4d8d", size = 34579, upload-time = "2025-12-17T23:32:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/79/ca/6ac7ad5f10ea18cc1d9de49716ba38c32132c7b64532430d92ef240c116b/time_machine-3.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3cdda6dee4966e38aeb487309bb414c6cb23a81fc500291c77a8fcd3098832e7", size = 35961, upload-time = "2025-12-17T23:32:02.521Z" }, - { url = "https://files.pythonhosted.org/packages/33/67/390dd958bed395ab32d79a9fe61fe111825c0dd4ded54dbba7e867f171e6/time_machine-3.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:33d9efd302a6998bcc8baa4d84f259f8a4081105bd3d7f7af7f1d0abd3b1c8aa", size = 34668, upload-time = "2025-12-17T23:32:03.585Z" }, - { url = "https://files.pythonhosted.org/packages/da/57/c88fff034a4e9538b3ae7c68c9cfb283670b14d17522c5a8bc17d29f9a4b/time_machine-3.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3a0b0a33971f14145853c9bd95a6ab0353cf7e0019fa2a7aa1ae9fddfe8eab50", size = 32891, upload-time = "2025-12-17T23:32:04.656Z" }, - { url = "https://files.pythonhosted.org/packages/2d/70/ebbb76022dba0fec8f9156540fc647e4beae1680c787c01b1b6200e56d70/time_machine-3.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2d0be9e5f22c38082d247a2cdcd8a936504e9db60b7b3606855fb39f299e9548", size = 34080, upload-time = "2025-12-17T23:32:06.146Z" }, - { url = "https://files.pythonhosted.org/packages/db/9a/2ca9e7af3df540dc1c79e3de588adeddb7dcc2107829248e6969c4f14167/time_machine-3.2.0-cp312-cp312-win32.whl", hash = "sha256:3f74623648b936fdce5f911caf386c0a0b579456410975de8c0dfeaaffece1d8", size = 17371, upload-time = "2025-12-17T23:32:07.164Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ce/21d23efc9c2151939af1b7ee4e60d86d661b74ef32b8eaa148f6fe8c899c/time_machine-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:34e26a41d994b5e4b205136a90e9578470386749cc9a2ecf51ca18f83ce25e23", size = 18132, upload-time = "2025-12-17T23:32:08.447Z" }, - { url = "https://files.pythonhosted.org/packages/2f/34/c2b70be483accf6db9e5d6c3139bce3c38fe51f898ccf64e8d3fe14fbf4d/time_machine-3.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:0615d3d82c418d6293f271c348945c5091a71f37e37173653d5c26d0e74b13a8", size = 16930, upload-time = "2025-12-17T23:32:09.477Z" }, - { url = "https://files.pythonhosted.org/packages/ee/cd/43ad5efc88298af3c59b66769cea7f055567a85071579ed40536188530c1/time_machine-3.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c421a8eb85a4418a7675a41bf8660224318c46cc62e4751c8f1ceca752059090", size = 19318, upload-time = "2025-12-17T23:32:10.518Z" }, - { url = "https://files.pythonhosted.org/packages/b0/f6/084010ef7f4a3f38b5a4900923d7c85b29e797655c4f6ee4ce54d903cca8/time_machine-3.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4e758f7727d0058c4950c66b58200c187072122d6f7a98b610530a4233ea7b", size = 15390, upload-time = "2025-12-17T23:32:11.625Z" }, - { url = "https://files.pythonhosted.org/packages/25/aa/1cabb74134f492270dc6860cb7865859bf40ecf828be65972827646e91ad/time_machine-3.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:154bd3f75c81f70218b2585cc12b60762fb2665c507eec5ec5037d8756d9b4e0", size = 33115, upload-time = "2025-12-17T23:32:13.219Z" }, - { url = "https://files.pythonhosted.org/packages/5e/03/78c5d7dfa366924eb4dbfcc3fc917c39a4280ca234b12819cc1f16c03d88/time_machine-3.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50cfe5ebea422c896ad8d278af9648412b7533b8ea6adeeee698a3fd9b1d3b7", size = 34705, upload-time = "2025-12-17T23:32:14.29Z" }, - { url = "https://files.pythonhosted.org/packages/86/93/d5e877c24541f674c6869ff6e9c56833369796010190252e92c9d7ae5f0f/time_machine-3.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636576501724bd6a9124e69d86e5aef263479e89ef739c5db361469f0463a0a1", size = 36104, upload-time = "2025-12-17T23:32:15.354Z" }, - { url = "https://files.pythonhosted.org/packages/22/1c/d4bae72f388f67efc9609f89b012e434bb19d9549c7a7b47d6c7d9e5c55d/time_machine-3.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40e6f40c57197fcf7ec32d2c563f4df0a82c42cdcc3cab27f688e98f6060df10", size = 34765, upload-time = "2025-12-17T23:32:16.434Z" }, - { url = "https://files.pythonhosted.org/packages/1d/c3/ac378cf301d527d8dfad2f0db6bad0dfb1ab73212eaa56d6b96ee5d9d20b/time_machine-3.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a1bcf0b846bbfc19a79bc19e3fa04d8c7b1e8101c1b70340ffdb689cd801ea53", size = 33010, upload-time = "2025-12-17T23:32:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/06/35/7ce897319accda7a6970b288a9a8c52d25227342a7508505a2b3d235b649/time_machine-3.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae55a56c179f4fe7a62575ad5148b6ed82f6c7e5cf2f9a9ec65f2f5b067db5f5", size = 34185, upload-time = "2025-12-17T23:32:18.566Z" }, - { url = "https://files.pythonhosted.org/packages/bf/28/f922022269749cb02eee2b62919671153c4088994fa955a6b0e50327ff81/time_machine-3.2.0-cp313-cp313-win32.whl", hash = "sha256:a66fe55a107e46916007a391d4030479df8864ec6ad6f6a6528221befc5c886e", size = 17397, upload-time = "2025-12-17T23:32:19.605Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/fd87cde397f4a7bea493152f0aca8fd569ec709cad9e0f2ca7011eb8c7f7/time_machine-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:30c9ce57165df913e4f74e285a8ab829ff9b7aa3e5ec0973f88f642b9a7b3d15", size = 18139, upload-time = "2025-12-17T23:32:20.991Z" }, - { url = "https://files.pythonhosted.org/packages/75/81/b8ce58233addc5d7d54d2fabc49dcbc02d79e3f079d150aa1bec3d5275ef/time_machine-3.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:89cad7e179e9bdcc84dcf09efe52af232c4cc7a01b3de868356bbd59d95bd9b8", size = 16964, upload-time = "2025-12-17T23:32:22.075Z" }, - { url = "https://files.pythonhosted.org/packages/67/e7/487f0ba5fe6c58186a5e1af2a118dfa2c160fedb37ef53a7e972d410408e/time_machine-3.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:59d71545e62525a4b85b6de9ab5c02ee3c61110fd7f636139914a2335dcbfc9c", size = 20000, upload-time = "2025-12-17T23:32:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/e1/17/eb2c0054c8d44dd42df84ccd434539249a9c7d0b8eb53f799be2102500ab/time_machine-3.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:999672c621c35362bc28e03ca0c7df21500195540773c25993421fd8d6cc5003", size = 15657, upload-time = "2025-12-17T23:32:24.125Z" }, - { url = "https://files.pythonhosted.org/packages/43/21/93443b5d1dd850f8bb9442e90d817a9033dcce6bfbdd3aabbb9786251c80/time_machine-3.2.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5faf7397f0580c7b9d67288522c8d7863e85f0cffadc0f1fccdb2c3dfce5783e", size = 39216, upload-time = "2025-12-17T23:32:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/9f/9e/18544cf8acc72bb1dc03762231c82ecc259733f4bb6770a7bbe5cd138603/time_machine-3.2.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3dd886ec49f1fa5a00e844f5947e5c0f98ce574750c24b7424c6f77fc1c3e87", size = 40764, upload-time = "2025-12-17T23:32:26.643Z" }, - { url = "https://files.pythonhosted.org/packages/27/f7/9fe9ce2795636a3a7467307af6bdf38bb613ddb701a8a5cd50ec713beb5e/time_machine-3.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0ecd96bc7bbe450acaaabe569d84e81688f1be8ad58d1470e42371d145fb53", size = 43526, upload-time = "2025-12-17T23:32:27.693Z" }, - { url = "https://files.pythonhosted.org/packages/03/c1/a93e975ba9dec22e87ec92d18c28e67d36bd536f9119ffa439b2892b0c9c/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:158220e946c1c4fb8265773a0282c88c35a7e3bb5d78e3561214e3b3231166f3", size = 41727, upload-time = "2025-12-17T23:32:28.985Z" }, - { url = "https://files.pythonhosted.org/packages/5f/fb/e3633e5a6bbed1c76bb2e9810dabc2f8467532ffcd29b9aed404b473061a/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c1aee29bc54356f248d5d7dfdd131e12ca825e850a08c0ebdb022266d073013", size = 38952, upload-time = "2025-12-17T23:32:30.031Z" }, - { url = "https://files.pythonhosted.org/packages/82/3d/02e9fb2526b3d6b1b45bc8e4d912d95d1cd699d1a3f6df985817d37a0600/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8ed2224f09d25b1c2fc98683613aca12f90f682a427eabb68fc824d27014e4a", size = 39829, upload-time = "2025-12-17T23:32:31.075Z" }, - { url = "https://files.pythonhosted.org/packages/85/c8/c14265212436da8e0814c45463987b3f57de3eca4de023cc2eabb0c62ef3/time_machine-3.2.0-cp313-cp313t-win32.whl", hash = "sha256:3498719f8dab51da76d29a20c1b5e52ee7db083dddf3056af7fa69c1b94e1fe6", size = 17852, upload-time = "2025-12-17T23:32:32.079Z" }, - { url = "https://files.pythonhosted.org/packages/1d/bc/8acb13cf6149f47508097b158a9a8bec9ec4530a70cb406124e8023581f5/time_machine-3.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e0d90bee170b219e1d15e6a58164aa808f5170090e4f090bd0670303e34181b1", size = 18918, upload-time = "2025-12-17T23:32:33.106Z" }, - { url = "https://files.pythonhosted.org/packages/24/87/c443ee508c2708fd2514ccce9052f5e48888783ce690506919629ebc8eb0/time_machine-3.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:051de220fdb6e20d648111bbad423d9506fdbb2e44d4429cef3dc0382abf1fc2", size = 17261, upload-time = "2025-12-17T23:32:34.446Z" }, - { url = "https://files.pythonhosted.org/packages/61/70/b4b980d126ed155c78d1879c50d60c8dcbd47bd11cb14ee7be50e0dfc07f/time_machine-3.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1398980c017fe5744d66f419e0115ee48a53b00b146d738e1416c225eb610b82", size = 19303, upload-time = "2025-12-17T23:32:35.796Z" }, - { url = "https://files.pythonhosted.org/packages/73/73/eaa33603c69a68fe2b6f54f9dd75481693d62f1d29676531002be06e2d1c/time_machine-3.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f8f4e35f4191ef70c2ab8ff490761ee9051b891afce2bf86dde3918eb7b537b", size = 15431, upload-time = "2025-12-17T23:32:37.244Z" }, - { url = "https://files.pythonhosted.org/packages/76/10/b81e138e86cc7bab40cdb59d294b341e172201f4a6c84bb0ec080407977a/time_machine-3.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6db498686ecf6163c5aa8cf0bcd57bbe0f4081184f247edf3ee49a2612b584f9", size = 33206, upload-time = "2025-12-17T23:32:38.713Z" }, - { url = "https://files.pythonhosted.org/packages/d3/72/4deab446b579e8bd5dca91de98595c5d6bd6a17ce162abf5c5f2ce40d3d8/time_machine-3.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:027c1807efb74d0cd58ad16524dec94212fbe900115d70b0123399883657ac0f", size = 34792, upload-time = "2025-12-17T23:32:40.223Z" }, - { url = "https://files.pythonhosted.org/packages/2c/39/439c6b587ddee76d533fe972289d0646e0a5520e14dc83d0a30aeb5565f7/time_machine-3.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92432610c05676edd5e6946a073c6f0c926923123ce7caee1018dc10782c713d", size = 36187, upload-time = "2025-12-17T23:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/4b/db/2da4368db15180989bab83746a857bde05ad16e78f326801c142bb747a06/time_machine-3.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c25586b62480eb77ef3d953fba273209478e1ef49654592cd6a52a68dfe56a67", size = 34855, upload-time = "2025-12-17T23:32:42.817Z" }, - { url = "https://files.pythonhosted.org/packages/88/84/120a431fee50bc4c241425bee4d3a4910df4923b7ab5f7dff1bf0c772f08/time_machine-3.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6bf3a2fa738d15e0b95d14469a0b8ea42635467408d8b490e263d5d45c9a177f", size = 33222, upload-time = "2025-12-17T23:32:43.94Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ea/89cfda82bb8c57ff91bb9a26751aa234d6d90e9b4d5ab0ad9dce0f9f0329/time_machine-3.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ce76b82276d7ad2a66cdc85dad4df19d1422b69183170a34e8fbc4c3f35502f7", size = 34270, upload-time = "2025-12-17T23:32:45.037Z" }, - { url = "https://files.pythonhosted.org/packages/8a/aa/235357da4f69a51a8d35fcbfcfa77cdc7dc24f62ae54025006570bda7e2d/time_machine-3.2.0-cp314-cp314-win32.whl", hash = "sha256:14d6778273c543441863dff712cd1d7803dee946b18de35921eb8df10714539d", size = 17544, upload-time = "2025-12-17T23:32:46.099Z" }, - { url = "https://files.pythonhosted.org/packages/7b/51/6c8405a7276be79693b792cff22ce41067ec05db26a7d02f2d5b06324434/time_machine-3.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbf821da96dbc80d349fa9e7c36e670b41d68a878d28c8850057992fed430eef", size = 18423, upload-time = "2025-12-17T23:32:47.468Z" }, - { url = "https://files.pythonhosted.org/packages/d9/03/a3cf419e20c35fc203c6e4fed48b5b667c1a2b4da456d9971e605f73ecef/time_machine-3.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:71c75d71f8e68abc8b669bca26ed2ddd558430a6c171e32b8620288565f18c0e", size = 17050, upload-time = "2025-12-17T23:32:48.91Z" }, - { url = "https://files.pythonhosted.org/packages/86/a1/142de946dc4393f910bf4564b5c3ba819906e1f49b06c9cb557519c849e4/time_machine-3.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4e374779021446fc2b5c29d80457ec9a3b1a5df043dc2aae07d7c1415d52323c", size = 19991, upload-time = "2025-12-17T23:32:49.933Z" }, - { url = "https://files.pythonhosted.org/packages/ee/62/7f17def6289901f94726921811a16b9adce46e666362c75d45730c60274f/time_machine-3.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:122310a6af9c36e9a636da32830e591e7923e8a07bdd0a43276c3a36c6821c90", size = 15707, upload-time = "2025-12-17T23:32:50.969Z" }, - { url = "https://files.pythonhosted.org/packages/5d/d3/3502fb9bd3acb159c18844b26c43220201a0d4a622c0c853785d07699a92/time_machine-3.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba3eeb0f018cc362dd8128befa3426696a2e16dd223c3fb695fde184892d4d8c", size = 39207, upload-time = "2025-12-17T23:32:52.033Z" }, - { url = "https://files.pythonhosted.org/packages/5a/be/8b27f4aa296fda14a5a2ad7f588ddd450603c33415ab3f8e85b2f1a44678/time_machine-3.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:77d38ba664b381a7793f8786efc13b5004f0d5f672dae814430445b8202a67a6", size = 40764, upload-time = "2025-12-17T23:32:53.167Z" }, - { url = "https://files.pythonhosted.org/packages/42/cd/fe4c4e5c8ab6d48fab3624c32be9116fb120173a35fe67e482e5cf68b3d2/time_machine-3.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09abeb8f03f044d72712207e0489a62098ad3ad16dac38927fcf80baca4d6a7", size = 43508, upload-time = "2025-12-17T23:32:54.597Z" }, - { url = "https://files.pythonhosted.org/packages/b4/28/5a3ba2fce85b97655a425d6bb20a441550acd2b304c96b2c19d3839f721a/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b28367ce4f73987a55e230e1d30a57a3af85da8eb1a140074eb6e8c7e6ef19f", size = 41712, upload-time = "2025-12-17T23:32:55.781Z" }, - { url = "https://files.pythonhosted.org/packages/81/58/e38084be7fdabb4835db68a3a47e58c34182d79fc35df1ecbe0db2c5359f/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:903c7751c904581da9f7861c3015bed7cdc40047321291d3694a3cdc783bbca3", size = 38939, upload-time = "2025-12-17T23:32:56.867Z" }, - { url = "https://files.pythonhosted.org/packages/40/d0/ad3feb0a392ef4e0c08bc32024950373ddc0669002cbdcbb9f3bf0c2d114/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:528217cad85ede5f85c8bc78b0341868d3c3cfefc6ecb5b622e1cacb6c73247b", size = 39837, upload-time = "2025-12-17T23:32:58.283Z" }, - { url = "https://files.pythonhosted.org/packages/5b/9e/5f4b2ea63b267bd78f3245e76f5528836611b5f2d30b5e7300a722fe4428/time_machine-3.2.0-cp314-cp314t-win32.whl", hash = "sha256:75724762ffd517e7e80aaec1fad1ff5a7414bd84e2b3ee7a0bacfeb67c14926e", size = 18091, upload-time = "2025-12-17T23:32:59.403Z" }, - { url = "https://files.pythonhosted.org/packages/39/6f/456b1f4d2700ae02b19eba830f870596a4b89b74bac3b6c80666f1b108c5/time_machine-3.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2526abbd053c5bca898d1b3e7898eec34626b12206718d8c7ce88fd12c1c9c5c", size = 19208, upload-time = "2025-12-17T23:33:00.488Z" }, - { url = "https://files.pythonhosted.org/packages/2f/22/8063101427ecd3d2652aada4d21d0876b07a3dc789125bca2ee858fec3ed/time_machine-3.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7f2fb6784b414edbe2c0b558bfaab0c251955ba27edd62946cce4a01675a992c", size = 17359, upload-time = "2025-12-17T23:33:01.54Z" }, -] - -[[package]] -name = "traitlets" -version = "5.14.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, -] - -[[package]] -name = "types-aiofiles" -version = "25.1.0.20260409" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/66/9e62a2692792bc96c0f423f478149f4a7b84720704c546c8960b0a047c89/types_aiofiles-25.1.0.20260409.tar.gz", hash = "sha256:49e67d72bdcf9fe406f5815758a78dc34a1249bb5aa2adba78a80aec0a775435", size = 14812, upload-time = "2026-04-09T04:22:35.308Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/d0/28236f869ba4dfb223ecdbc267eb2bdb634b81a561dd992230a4f9ec48fa/types_aiofiles-25.1.0.20260409-py3-none-any.whl", hash = "sha256:923fedb532c772cc0f62e0ce4282725afa82ca5b41cabd9857f06b55e5eee8de", size = 14372, upload-time = "2026-04-09T04:22:34.328Z" }, -] - -[[package]] -name = "types-certifi" -version = "2021.10.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095, upload-time = "2022-06-09T15:19:05.244Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136, upload-time = "2022-06-09T15:19:03.127Z" }, -] - -[[package]] -name = "types-cffi" -version = "2.0.0.20260408" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "types-setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/64/67/eb4ef3408fdc0b4e5af38b30c0e6ad4663b41bdae9fb85a9f09a8db61a99/types_cffi-2.0.0.20260408.tar.gz", hash = "sha256:aa8b9c456ab715c079fc655929811f21f331bfb940f4a821987c581bf4e36230", size = 17541, upload-time = "2026-04-08T04:36:03.918Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/a3/7fbd93ededcc7c77e9e5948b9794161733ebdbf618a27965b1bea0e728a4/types_cffi-2.0.0.20260408-py3-none-any.whl", hash = "sha256:68bd296742b4ff7c0afe3547f50bd0acc55416ecf322ffefd2b7344ef6388a42", size = 20101, upload-time = "2026-04-08T04:36:02.995Z" }, -] - -[[package]] -name = "types-croniter" -version = "6.2.2.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c5/e4/89a0101471d6fe4e912dad24c54ae7afd90a9eaa5c74adef2c81f383f8da/types_croniter-6.2.2.20260408.tar.gz", hash = "sha256:a28a18908db371654990d30a3fd99856adc5137e475a23dbda4b10dce85525da", size = 12040, upload-time = "2026-04-08T04:27:20.068Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/05/b32e67944ff33e83c181cadf5835858d63f4292a2f2ff5bf6a1edb7f6fed/types_croniter-6.2.2.20260408-py3-none-any.whl", hash = "sha256:242087a5b6e201b7004e55f71ed34f466951b74551c64ef1c6a8a08c47d3cc0d", size = 9732, upload-time = "2026-04-08T04:27:19.229Z" }, -] - -[[package]] -name = "types-deprecated" -version = "1.3.1.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/db/076de3e81b106d3cec17aec9640ab1b2d02f29bad441de280459c161ce65/types_deprecated-1.3.1.20260408.tar.gz", hash = "sha256:62d6a86d0cc754c14bb2de31162d069b1c6a07ce11ee65e5258f8f75308eb3a3", size = 8524, upload-time = "2026-04-08T04:26:39.894Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/d0/d3258379deb749d949c3c72313981c9d2cceec518b87dcf506f022f5d49f/types_deprecated-1.3.1.20260408-py3-none-any.whl", hash = "sha256:b64e1eab560d4fa9394a27a3099211344b0e0f2f3ac8026d825c86e70d65cdd5", size = 9079, upload-time = "2026-04-08T04:26:38.752Z" }, -] - -[[package]] -name = "types-docutils" -version = "0.22.3.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3c/49/48a386fe15539556de085b87a69568b028cca2fa4b92596a3d4f79ac6784/types_docutils-0.22.3.20260408.tar.gz", hash = "sha256:22d5d45e4e0d65a1bc8280987a73e28669bb1cc9d16b18d0afc91713d1be26da", size = 57383, upload-time = "2026-04-08T04:27:26.924Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/47/1667fda6e9fcb044f8fb797f6dc4367b88dc2ab40f1a035e387f5405e870/types_docutils-0.22.3.20260408-py3-none-any.whl", hash = "sha256:2545a86966022cdf1468d430b0007eba0837be77974a7f3fafa1b04a6815d531", size = 91981, upload-time = "2026-04-08T04:27:25.934Z" }, -] - -[[package]] -name = "types-markdown" -version = "3.10.2.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/0e/a690840934c459aa50e0470e7550d7f151632eafa4a8e3c21d18009ad15c/types_markdown-3.10.2.20260408.tar.gz", hash = "sha256:d5cba15ed65a1420e80e31c17e3d4a2ad7208a3f3a4da97fd2c5f093caf523cd", size = 19784, upload-time = "2026-04-08T04:33:07.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/7e/265a8df257c8dced6ea89295f793a19f0a49ccbfeae1ed562368b2caf7a3/types_markdown-3.10.2.20260408-py3-none-any.whl", hash = "sha256:b0bbe8b7a8174db732067b86e391262898f5f536589ea81efec6d35ceb829331", size = 25857, upload-time = "2026-04-08T04:33:06.769Z" }, -] - -[[package]] -name = "types-paramiko" -version = "4.0.0.20260408" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/f5/2a556b03ba264508b6bc6a65131500265f210ff3ebf5d76dbe51b53c3979/types_paramiko-4.0.0.20260408.tar.gz", hash = "sha256:978191a2e11064fa4c7f9ada0fccf49159a17beb98b780310dd2c2d2b4106063", size = 29116, upload-time = "2026-04-08T04:35:04.631Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/e2/cf451598a6a8820139d021b2be08a836b9b905d744bcc73b72172e7e10b3/types_paramiko-4.0.0.20260408-py3-none-any.whl", hash = "sha256:350bf53edb4eb88181be68854d598e1cc3a8764fe905d49913025b86e831adbc", size = 38816, upload-time = "2026-04-08T04:35:03.503Z" }, -] - -[[package]] -name = "types-protobuf" -version = "7.34.1.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/b1/4521e68c2cc17703d80eb42796751345376dd4c706f84007ef5e7c707774/types_protobuf-7.34.1.20260408.tar.gz", hash = "sha256:e2c0a0430e08c75b52671a6f0035abfdcc791aad12af16274282de1b721758ab", size = 68835, upload-time = "2026-04-08T04:26:43.613Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/b5/0bc9874d89c58fb0ce851e150055ce732d254dbb10b06becbc7635d0d635/types_protobuf-7.34.1.20260408-py3-none-any.whl", hash = "sha256:ebbcd4e27b145aef6a59bc0cb6c013b3528151c1ba5e7f7337aeee355d276a5e", size = 86012, upload-time = "2026-04-08T04:26:42.566Z" }, -] - -[[package]] -name = "types-pymysql" -version = "1.1.0.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/04/c3570f05ebab083f28698c829dddf754ffefc30aae4e29915610848e44db/types_pymysql-1.1.0.20260408.tar.gz", hash = "sha256:b784dc37908479e3767e2d794ab507b3674adb1c686ca3d13fc9e2960dbcb9ec", size = 22344, upload-time = "2026-04-08T04:27:47.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/b3/15dee33878709705a4cc83bcc1bb30e00e95bbe038b472cb1207a15b50a1/types_pymysql-1.1.0.20260408-py3-none-any.whl", hash = "sha256:da630647eaaa7a926a3907794f4067f269cd245b2c202c74aa3c6a3bd660a9db", size = 23071, upload-time = "2026-04-08T04:27:46.735Z" }, -] - -[[package]] -name = "types-pyopenssl" -version = "24.1.0.20240722" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "types-cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458, upload-time = "2024-07-22T02:32:22.558Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499, upload-time = "2024-07-22T02:32:21.232Z" }, -] - -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/f3/2427775f80cd5e19a0a71ba8e5ab7645a01a852f43a5fd0ffc24f66338e0/types_python_dateutil-2.9.0.20260408.tar.gz", hash = "sha256:8b056ec01568674235f64ecbcef928972a5fac412f5aab09c516dfa2acfbb582", size = 16981, upload-time = "2026-04-08T04:28:10.995Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/c6/eeba37bfee282a6a97f889faef9352d6172c6a5088eb9a4daf570d9d748d/types_python_dateutil-2.9.0.20260408-py3-none-any.whl", hash = "sha256:473139d514a71c9d1fbd8bb328974bedcb1cc3dba57aad04ffa4157f483c216f", size = 18437, upload-time = "2026-04-08T04:28:10.095Z" }, -] - -[[package]] -name = "types-python-slugify" -version = "8.0.2.20240310" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d2/cb/7fdc1982b117d216a9ebbe4ecc6619690991c48f994d93dee7888f459976/types-python-slugify-8.0.2.20240310.tar.gz", hash = "sha256:5157b508c7fed587520c70d77f62aea0fafdc6620893c2ec8972f13a1faf5560", size = 3661, upload-time = "2024-03-10T02:19:03.582Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/6d/873300a60133e51b284a5287a618d3d948a65160e3954b90b3dc5e562667/types_python_slugify-8.0.2.20240310-py3-none-any.whl", hash = "sha256:0efec18b802c69ebd22dcee55c91afaeaa80e1e40ddd66ccabf69fd42ce87b74", size = 3566, upload-time = "2024-03-10T02:19:02.575Z" }, -] - -[[package]] -name = "types-pytz" -version = "2026.1.1.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/b7/33f5a4f29b1f285b99ff79a607751a7996194cbb98705e331dab7a2daa28/types_pytz-2026.1.1.20260408.tar.gz", hash = "sha256:89b6a34b9198ea2a4b98a9d15cbca987053f52a105fd44f7ce3789cae4349408", size = 10788, upload-time = "2026-04-08T04:28:14.54Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/90/12c059e6bb330a22d9cc97daf027ac7fb7f50fbf518e4d88185b4d39120e/types_pytz-2026.1.1.20260408-py3-none-any.whl", hash = "sha256:c7e4dec76221fb7d0c97b91ad8561d689bebe39b6bcb7b728387e7ffd8cde788", size = 10124, upload-time = "2026-04-08T04:28:13.353Z" }, -] - -[[package]] -name = "types-pyyaml" -version = "6.0.12.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/73/b759b1e413c31034cc01ecdfb96b38115d0ab4db55a752a3929f0cd449fd/types_pyyaml-6.0.12.20260408.tar.gz", hash = "sha256:92a73f2b8d7f39ef392a38131f76b970f8c66e4c42b3125ae872b7c93b556307", size = 17735, upload-time = "2026-04-08T04:30:50.974Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/f0/c391068b86abb708882c6d75a08cd7d25b2c7227dab527b3a3685a3c635b/types_pyyaml-6.0.12.20260408-py3-none-any.whl", hash = "sha256:fbc42037d12159d9c801ebfcc79ebd28335a7c13b08a4cfbc6916df78fee9384", size = 20339, upload-time = "2026-04-08T04:30:50.113Z" }, -] - -[[package]] -name = "types-redis" -version = "4.6.0.20241004" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "types-pyopenssl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/95/c054d3ac940e8bac4ca216470c80c26688a0e79e09f520a942bb27da3386/types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", size = 49679, upload-time = "2024-10-04T02:43:59.224Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/82/7d25dce10aad92d2226b269bce2f85cfd843b4477cd50245d7d40ecf8f89/types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed", size = 58737, upload-time = "2024-10-04T02:43:57.968Z" }, -] - -[[package]] -name = "types-requests" -version = "2.33.0.20260408" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/6a/749dc53a54a3f35842c1f8197b3ca6b54af6d7458a1bfc75f6629b6da666/types_requests-2.33.0.20260408.tar.gz", hash = "sha256:95b9a86376807a216b2fb412b47617b202091c3ea7c078f47cc358d5528ccb7b", size = 23882, upload-time = "2026-04-08T04:34:49.33Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/b8/78fd6c037de4788c040fdd323b3369804400351b7827473920f6c1d03c10/types_requests-2.33.0.20260408-py3-none-any.whl", hash = "sha256:81f31d5ea4acb39f03be7bc8bed569ba6d5a9c5d97e89f45ac43d819b68ca50f", size = 20739, upload-time = "2026-04-08T04:34:48.325Z" }, -] - -[[package]] -name = "types-setuptools" -version = "82.0.0.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/12/3464b410c50420dd4674fa5fe9d3880711c1dbe1a06f5fe4960ee9067b9e/types_setuptools-82.0.0.20260408.tar.gz", hash = "sha256:036c68caf7e672a699f5ebbf914708d40644c14e05298bc49f7272be91cf43d3", size = 44861, upload-time = "2026-04-08T04:29:33.292Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/e1/46a4fc3ef03aabf5d18bac9df5cf37c6b02c3bddf3e05c3533f4b4588331/types_setuptools-82.0.0.20260408-py3-none-any.whl", hash = "sha256:ece0a215cdfa6463a65fd6f68bd940f39e455729300ddfe61cab1147ed1d2462", size = 68428, upload-time = "2026-04-08T04:29:32.175Z" }, -] - -[[package]] -name = "types-tabulate" -version = "0.10.0.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/59/b563bfb6e216b8573052c09cb4abcbdca836487db4cfad9b7d492c327c0b/types_tabulate-0.10.0.20260408.tar.gz", hash = "sha256:903d62fdf7e5a0ff659fd5d629df716232f7658c6d30e98f0374488d06ffacf4", size = 8367, upload-time = "2026-04-08T04:30:00.482Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/d1/34e27f543dd944f51fc6b0013a1a41113079cede9cc3be0a5f426f2f8d9d/types_tabulate-0.10.0.20260408-py3-none-any.whl", hash = "sha256:2b19d193603d38c34645de53c0c1087e2364487d518d4a2f44268db2366723cc", size = 8139, upload-time = "2026-04-08T04:29:59.699Z" }, -] - -[[package]] -name = "types-toml" -version = "0.10.8.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/9b/887564a51a84c96ba08b715570e546f0ea793df6372b736bfbc596ca5536/types_toml-0.10.8.20260408.tar.gz", hash = "sha256:6b30b031235565a12febb1388900b129f1adeabfcfa594da46d0372b2ac107ad", size = 9341, upload-time = "2026-04-08T04:27:54.394Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/f1/942d95ba026779bc6e3064f8b094216588dc3276cc328cf8e03a0541918d/types_toml-0.10.8.20260408-py3-none-any.whl", hash = "sha256:e958d4c660385e548705a298f17dc162baf44c8b6d6aff79aeefe75f4f77ac87", size = 9677, upload-time = "2026-04-08T04:27:53.526Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - -[[package]] -name = "wcwidth" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, -] - -[[package]] -name = "wheel" -version = "0.46.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/89/24/a2eb353a6edac9a0303977c4cb048134959dd2a51b48a269dfc9dde00c8a/wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803", size = 60605, upload-time = "2026-01-22T12:39:49.136Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", size = 30557, upload-time = "2026-01-22T12:39:48.099Z" }, -] - -[[package]] -name = "yamllint" -version = "1.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pathspec" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/a0/8fc2d68e132cf918f18273fdc8a1b8432b60d75ac12fdae4b0ef5c9d2e8d/yamllint-1.38.0.tar.gz", hash = "sha256:09e5f29531daab93366bb061e76019d5e91691ef0a40328f04c927387d1d364d", size = 142446, upload-time = "2026-01-13T07:47:53.276Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/92/aed08e68de6e6a3d7c2328ce7388072cd6affc26e2917197430b646aed02/yamllint-1.38.0-py3-none-any.whl", hash = "sha256:fc394a5b3be980a4062607b8fdddc0843f4fa394152b6da21722f5d59013c220", size = 68940, upload-time = "2026-01-13T07:47:51.343Z" }, -] - -[[package]] -name = "yarl" -version = "1.23.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, - { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, - { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, -] diff --git a/task-sdk/pyproject.toml b/task-sdk/pyproject.toml index ccdab19e5b1aa..5937011e8d243 100644 --- a/task-sdk/pyproject.toml +++ b/task-sdk/pyproject.toml @@ -144,7 +144,6 @@ path = "src/airflow/sdk/__init__.py" "../shared/secrets_masker/src/airflow_shared/secrets_masker" = "src/airflow/sdk/_shared/secrets_masker" "../shared/serialization/src/airflow_shared/serialization" = "src/airflow/sdk/_shared/serialization" "../shared/timezones/src/airflow_shared/timezones" = "src/airflow/sdk/_shared/timezones" -"../shared/workloads/src/airflow_shared/workloads" = "src/airflow/sdk/_shared/workloads" "../shared/listeners/src/airflow_shared/listeners" = "src/airflow/sdk/_shared/listeners" "../shared/plugins_manager/src/airflow_shared/plugins_manager" = "src/airflow/sdk/_shared/plugins_manager" "../shared/providers_discovery/src/airflow_shared/providers_discovery" = "src/airflow/sdk/_shared/providers_discovery" @@ -321,7 +320,6 @@ shared_distributions = [ "apache-airflow-shared-serialization", "apache-airflow-shared-state", "apache-airflow-shared-timezones", - "apache-airflow-shared-workloads", "apache-airflow-shared-observability", "apache-airflow-shared-plugins-manager", "apache-airflow-shared-providers-discovery", diff --git a/task-sdk/src/airflow/sdk/_shared/workloads b/task-sdk/src/airflow/sdk/_shared/workloads deleted file mode 120000 index 25036f1ff8e02..0000000000000 --- a/task-sdk/src/airflow/sdk/_shared/workloads +++ /dev/null @@ -1 +0,0 @@ -../../../../../shared/workloads/src/airflow_shared/workloads \ No newline at end of file diff --git a/task-sdk/src/airflow/sdk/execution_time/comms.py b/task-sdk/src/airflow/sdk/execution_time/comms.py index 7208963fe0ff2..b9678c79e291b 100644 --- a/task-sdk/src/airflow/sdk/execution_time/comms.py +++ b/task-sdk/src/airflow/sdk/execution_time/comms.py @@ -64,7 +64,6 @@ import structlog from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, JsonValue, TypeAdapter -from airflow.sdk._shared.workloads import TaskInstanceDTO # noqa: TC001 -- Pydantic needs this at runtime from airflow.sdk.api.datamodels._generated import ( AssetEventDagRunReference, AssetEventResponse, @@ -98,6 +97,10 @@ XComSequenceSliceResponse, ) from airflow.sdk.exceptions import ErrorType +from airflow.sdk.execution_time.workloads.task import ( + # Pydantic needs this at runtime since we don't model_rebuild() StartupDetails. + TaskInstanceDTO, # noqa: TC001 +) try: from socket import recv_fds diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index c3d0594bebe14..5f411e643ed5f 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -52,9 +52,9 @@ from structlog.typing import FilteringBoundLogger from typing_extensions import Self - from airflow.sdk._shared.workloads import TaskInstanceDTO from airflow.sdk.api.datamodels._generated import BundleInfo from airflow.sdk.execution_time.comms import StartupDetails + from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO def _start_server() -> socket.socket: diff --git a/task-sdk/src/airflow/sdk/execution_time/supervisor.py b/task-sdk/src/airflow/sdk/execution_time/supervisor.py index 822e9700da81b..d505c3111c17b 100644 --- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py +++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py @@ -167,11 +167,11 @@ from structlog.typing import FilteringBoundLogger, WrappedLogger from typing_extensions import Self - from airflow._shared.workloads import TaskInstanceDTO from airflow.executors.workloads import BundleInfo from airflow.sdk.bases.secrets_backend import BaseSecretsBackend from airflow.sdk.definitions.connection import Connection from airflow.sdk.execution_time.selector_loop import SelectorCallback + from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO from airflow.sdk.types import RuntimeTaskInstanceProtocol as RuntimeTI __all__ = ["ActivitySubprocess", "WatchedSubprocess", "supervise", "supervise_task"] diff --git a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py index 4dfaa77dfd478..344bebf5cd3e4 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py +++ b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py @@ -52,7 +52,6 @@ from task_sdk import FAKE_BUNDLE, make_client from uuid6 import uuid7 -from airflow._shared.workloads import TaskInstanceDTO from airflow.executors.workloads import BundleInfo from airflow.sdk import DAG, BaseOperator, timezone from airflow.sdk.api import client as sdk_client @@ -169,6 +168,7 @@ supervise_task, ) from airflow.sdk.execution_time.task_runner import run +from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO from tests_common.test_utils.config import conf_vars diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py index 7ed02ce5e78d7..0fccb92573d28 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py +++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py @@ -170,6 +170,7 @@ run, startup, ) +from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO from airflow.sdk.execution_time.xcom import XCom from airflow.sdk.serde import deserialize from airflow.triggers.base import BaseEventTrigger, BaseTrigger, TriggerEvent From 664baa2ad8f7cc7150a84ff3fa6023e361e317aa Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 6 May 2026 19:55:52 +0800 Subject: [PATCH 48/75] Move sdk.java out of provider as apache-airflow-coordinators-java distribution --- .dockerignore | 1 + .../ISSUE_TEMPLATE/1-airflow_bug_report.yml | 1 - .github/boring-cyborg.yml | 3 - .../src/airflow/config_templates/config.yml | 32 ++- .../src/airflow/dag_processing/manager.py | 9 +- .../src/airflow/dag_processing/processor.py | 54 ++-- airflow-core/src/airflow/models/dagcode.py | 14 +- .../src/airflow/provider.yaml.schema.json | 7 - .../src/airflow/provider_info.schema.json | 7 - airflow-core/src/airflow/providers_manager.py | 28 --- .../unit/always/test_providers_manager.py | 28 --- dev/breeze/doc/images/output_build-docs.svg | 4 +- dev/breeze/doc/images/output_build-docs.txt | 2 +- ...release-management_add-back-references.svg | 4 +- ...release-management_add-back-references.txt | 2 +- ...ement_generate-issue-content-providers.svg | 6 +- ...ement_generate-issue-content-providers.txt | 2 +- ...management_generate-providers-metadata.svg | 30 +-- ...management_generate-providers-metadata.txt | 2 +- ...agement_prepare-provider-distributions.svg | 6 +- ...agement_prepare-provider-distributions.txt | 2 +- ...agement_prepare-provider-documentation.svg | 6 +- ...agement_prepare-provider-documentation.txt | 2 +- ...output_release-management_publish-docs.svg | 4 +- ...output_release-management_publish-docs.txt | 2 +- ...t_sbom_generate-providers-requirements.svg | 6 +- ...t_sbom_generate-providers-requirements.txt | 2 +- .../output_workflow-run_publish-docs.svg | 4 +- .../output_workflow-run_publish-docs.txt | 2 +- .../src/airflow_breeze/global_constants.py | 8 +- .../utils/docker_command_utils.py | 1 + .../includes/sections-and-options.rst | 2 +- providers/sdk/java/README.rst | 60 ----- .../sdk/java/docs/.latest-doc-only-change.txt | 1 - providers/sdk/java/docs/changelog.rst | 40 --- providers/sdk/java/docs/commits.rst | 35 --- providers/sdk/java/docs/conf.py | 27 -- .../sdk/java/docs/configurations-ref.rst | 19 -- providers/sdk/java/docs/index.rst | 123 ---------- .../installing-providers-from-sources.rst | 18 -- providers/sdk/java/docs/security.rst | 18 -- providers/sdk/java/provider.yaml | 58 ----- providers/sdk/java/pyproject.toml | 131 ---------- .../java/src/airflow/providers/__init__.py | 17 -- .../src/airflow/providers/sdk/__init__.py | 17 -- .../airflow/providers/sdk/java/__init__.py | 39 --- .../providers/sdk/java/get_provider_info.py | 47 ---- .../tests/unit/sdk/java/test_java_provider.py | 58 ----- pyproject.toml | 11 +- scripts/ci/docker-compose/local.yml | 3 + scripts/ci/docker-compose/remove-sources.yml | 2 +- scripts/ci/docker-compose/tests-sources.yml | 2 +- .../install_airflow_and_providers.py | 1 - .../sdk => sdk/coordinators}/java/.gitignore | 0 .../sdk => sdk/coordinators}/java/LICENSE | 0 .../sdk => sdk/coordinators}/java/NOTICE | 0 sdk/coordinators/java/README.rst | 51 ++++ sdk/coordinators/java/pyproject.toml | 108 ++++++++ .../airflow/sdk/coordinators/java/__init__.py | 10 +- .../sdk/coordinators}/java/bundle_scanner.py | 8 +- .../sdk/coordinators}/java/coordinator.py | 76 ++++-- .../coordinators/java/tests}/__init__.py | 0 .../coordinators/java/tests/unit}/__init__.py | 0 .../java/tests/unit/coordinators}/__init__.py | 1 - .../tests/unit/coordinators/java}/__init__.py | 1 - .../coordinators}/java/test_bundle_scanner.py | 11 +- .../coordinators}/java/test_coordinator.py | 136 +++++----- task-sdk/src/airflow/sdk/__init__.py | 5 + .../airflow/sdk/execution_time/coordinator.py | 196 ++++++++++----- .../airflow/sdk/execution_time/task_runner.py | 91 +++---- .../airflow/sdk/providers_manager_runtime.py | 28 --- .../execution_time/test_coordinator.py | 232 +++++++++++------- .../test_providers_manager_runtime.py | 27 -- uv.lock | 34 +++ 74 files changed, 766 insertions(+), 1259 deletions(-) delete mode 100644 providers/sdk/java/README.rst delete mode 100644 providers/sdk/java/docs/.latest-doc-only-change.txt delete mode 100644 providers/sdk/java/docs/changelog.rst delete mode 100644 providers/sdk/java/docs/commits.rst delete mode 100644 providers/sdk/java/docs/conf.py delete mode 100644 providers/sdk/java/docs/configurations-ref.rst delete mode 100644 providers/sdk/java/docs/index.rst delete mode 100644 providers/sdk/java/docs/installing-providers-from-sources.rst delete mode 100644 providers/sdk/java/docs/security.rst delete mode 100644 providers/sdk/java/provider.yaml delete mode 100644 providers/sdk/java/pyproject.toml delete mode 100644 providers/sdk/java/src/airflow/providers/__init__.py delete mode 100644 providers/sdk/java/src/airflow/providers/sdk/__init__.py delete mode 100644 providers/sdk/java/src/airflow/providers/sdk/java/__init__.py delete mode 100644 providers/sdk/java/src/airflow/providers/sdk/java/get_provider_info.py delete mode 100644 providers/sdk/java/tests/unit/sdk/java/test_java_provider.py rename {providers/sdk => sdk/coordinators}/java/.gitignore (100%) rename {providers/sdk => sdk/coordinators}/java/LICENSE (100%) rename {providers/sdk => sdk/coordinators}/java/NOTICE (100%) create mode 100644 sdk/coordinators/java/README.rst create mode 100644 sdk/coordinators/java/pyproject.toml rename providers/sdk/java/tests/conftest.py => sdk/coordinators/java/src/airflow/sdk/coordinators/java/__init__.py (79%) rename {providers/sdk/java/src/airflow/providers/sdk => sdk/coordinators/java/src/airflow/sdk/coordinators}/java/bundle_scanner.py (95%) rename {providers/sdk/java/src/airflow/providers/sdk => sdk/coordinators/java/src/airflow/sdk/coordinators}/java/coordinator.py (62%) rename {providers/sdk/java/tests/unit/sdk => sdk/coordinators/java/tests}/__init__.py (100%) rename {providers/sdk/java/tests/unit/sdk/java => sdk/coordinators/java/tests/unit}/__init__.py (100%) rename {providers/sdk/java/tests/unit => sdk/coordinators/java/tests/unit/coordinators}/__init__.py (92%) rename {providers/sdk/java/src/airflow => sdk/coordinators/java/tests/unit/coordinators/java}/__init__.py (92%) rename {providers/sdk/java/tests/unit/sdk => sdk/coordinators/java/tests/unit/coordinators}/java/test_bundle_scanner.py (96%) rename {providers/sdk/java/tests/unit/sdk => sdk/coordinators/java/tests/unit/coordinators}/java/test_coordinator.py (65%) diff --git a/.dockerignore b/.dockerignore index df08c066ce3b4..368d437dcfd1a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -39,6 +39,7 @@ !task-sdk/ !airflow-ctl/ !go-sdk/ +!sdk/ # Add all "test" distributions !tests diff --git a/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml b/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml index e5060dbe3671d..a9c8978740344 100644 --- a/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml @@ -192,7 +192,6 @@ body: - redis - salesforce - samba - - sdk-java - segment - sendgrid - sftp diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml index 3f5477f4512c5..21b44a9fb840f 100644 --- a/.github/boring-cyborg.yml +++ b/.github/boring-cyborg.yml @@ -189,9 +189,6 @@ labelPRBasedOnFilePath: provider:keycloak: - providers/keycloak/** - provider:sdk-java: - - providers/sdk/java/** - provider:microsoft-azure: - providers/microsoft/azure/** diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml index b2e1e7eeef819..e40164194aa67 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -2007,17 +2007,39 @@ workers: sdk: description: Settings for non-Python SDK runtime coordination options: - queue_to_sdk: + coordinators: description: | - JSON mapping of queue names to SDK runtime coordinator names. + JSON list of runtime coordinator entries. + + Each entry is an object with ``name``, ``classpath`` and optional + ``kwargs``. ``classpath`` is resolved via ``import_string`` and + constructed with ``kwargs`` once per process. Entries are + independent instances, so the same ``classpath`` can be configured + multiple times with different ``kwargs`` (for example, two + ``JavaCoordinator`` instances pinned to different JDK versions). + version_added: 3.1.7 + type: string + example: | + [ + { + "name": "jdk-17", + "classpath": "airflow.sdk.coordinators.java.JavaCoordinator", + "kwargs": {"java_executable": "/usr/lib/jvm/java-17-openjdk/bin/java", "jvm_args": ["-Xmx1024m"]} + } + ] + default: ~ + queue_to_coordinator: + description: | + JSON mapping of queue names to coordinator ``name`` from + ``[sdk] coordinators``. When a task's ``language`` field is not set, this mapping is checked - to route the task to a non-Python runtime coordinator based on its + to route the task to a configured coordinator instance based on its queue. This is useful when queues are used as environment or - isolation identifiers (e.g. ``foo``, ``bar``). + isolation identifiers (e.g. ``legacy-java``, ``modern-java``). version_added: 3.1.7 type: string - example: '{"foo": "java", "bar": "java", "go-queue": "go"}' + example: '{"legacy-java": "jdk-11", "modern-java": "jdk-17"}' default: ~ api_auth: description: Settings relating to authentication on the Airflow APIs diff --git a/airflow-core/src/airflow/dag_processing/manager.py b/airflow-core/src/airflow/dag_processing/manager.py index c80552bca890a..1283e313a5ad3 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -869,16 +869,13 @@ def _find_files_in_bundle(self, bundle: BaseDagBundle) -> list[Path]: return rel_paths def _get_runtime_file_extensions(self) -> tuple[str, ...]: - """Collect file extensions from registered runtime coordinators (cached after first call).""" + """Collect file extensions from configured runtime coordinators (cached after first call).""" if self._runtime_file_extensions is not None: return self._runtime_file_extensions - from airflow.providers_manager import ProvidersManager + from airflow.sdk.execution_time.coordinator import get_coordinator_manager - extensions: list[str] = [] - for coordinator_cls in ProvidersManager().coordinators: - extensions.append(coordinator_cls.file_extension) - self._runtime_file_extensions = tuple(extensions) + self._runtime_file_extensions = get_coordinator_manager().file_extensions() return self._runtime_file_extensions def _get_observed_filelocs(self, present: set[DagFileInfo]) -> set[str]: diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index cc77833cf7a77..2c0f80e9e3712 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -572,12 +572,12 @@ def start( # type: ignore[override] ) -> Self: logger = kwargs["logger"] - # Check if a provider-registered runtime coordinator should handle this file - logger.debug("Checking for provider-registered runtime coordinator entrypoint for file", path=path) + # Check if a configured runtime coordinator should handle this file + logger.debug("Checking for runtime coordinator entrypoint for file", path=path) resolved_target = cls._resolve_processor_target(path, bundle_name, bundle_path, logger) if resolved_target is not None: target = resolved_target - logger.debug("Resolved provider-registered runtime coordinator entrypoint for file", path=path) + logger.debug("Resolved runtime coordinator entrypoint for file", path=path) else: _pre_import_airflow_modules(os.fspath(path), logger) @@ -600,44 +600,26 @@ def _resolve_processor_target( log: FilteringBoundLogger, ) -> Callable[[], None] | None: """ - Return the entrypoint of the first provider runtime coordinator that can handle *path*. + Return the entrypoint of the first runtime coordinator that can handle *path*. The returned callable is a ``functools.partial`` that binds *path*, *bundle_name* and *bundle_path* so the supervisor can pass it as a no-arg ``target`` to ``WatchedSubprocess.start``. """ - from airflow.providers_manager import ProvidersManager - - for coordinator_cls in ProvidersManager().coordinators: - try: - log.debug( - "Checking runtime coordinator %s for file %s", - coordinator_cls, - path, - ) - if coordinator_cls.can_handle_dag_file(bundle_name, path): - log.debug( - "Using runtime coordinator %s for file %s", - coordinator_cls, - path, - ) - return functools.partial( - coordinator_cls.run_dag_parsing, - path=os.fspath(path), - bundle_name=bundle_name, - bundle_path=os.fspath(bundle_path), - ) - log.debug( - "Runtime coordinator %s cannot handle file %s with bundle name %s", - coordinator_cls, - path, - bundle_name, - ) - except Exception: - log.warning("Failed to check runtime coordinator %s", coordinator_cls, exc_info=True) - - log.debug("No runtime coordinator found for file %s, using default processor", path) - return None + from airflow.sdk.execution_time.coordinator import get_coordinator_manager + + coordinator = get_coordinator_manager().for_dag_file(bundle_name, path) + if coordinator is None: + log.debug("No runtime coordinator found for file %s, using default processor", path) + return None + + log.debug("Using runtime coordinator %s for file %s", type(coordinator).__qualname__, path) + return functools.partial( + coordinator.run_dag_parsing, + path=os.fspath(path), + bundle_name=bundle_name, + bundle_path=os.fspath(bundle_path), + ) def _on_child_started( self, diff --git a/airflow-core/src/airflow/models/dagcode.py b/airflow-core/src/airflow/models/dagcode.py index 528859f4cd311..cdec5baa95717 100644 --- a/airflow-core/src/airflow/models/dagcode.py +++ b/airflow-core/src/airflow/models/dagcode.py @@ -119,14 +119,12 @@ def code(cls, dag_id, session: Session = NEW_SESSION) -> str: @staticmethod def get_code_from_file(fileloc): - # Try from runtime coordinator first (classes are pre-loaded by ProvidersManager) - from airflow.providers_manager import ProvidersManager - - for coordinator_cls in ProvidersManager().coordinators: - # TODO: Perhaps the `can_handle_dag_file` interface should just accept `path` only? - # Or maybe we can have different granularity for this. that 1 with bundle + path, another with just path - if coordinator_cls.can_handle_dag_file("", fileloc): - return coordinator_cls.get_code_from_file(fileloc) + # Try from runtime coordinator first. + from airflow.sdk.execution_time.coordinator import get_coordinator_manager + + coordinator = get_coordinator_manager().for_dag_file("", fileloc) + if coordinator is not None: + return coordinator.get_code_from_file(fileloc) # Then fallback to python native try: diff --git a/airflow-core/src/airflow/provider.yaml.schema.json b/airflow-core/src/airflow/provider.yaml.schema.json index 1c41b906289cf..5714b8db658c5 100644 --- a/airflow-core/src/airflow/provider.yaml.schema.json +++ b/airflow-core/src/airflow/provider.yaml.schema.json @@ -624,13 +624,6 @@ } } }, - "coordinators": { - "type": "array", - "description": "Runtime Coordinator class names (BaseCoordinator subclasses)", - "items": { - "type": "string" - } - }, "source-date-epoch": { "type": "integer", "description": "Source date epoch - seconds since epoch (gmtime) when the release documentation was prepared. Used to generate reproducible package builds with flint.", diff --git a/airflow-core/src/airflow/provider_info.schema.json b/airflow-core/src/airflow/provider_info.schema.json index 92601fc58af74..86fc726a05168 100644 --- a/airflow-core/src/airflow/provider_info.schema.json +++ b/airflow-core/src/airflow/provider_info.schema.json @@ -446,13 +446,6 @@ "type": "string" } } - }, - "coordinators": { - "type": "array", - "description": "Runtime Coordinator class names (BaseCoordinator subclasses)", - "items": { - "type": "string" - } } }, "definitions": { diff --git a/airflow-core/src/airflow/providers_manager.py b/airflow-core/src/airflow/providers_manager.py index 8945589b4b046..6fefcbc39b06d 100644 --- a/airflow-core/src/airflow/providers_manager.py +++ b/airflow-core/src/airflow/providers_manager.py @@ -41,7 +41,6 @@ if TYPE_CHECKING: from airflow.cli.cli_config import CLICommand - from airflow.sdk.execution_time.coordinator import BaseCoordinator log = logging.getLogger(__name__) @@ -449,7 +448,6 @@ def __init__(self): ) # Set of plugins contained in providers self._plugins_set: set[PluginInfo] = set() - self._coordinators: list[type[BaseCoordinator]] = [] self._init_airflow_core_hooks() self._runtime_manager = None @@ -627,12 +625,6 @@ def initialize_providers_configuration(self): self.initialize_providers_list() self._discover_config() - @provider_info_cache("coordinators") - def initialize_providers_coordinators(self): - """Lazy initialization of providers runtime coordinators.""" - self.initialize_providers_list() - self._discover_coordinators() - @provider_info_cache("plugins") def initialize_providers_plugins(self): self.initialize_providers_list() @@ -1288,19 +1280,6 @@ def _discover_config(self) -> None: if provider.data.get("config"): self._provider_configs[provider_package] = provider.data.get("config") # type: ignore[assignment] - def _discover_coordinators(self) -> None: - """Retrieve and pre-load all coordinators defined in the providers.""" - seen: set[str] = set() - for provider_package, provider in self._provider_dict.items(): - for coordinator_class_path in provider.data.get("coordinators", []): - if coordinator_class_path in seen: - continue - coordinator_cls = _correctness_check(provider_package, coordinator_class_path, provider) - if coordinator_cls: - seen.add(coordinator_class_path) - self._coordinators.append(coordinator_cls) - self._coordinators = sorted(self._coordinators, key=lambda c: c.__qualname__) - def _discover_plugins(self) -> None: """Retrieve all plugins defined in the providers.""" for provider_package, provider in self._provider_dict.items(): @@ -1498,12 +1477,6 @@ def db_managers(self) -> list[str]: self.initialize_providers_db_managers() return sorted(self._db_manager_class_name_set) - @property - def coordinators(self) -> list[type[BaseCoordinator]]: - """Returns pre-loaded coordinator classes available in providers.""" - self.initialize_providers_coordinators() - return self._coordinators - @property def filesystem_module_names(self) -> list[str]: self.initialize_providers_filesystems() @@ -1575,7 +1548,6 @@ def _cleanup(self): self._trigger_info_set.clear() self._notification_info_set.clear() self._plugins_set.clear() - self._coordinators.clear() self._cli_command_functions_set.clear() self._cli_command_provider_name_set.clear() diff --git a/airflow-core/tests/unit/always/test_providers_manager.py b/airflow-core/tests/unit/always/test_providers_manager.py index b13930c98d1c2..afa473e80a4f0 100644 --- a/airflow-core/tests/unit/always/test_providers_manager.py +++ b/airflow-core/tests/unit/always/test_providers_manager.py @@ -258,34 +258,6 @@ def test_dialects(self): assert len(dialect_class_names) == 3 assert dialect_class_names == ["default", "mssql", "postgresql"] - @patch("airflow.providers_manager.import_string") - def test_coordinators(self, mock_import_string): - class ACoordinator: - pass - - class ZCoordinator: - pass - - mock_import_string.side_effect = lambda path: { - "airflow.providers.sdk.java.coordinator.ACoordinator": ACoordinator, - "airflow.providers.sdk.java.coordinator.ZCoordinator": ZCoordinator, - }[path] - providers_manager = ProvidersManager() - providers_manager._provider_dict = LazyDictWithCache() - providers_manager._provider_dict["apache-airflow-providers-sdk-java"] = ProviderInfo( - version="0.0.1", - data={ - "coordinators": [ - "airflow.providers.sdk.java.coordinator.ZCoordinator", - "airflow.providers.sdk.java.coordinator.ACoordinator", - "airflow.providers.sdk.java.coordinator.ZCoordinator", - ] - }, - ) - - with patch.object(providers_manager, "initialize_providers_list"): - assert providers_manager.coordinators == [ACoordinator, ZCoordinator] - class TestWithoutCheckProviderManager: @pytest.fixture(autouse=True) diff --git a/dev/breeze/doc/images/output_build-docs.svg b/dev/breeze/doc/images/output_build-docs.svg index 2a0812c05163e..1858bbb097e91 100644 --- a/dev/breeze/doc/images/output_build-docs.svg +++ b/dev/breeze/doc/images/output_build-docs.svg @@ -240,8 +240,8 @@ | hashicorp | helm-chart | http | imap | influxdb | informatica | jdbc | jenkins | keycloak | microsoft.azure |        microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai | openfaas | openlineage |  opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres | presto | qdrant | redis |    -salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh |    -standard | tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...  +salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh | standard |    +tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...             Build documents. diff --git a/dev/breeze/doc/images/output_build-docs.txt b/dev/breeze/doc/images/output_build-docs.txt index 247bee9e56ff6..54d8d4e3f39bb 100644 --- a/dev/breeze/doc/images/output_build-docs.txt +++ b/dev/breeze/doc/images/output_build-docs.txt @@ -1 +1 @@ -b4c249b4d1f7605a443774262109694a +c5f2067ec852773089ed0ca7b8d1d533 diff --git a/dev/breeze/doc/images/output_release-management_add-back-references.svg b/dev/breeze/doc/images/output_release-management_add-back-references.svg index 37e9086660253..f17f7f47ed43b 100644 --- a/dev/breeze/doc/images/output_release-management_add-back-references.svg +++ b/dev/breeze/doc/images/output_release-management_add-back-references.svg @@ -155,8 +155,8 @@ | hashicorp | helm-chart | http | imap | influxdb | informatica | jdbc | jenkins | keycloak | microsoft.azure |        microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai | openfaas | openlineage |  opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres | presto | qdrant | redis |    -salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh |    -standard | tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...  +salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh | standard |    +tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...             Command to add back references for documentation to make it backward compatible. diff --git a/dev/breeze/doc/images/output_release-management_add-back-references.txt b/dev/breeze/doc/images/output_release-management_add-back-references.txt index a43ec033fc2a6..ffc7eeea6018b 100644 --- a/dev/breeze/doc/images/output_release-management_add-back-references.txt +++ b/dev/breeze/doc/images/output_release-management_add-back-references.txt @@ -1 +1 @@ -a44de0a6fcf0ad832e0b2a73a883f0a0 +3df401aef0085547b08fe896a9a65381 diff --git a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg index 6566b6c97716f..8fe24cdf434e6 100644 --- a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg +++ b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg @@ -149,9 +149,9 @@ github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins | keycloak |                microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai |         openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres |     -presto | qdrant | redis | salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp |     -snowflake | sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex |    -ydb | zendesk]...                                                                                                      +presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake |    +sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb |          +zendesk]...                                                                                                            Generates content for issue to test the release. diff --git a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt index 0c327de82828f..c6189be26338f 100644 --- a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt +++ b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt @@ -1 +1 @@ -ee99c790838efb1d5e5a3b06e6c49846 +a85c889b710aa347eb6c47fc36b11720 diff --git a/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg b/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg index 742e316f5a754..867b9fedc0357 100644 --- a/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg +++ b/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg @@ -1,4 +1,4 @@ - + diff --git a/dev/breeze/doc/images/output_release-management_generate-providers-metadata.txt b/dev/breeze/doc/images/output_release-management_generate-providers-metadata.txt index 6e5c7fd64de14..3615848d57819 100644 --- a/dev/breeze/doc/images/output_release-management_generate-providers-metadata.txt +++ b/dev/breeze/doc/images/output_release-management_generate-providers-metadata.txt @@ -1 +1 @@ -de007da2573c2e6066fd2b0d26d14874 +fdfdca32a5248d3b91cb29e14cc538b4 diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.svg b/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.svg index dbabcc063abf1..3661b47f2a46d 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.svg +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.svg @@ -197,9 +197,9 @@ github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins | keycloak |                microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai |         openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres |     -presto | qdrant | redis | salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp |     -snowflake | sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex |    -ydb | zendesk]...                                                                                                      +presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake |    +sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb |          +zendesk]...                                                                                                            Prepare sdist/whl distributions of Airflow Providers. Each provider directory is wiped with `git clean -fdx`  (preserving .venv, .idea, .vscode) before build to keep in-tree generated files out of the artifact. See dev/breeze  diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt b/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt index aa4a21a6dcebd..f10fd70bd89fa 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt @@ -1 +1 @@ -71c54d02659478978d0aa40b2baf4fef +18d45fa2bec60ab0557f04fb4427b35e diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg index ae36d4fde676e..c4454038e4a78 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg @@ -218,9 +218,9 @@ github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins | keycloak |                microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai |         openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres |     -presto | qdrant | redis | salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp |     -snowflake | sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex |    -ydb | zendesk]...                                                                                                      +presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake |    +sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb |          +zendesk]...                                                                                                            Prepare CHANGELOG, README and COMMITS information for providers. diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt index 68e5927948e4f..5586a29b8136c 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt @@ -1 +1 @@ -542fd516d5584cf5bc1b6aa945338a8c +622441d283775edefeda685820e7169a diff --git a/dev/breeze/doc/images/output_release-management_publish-docs.svg b/dev/breeze/doc/images/output_release-management_publish-docs.svg index 291f8b0d144e3..d119da2013d75 100644 --- a/dev/breeze/doc/images/output_release-management_publish-docs.svg +++ b/dev/breeze/doc/images/output_release-management_publish-docs.svg @@ -194,8 +194,8 @@ | hashicorp | helm-chart | http | imap | influxdb | informatica | jdbc | jenkins | keycloak | microsoft.azure |        microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai | openfaas | openlineage |  opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres | presto | qdrant | redis |    -salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh |    -standard | tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...  +salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh | standard |    +tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...             Command to publish generated documentation to airflow-site diff --git a/dev/breeze/doc/images/output_release-management_publish-docs.txt b/dev/breeze/doc/images/output_release-management_publish-docs.txt index 487f7d9fef5f8..c73c7846664c8 100644 --- a/dev/breeze/doc/images/output_release-management_publish-docs.txt +++ b/dev/breeze/doc/images/output_release-management_publish-docs.txt @@ -1 +1 @@ -6a7fed8b89fffc1e9d8856bf1a2d5f2d +4521ec02334b8909f66e82c460a69446 diff --git a/dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg b/dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg index ec6cd73739017..fd62a65b513d4 100644 --- a/dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg +++ b/dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg @@ -189,9 +189,9 @@ │| grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins | keycloak | │ │microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | │ │odbc | openai | openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill │ -│| pgvector | pinecone | postgres | presto | qdrant | redis | salesforce | samba | sdk.java | │ -│segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh | standard | │ -│tableau | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk)│ +│| pgvector | pinecone | postgres | presto | qdrant | redis | salesforce | samba | segment | │ +│sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh | standard | tableau | │ +│telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk)│ │--provider-versionProvider version to generate the requirements for i.e `2.1.0`. `latest` is also a supported     │ │value to account for the most recent version of the provider (TEXT)│ │--force           Force update providers requirements even if they already exist.│ diff --git a/dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt b/dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt index 8a0c324836340..a7761ea29d68a 100644 --- a/dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt +++ b/dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt @@ -1 +1 @@ -c4babe6a19ea7748ed3488c930187a8e +fa98bbcd73f9160c29eff1b6779a23bc diff --git a/dev/breeze/doc/images/output_workflow-run_publish-docs.svg b/dev/breeze/doc/images/output_workflow-run_publish-docs.svg index 0b42e92e917d3..511790e79d721 100644 --- a/dev/breeze/doc/images/output_workflow-run_publish-docs.svg +++ b/dev/breeze/doc/images/output_workflow-run_publish-docs.svg @@ -200,8 +200,8 @@ | hashicorp | helm-chart | http | imap | influxdb | informatica | jdbc | jenkins | keycloak | microsoft.azure |        microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc | openai | openfaas | openlineage |  opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres | presto | qdrant | redis |    -salesforce | samba | sdk.java | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh |    -standard | tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...  +salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake | sqlite | ssh | standard |    +tableau | task-sdk | telegram | teradata | trino | vertica | vespa | weaviate | yandex | ydb | zendesk]...             Trigger publish docs to S3 workflow diff --git a/dev/breeze/doc/images/output_workflow-run_publish-docs.txt b/dev/breeze/doc/images/output_workflow-run_publish-docs.txt index cbb67ce0a1df9..6a433f7935a96 100644 --- a/dev/breeze/doc/images/output_workflow-run_publish-docs.txt +++ b/dev/breeze/doc/images/output_workflow-run_publish-docs.txt @@ -1 +1 @@ -a4876e7e49973aad884a0270de53885a +6ff7091e58988c6273e51f372bb8a1a6 diff --git a/dev/breeze/src/airflow_breeze/global_constants.py b/dev/breeze/src/airflow_breeze/global_constants.py index 8be1ff9724ed9..80ea5e6cb7e3d 100644 --- a/dev/breeze/src/airflow_breeze/global_constants.py +++ b/dev/breeze/src/airflow_breeze/global_constants.py @@ -789,25 +789,25 @@ def get_airflow_extras(): { "python-version": "3.10", "airflow-version": "2.11.1", - "remove-providers": "common.messaging edge3 fab git keycloak informatica common.ai opensearch sdk.java", + "remove-providers": "common.messaging edge3 fab git keycloak informatica common.ai opensearch", "run-unit-tests": "true", }, { "python-version": "3.10", "airflow-version": "3.0.6", - "remove-providers": "sdk.java", + "remove-providers": "", "run-unit-tests": "true", }, { "python-version": "3.10", "airflow-version": "3.1.8", - "remove-providers": "sdk.java", + "remove-providers": "", "run-unit-tests": "true", }, { "python-version": "3.10", "airflow-version": "3.2.1", - "remove-providers": "sdk.java", + "remove-providers": "", "run-unit-tests": "true", }, ] diff --git a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py index 91fcd6cd9afc1..b50ca5f7d1b69 100644 --- a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py +++ b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py @@ -110,6 +110,7 @@ ("scripts", "/opt/airflow/scripts"), ("uv.lock", "/opt/airflow/uv.lock"), ("scripts/docker/entrypoint_ci.sh", "/entrypoint"), + ("sdk", "/opt/airflow/sdk"), ("shared", "/opt/airflow/shared"), ("task-sdk", "/opt/airflow/task-sdk"), ] diff --git a/devel-common/src/sphinx_exts/includes/sections-and-options.rst b/devel-common/src/sphinx_exts/includes/sections-and-options.rst index e04383c8c5582..59d65b4dc776f 100644 --- a/devel-common/src/sphinx_exts/includes/sections-and-options.rst +++ b/devel-common/src/sphinx_exts/includes/sections-and-options.rst @@ -85,7 +85,7 @@ {% if "\n" in example %} .. code-block:: - {{ example }} + {{ example | indent(8) }} {% else %} ``{{ example }}`` {% endif %} diff --git a/providers/sdk/java/README.rst b/providers/sdk/java/README.rst deleted file mode 100644 index ba3081bb6cb53..0000000000000 --- a/providers/sdk/java/README.rst +++ /dev/null @@ -1,60 +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. - -.. NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN! - -.. IF YOU WANT TO MODIFY TEMPLATE FOR THIS FILE, YOU SHOULD MODIFY THE TEMPLATE - ``PROVIDER_README_TEMPLATE.rst.jinja2`` IN the ``dev/breeze/src/airflow_breeze/templates`` DIRECTORY - -Package ``apache-airflow-providers-sdk-java`` - -Release: ``0.1.0`` - - -Java Coordinator - - -Provider package ----------------- - -This is a provider package for ``sdk.java`` provider. All classes for this provider package -are in ``airflow.providers.sdk.java`` python package. - -You can find package information and changelog for the provider -in the `documentation `_. - -Installation ------------- - -You can install this package on top of an existing Airflow installation (see ``Requirements`` below -for the minimum Airflow version supported) via -``pip install apache-airflow-providers-sdk-java`` - -The package supports the following python versions: 3.10,3.11,3.12,3.13,3.14 - -Requirements ------------- - -================== ================== -PIP package Version required -================== ================== -``apache-airflow`` ``>=3.0.0`` -================== ================== - -The changelog for the provider package can be found in the -`changelog `_. diff --git a/providers/sdk/java/docs/.latest-doc-only-change.txt b/providers/sdk/java/docs/.latest-doc-only-change.txt deleted file mode 100644 index 2c1ab461a9c8e..0000000000000 --- a/providers/sdk/java/docs/.latest-doc-only-change.txt +++ /dev/null @@ -1 +0,0 @@ -da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/sdk/java/docs/changelog.rst b/providers/sdk/java/docs/changelog.rst deleted file mode 100644 index c5aa1ad337ef8..0000000000000 --- a/providers/sdk/java/docs/changelog.rst +++ /dev/null @@ -1,40 +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. - - -.. NOTE TO CONTRIBUTORS: - Please, only add notes to the Changelog just below the "Changelog" header when there are some breaking changes - and you want to add an explanation to the users on how they are supposed to deal with them. - The changelog is updated and maintained semi-automatically by release manager. - - -``apache-airflow-providers-sdk-java`` - - -Changelog ---------- - -0.1.0 -..... - -Features -~~~~~~~~ - -* ``Add the initial Java coordinator interface`` - -.. Below changes are excluded from the changelog. Move them to - appropriate section above if needed. Do not delete the lines(!): diff --git a/providers/sdk/java/docs/commits.rst b/providers/sdk/java/docs/commits.rst deleted file mode 100644 index 6b84d751e94e3..0000000000000 --- a/providers/sdk/java/docs/commits.rst +++ /dev/null @@ -1,35 +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. - - .. NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN! - - .. IF YOU WANT TO MODIFY THIS FILE, YOU SHOULD MODIFY THE TEMPLATE - `PROVIDER_COMMITS_TEMPLATE.rst.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY - - .. THE REMAINDER OF THE FILE IS AUTOMATICALLY GENERATED. IT WILL BE OVERWRITTEN! - -Package apache-airflow-providers-sdk-java ------------------------------------------------------- - -Java Coordinator - - -This is detailed commit list of changes for versions provider package: ``sdk.java``. -For high-level changelog, see :doc:`package information including changelog `. - -.. airflow-providers-commits:: diff --git a/providers/sdk/java/docs/conf.py b/providers/sdk/java/docs/conf.py deleted file mode 100644 index 596c5b5c7b5f3..0000000000000 --- a/providers/sdk/java/docs/conf.py +++ /dev/null @@ -1,27 +0,0 @@ -# Disable Flake8 because of all the sphinx imports -# -# 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. -"""Configuration of Providers docs building.""" - -from __future__ import annotations - -import os - -os.environ["AIRFLOW_PACKAGE_NAME"] = "apache-airflow-providers-sdk-java" - -from docs.provider_conf import * # noqa: F403 diff --git a/providers/sdk/java/docs/configurations-ref.rst b/providers/sdk/java/docs/configurations-ref.rst deleted file mode 100644 index ea8e668d75793..0000000000000 --- a/providers/sdk/java/docs/configurations-ref.rst +++ /dev/null @@ -1,19 +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. - -.. include:: /../../../../devel-common/src/sphinx_exts/includes/providers-configurations-ref.rst -.. include:: /../../../../devel-common/src/sphinx_exts/includes/sections-and-options.rst diff --git a/providers/sdk/java/docs/index.rst b/providers/sdk/java/docs/index.rst deleted file mode 100644 index 77e8b1e22d80e..0000000000000 --- a/providers/sdk/java/docs/index.rst +++ /dev/null @@ -1,123 +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. - -``apache-airflow-providers-sdk-java`` -=========================================== - -The SDK: Java provider registers Java-specific task coordinator and DAG file processor classes for Apache Airflow. - -.. toctree:: - :hidden: - :maxdepth: 1 - :caption: Basics - - Home - Changelog - Security - -.. toctree:: - :hidden: - :maxdepth: 1 - :caption: Guides - - Configuration - -.. toctree:: - :hidden: - :maxdepth: 1 - :caption: References - - Python API <_api/airflow/providers/sdk/java/index> - -.. toctree:: - :hidden: - :maxdepth: 1 - :caption: Resources - - PyPI Repository - Installing from sources - -.. THE REMAINDER OF THE FILE IS AUTOMATICALLY GENERATED. IT WILL BE OVERWRITTEN AT RELEASE TIME! - - -.. toctree:: - :hidden: - :maxdepth: 1 - :caption: Commits - - Detailed list of commits - - -apache-airflow-providers-sdk-java package ------------------------------------------------------- - -Java SDK support for Apache Airflow runtime coordinators. - - -Release: 0.1.0 - -Provider package ----------------- - -This package is for the ``sdk.java`` provider. -All classes for this package are included in the ``airflow.providers.sdk.java`` python package. - -Installation ------------- - -You can install this package on top of an existing Airflow installation via -``pip install apache-airflow-providers-sdk-java``. -For the minimum Airflow version supported, see ``Requirements`` below. - -Requirements ------------- - -The minimum Apache Airflow version supported by this provider distribution is ``3.3.0``. - -================== ================== -PIP package Version required -================== ================== -``apache-airflow`` ``>=3.3.0`` -================== ================== - -Cross provider package dependencies ------------------------------------ - -Those are dependencies that might be needed in order to use all the features of the package. -You need to install the specified provider distributions in order to use them. - -You can install such cross-provider dependencies when installing from PyPI. For example: - -.. code-block:: bash - - pip install apache-airflow-providers-sdk-java[common.compat] - - -================================================================================================================== ================= -Dependent package Extra -================================================================================================================== ================= -`apache-airflow-providers-common-compat `_ ``common.compat`` -================================================================================================================== ================= - -Downloading official packages ------------------------------ - -You can download officially released packages and verify their checksums and signatures from the -`Official Apache Download site `_ - -* `The apache-airflow-providers-sdk-java 0.1.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-sdk-java 0.1.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/sdk/java/docs/installing-providers-from-sources.rst b/providers/sdk/java/docs/installing-providers-from-sources.rst deleted file mode 100644 index fdbb17d017579..0000000000000 --- a/providers/sdk/java/docs/installing-providers-from-sources.rst +++ /dev/null @@ -1,18 +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. - -.. include:: /../../../../devel-common/src/sphinx_exts/includes/installing-providers-from-sources.rst diff --git a/providers/sdk/java/docs/security.rst b/providers/sdk/java/docs/security.rst deleted file mode 100644 index 351ff007ebf2f..0000000000000 --- a/providers/sdk/java/docs/security.rst +++ /dev/null @@ -1,18 +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. - -.. include:: /../../../../devel-common/src/sphinx_exts/includes/security.rst diff --git a/providers/sdk/java/provider.yaml b/providers/sdk/java/provider.yaml deleted file mode 100644 index d10f841962034..0000000000000 --- a/providers/sdk/java/provider.yaml +++ /dev/null @@ -1,58 +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. - ---- -package-name: apache-airflow-providers-sdk-java -name: "SDK: Java" -description: | - Java SDK support for Apache Airflow runtime coordinators. - -state: ready -lifecycle: incubation -source-date-epoch: 1775631151 -# Note that those versions are maintained by release manager - do not update them manually -# with the exception of case where other provider in sources has >= new provider version. -# In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have -# to be done in the same PR -versions: - - 0.1.0 - -integrations: - - integration-name: Java - external-doc-url: https://openjdk.org/ - tags: - - software - -config: - java: - description: "Options for the Java SDK provider." - options: - bundles_folder: - description: | - Path to the directory containing Java DAG bundle JARs. - When using Python stub DAGs that delegate task execution to Java, - the coordinator scans this directory to find the JAR bundle matching - the target dag_id. Each immediate subdirectory is treated as a - separate bundle home, and the directory itself is also checked - (flat layout). - type: string - version_added: ~ - example: ~/airflow/java-bundles - default: "" - -coordinators: - - airflow.providers.sdk.java.coordinator.JavaCoordinator diff --git a/providers/sdk/java/pyproject.toml b/providers/sdk/java/pyproject.toml deleted file mode 100644 index 6baca6f81fdd4..0000000000000 --- a/providers/sdk/java/pyproject.toml +++ /dev/null @@ -1,131 +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. - -# NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN! - -# IF YOU WANT TO MODIFY THIS FILE EXCEPT DEPENDENCIES, YOU SHOULD MODIFY THE TEMPLATE -# `pyproject_TEMPLATE.toml.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY -[build-system] -requires = ["flit_core==3.12.0"] -build-backend = "flit_core.buildapi" - -[project] -name = "apache-airflow-providers-sdk-java" -version = "0.1.0" -description = "Provider package apache-airflow-providers-sdk-java for Apache Airflow" -readme = "README.rst" -license = "Apache-2.0" -license-files = ['LICENSE', 'NOTICE'] -authors = [ - {name="Apache Software Foundation", email="dev@airflow.apache.org"}, -] -maintainers = [ - {name="Apache Software Foundation", email="dev@airflow.apache.org"}, -] -keywords = [ "airflow-provider", "sdk.java", "airflow", "integration" ] -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Environment :: Web Environment", - "Intended Audience :: Developers", - "Intended Audience :: System Administrators", - "Framework :: Apache Airflow", - "Framework :: Apache Airflow :: Provider", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: System :: Monitoring", -] -requires-python = ">=3.10" - -# The dependencies should be modified in place in the generated file. -# Any change in the dependencies is preserved when the file is regenerated -# Make sure to run ``prek update-providers-dependencies --all-files`` -# After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build`` -dependencies = [ - "apache-airflow>=3.3.0", -] - -# The optional dependencies should be modified in place in the generated file -# Any change in the dependencies is preserved when the file is regenerated -[project.optional-dependencies] -"common.compat" = [ - "apache-airflow-providers-common-compat" -] - -[dependency-groups] -dev = [ - "apache-airflow", - "apache-airflow-task-sdk", - "apache-airflow-devel-common", - "apache-airflow-providers-common-compat", - # Additional devel dependencies (do not remove this line and add extra development dependencies) -] - -# To build docs: -# -# uv run --group docs build-docs -# -# To enable auto-refreshing build with server: -# -# uv run --group docs build-docs --autobuild -# -# To see more options: -# -# uv run --group docs build-docs --help -# -docs = [ - "apache-airflow-devel-common[docs]" -] - -[tool.uv.sources] -# These names must match the names as defined in the pyproject.toml of the workspace items, -# *not* the workspace folder paths -apache-airflow = {workspace = true} -apache-airflow-devel-common = {workspace = true} -apache-airflow-task-sdk = {workspace = true} -apache-airflow-providers-common-sql = {workspace = true} -apache-airflow-providers-standard = {workspace = true} - -[project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-sdk-java/0.1.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-sdk-java/0.1.0/changelog.html" -"Bug Tracker" = "https://github.com/apache/airflow/issues" -"Source Code" = "https://github.com/apache/airflow" -"Slack Chat" = "https://s.apache.org/airflow-slack" -"Mastodon" = "https://fosstodon.org/@airflow" -"YouTube" = "https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/" - -[project.entry-points."apache_airflow_provider"] -provider_info = "airflow.providers.sdk.java.get_provider_info:get_provider_info" - -[tool.flit.module] -name = "airflow.providers.sdk.java" - -# Explicit sdist contents so the build does not rely on VCS information -# (flit 4.0 makes --no-use-vcs the default — see https://github.com/pypa/flit/pull/782). -[tool.flit.sdist] -include = [ - "docs/", - "provider.yaml", - "src/airflow/__init__.py", - "src/airflow/providers/__init__.py", - "src/airflow/providers/sdk/__init__.py", - "tests/", -] diff --git a/providers/sdk/java/src/airflow/providers/__init__.py b/providers/sdk/java/src/airflow/providers/__init__.py deleted file mode 100644 index 5966d6b1d5261..0000000000000 --- a/providers/sdk/java/src/airflow/providers/__init__.py +++ /dev/null @@ -1,17 +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. -__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/providers/sdk/java/src/airflow/providers/sdk/__init__.py b/providers/sdk/java/src/airflow/providers/sdk/__init__.py deleted file mode 100644 index 5966d6b1d5261..0000000000000 --- a/providers/sdk/java/src/airflow/providers/sdk/__init__.py +++ /dev/null @@ -1,17 +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. -__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/providers/sdk/java/src/airflow/providers/sdk/java/__init__.py b/providers/sdk/java/src/airflow/providers/sdk/java/__init__.py deleted file mode 100644 index 1c942bc68df44..0000000000000 --- a/providers/sdk/java/src/airflow/providers/sdk/java/__init__.py +++ /dev/null @@ -1,39 +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. -# -# NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE -# OVERWRITTEN WHEN PREPARING DOCUMENTATION FOR THE PACKAGES. -# -# IF YOU WANT TO MODIFY THIS FILE, YOU SHOULD MODIFY THE TEMPLATE -# `PROVIDER__INIT__PY_TEMPLATE.py.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY -# -from __future__ import annotations - -import packaging.version - -from airflow import __version__ as airflow_version - -__all__ = ["__version__"] - -__version__ = "0.1.0" - -if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( - "3.3.0" -): - raise RuntimeError( - f"The package `apache-airflow-providers-sdk-java:{__version__}` needs Apache Airflow 3.3.0+" - ) diff --git a/providers/sdk/java/src/airflow/providers/sdk/java/get_provider_info.py b/providers/sdk/java/src/airflow/providers/sdk/java/get_provider_info.py deleted file mode 100644 index 89df45102b732..0000000000000 --- a/providers/sdk/java/src/airflow/providers/sdk/java/get_provider_info.py +++ /dev/null @@ -1,47 +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. - -# NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN! -# -# IF YOU WANT TO MODIFY THIS FILE, YOU SHOULD MODIFY THE TEMPLATE -# `get_provider_info_TEMPLATE.py.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY - - -def get_provider_info(): - return { - "package-name": "apache-airflow-providers-sdk-java", - "name": "SDK: Java", - "description": "Java SDK support for Apache Airflow runtime coordinators.\n", - "integrations": [ - {"integration-name": "Java", "external-doc-url": "https://openjdk.org/", "tags": ["software"]} - ], - "config": { - "java": { - "description": "Options for the Java SDK provider.", - "options": { - "bundles_folder": { - "description": "Path to the directory containing Java DAG bundle JARs.\nWhen using Python stub DAGs that delegate task execution to Java,\nthe coordinator scans this directory to find the JAR bundle matching\nthe target dag_id. Each immediate subdirectory is treated as a\nseparate bundle home, and the directory itself is also checked\n(flat layout).\n", - "type": "string", - "version_added": None, - "example": "~/airflow/java-bundles", - "default": "", - } - }, - } - }, - "coordinators": ["airflow.providers.sdk.java.coordinator.JavaCoordinator"], - } diff --git a/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py b/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py deleted file mode 100644 index e0489ada7cc17..0000000000000 --- a/providers/sdk/java/tests/unit/sdk/java/test_java_provider.py +++ /dev/null @@ -1,58 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from __future__ import annotations - -import pytest - -from airflow.providers.sdk.java.coordinator import JavaCoordinator -from airflow.providers.sdk.java.get_provider_info import get_provider_info - -from tests_common.test_utils.version_compat import AIRFLOW_V_3_3_PLUS - -if not AIRFLOW_V_3_3_PLUS: - pytest.skip("Coordinator is only compatible with Airflow >= 3.3.0", allow_module_level=True) - - -def test_get_provider_info_exposes_java_runtime_components(): - assert get_provider_info() == { - "package-name": "apache-airflow-providers-sdk-java", - "name": "SDK: Java", - "description": "Java SDK support for Apache Airflow runtime coordinators.\n", - "integrations": [ - {"integration-name": "Java", "external-doc-url": "https://openjdk.org/", "tags": ["software"]} - ], - "config": { - "java": { - "description": "Options for the Java SDK provider.", - "options": { - "bundles_folder": { - "description": "Path to the directory containing Java DAG bundle JARs.\nWhen using Python stub DAGs that delegate task execution to Java,\nthe coordinator scans this directory to find the JAR bundle matching\nthe target dag_id. Each immediate subdirectory is treated as a\nseparate bundle home, and the directory itself is also checked\n(flat layout).\n", - "type": "string", - "version_added": None, - "example": "~/airflow/java-bundles", - "default": "", - } - }, - } - }, - "coordinators": ["airflow.providers.sdk.java.coordinator.JavaCoordinator"], - } - - -def test_java_provider_entrypoints_are_importable(): - assert JavaCoordinator.sdk == "java" diff --git a/pyproject.toml b/pyproject.toml index 9934271e31d10..b183ec7f6f408 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -338,9 +338,6 @@ apache-airflow = "airflow.__main__:main" "samba" = [ "apache-airflow-providers-samba>=4.9.0" ] -"sdk.java" = [ - "apache-airflow-providers-sdk-java>=0.1.0" -] "segment" = [ "apache-airflow-providers-segment>=3.7.0" ] @@ -484,7 +481,6 @@ apache-airflow = "airflow.__main__:main" "apache-airflow-providers-redis>=4.0.0", "apache-airflow-providers-salesforce>=5.9.0", "apache-airflow-providers-samba>=4.9.0", - "apache-airflow-providers-sdk-java>=0.1.0", "apache-airflow-providers-segment>=3.7.0", "apache-airflow-providers-sendgrid>=4.0.0", "apache-airflow-providers-sftp>=5.0.0", @@ -1238,8 +1234,6 @@ mypy_path = [ "$MYPY_CONFIG_FILE_DIR/providers/salesforce/tests", "$MYPY_CONFIG_FILE_DIR/providers/samba/src", "$MYPY_CONFIG_FILE_DIR/providers/samba/tests", - "$MYPY_CONFIG_FILE_DIR/providers/sdk/java/src", - "$MYPY_CONFIG_FILE_DIR/providers/sdk/java/tests", "$MYPY_CONFIG_FILE_DIR/providers/segment/src", "$MYPY_CONFIG_FILE_DIR/providers/segment/tests", "$MYPY_CONFIG_FILE_DIR/providers/sendgrid/src", @@ -1496,7 +1490,6 @@ apache-airflow-providers-qdrant = false apache-airflow-providers-redis = false apache-airflow-providers-salesforce = false apache-airflow-providers-samba = false -apache-airflow-providers-sdk-java = false apache-airflow-providers-segment = false apache-airflow-providers-sendgrid = false apache-airflow-providers-sftp = false @@ -1647,7 +1640,6 @@ apache-airflow-providers-qdrant = false apache-airflow-providers-redis = false apache-airflow-providers-salesforce = false apache-airflow-providers-samba = false -apache-airflow-providers-sdk-java = false apache-airflow-providers-segment = false apache-airflow-providers-sendgrid = false apache-airflow-providers-sftp = false @@ -1809,7 +1801,6 @@ apache-airflow-providers-qdrant = { workspace = true } apache-airflow-providers-redis = { workspace = true } apache-airflow-providers-salesforce = { workspace = true } apache-airflow-providers-samba = { workspace = true } -apache-airflow-providers-sdk-java = { workspace = true } apache-airflow-providers-segment = { workspace = true } apache-airflow-providers-sendgrid = { workspace = true } apache-airflow-providers-sftp = { workspace = true } @@ -1850,6 +1841,7 @@ members = [ "chart", "kubernetes-tests", "task-sdk", + "sdk/coordinators/java", "providers-summary-docs", "docker-stack-docs", "shared/configuration", @@ -1947,7 +1939,6 @@ members = [ "providers/redis", "providers/salesforce", "providers/samba", - "providers/sdk/java", "providers/segment", "providers/sendgrid", "providers/sftp", diff --git a/scripts/ci/docker-compose/local.yml b/scripts/ci/docker-compose/local.yml index f91198189f6db..45dab4d56ffaf 100644 --- a/scripts/ci/docker-compose/local.yml +++ b/scripts/ci/docker-compose/local.yml @@ -123,6 +123,9 @@ services: - type: bind source: ../../../scripts/docker/entrypoint_ci.sh target: /entrypoint + - type: bind + source: ../../../sdk + target: /opt/airflow/sdk - type: bind source: ../../../shared target: /opt/airflow/shared diff --git a/scripts/ci/docker-compose/remove-sources.yml b/scripts/ci/docker-compose/remove-sources.yml index 24ca15bbb0c47..cf78e9258f39f 100644 --- a/scripts/ci/docker-compose/remove-sources.yml +++ b/scripts/ci/docker-compose/remove-sources.yml @@ -107,7 +107,7 @@ services: - ../../../empty:/opt/airflow/providers/redis/src - ../../../empty:/opt/airflow/providers/salesforce/src - ../../../empty:/opt/airflow/providers/samba/src - - ../../../empty:/opt/airflow/providers/sdk/java/src + - ../../../empty:/opt/airflow/sdk/coordinators/java/src - ../../../empty:/opt/airflow/providers/segment/src - ../../../empty:/opt/airflow/providers/sendgrid/src - ../../../empty:/opt/airflow/providers/sftp/src diff --git a/scripts/ci/docker-compose/tests-sources.yml b/scripts/ci/docker-compose/tests-sources.yml index de736d60237ae..67b2590f69b61 100644 --- a/scripts/ci/docker-compose/tests-sources.yml +++ b/scripts/ci/docker-compose/tests-sources.yml @@ -120,7 +120,7 @@ services: - ../../../providers/redis/tests:/opt/airflow/providers/redis/tests - ../../../providers/salesforce/tests:/opt/airflow/providers/salesforce/tests - ../../../providers/samba/tests:/opt/airflow/providers/samba/tests - - ../../../providers/sdk/java/tests:/opt/airflow/providers/sdk/java/tests + - ../../../sdk/coordinators/java/tests:/opt/airflow/sdk/coordinators/java/tests - ../../../providers/segment/tests:/opt/airflow/providers/segment/tests - ../../../providers/sendgrid/tests:/opt/airflow/providers/sendgrid/tests - ../../../providers/sftp/tests:/opt/airflow/providers/sftp/tests diff --git a/scripts/in_container/install_airflow_and_providers.py b/scripts/in_container/install_airflow_and_providers.py index 5e2c47ec58f19..4f203e8c6c3f0 100755 --- a/scripts/in_container/install_airflow_and_providers.py +++ b/scripts/in_container/install_airflow_and_providers.py @@ -1064,7 +1064,6 @@ def install_airflow_and_providers( "apache-airflow-providers-common-messaging", "apache-airflow-providers-git", "apache-airflow-providers-edge3", - "apache-airflow-providers-sdk-java", ] run_command( ["uv", "pip", "uninstall", *providers_to_uninstall_for_airflow_2], diff --git a/providers/sdk/java/.gitignore b/sdk/coordinators/java/.gitignore similarity index 100% rename from providers/sdk/java/.gitignore rename to sdk/coordinators/java/.gitignore diff --git a/providers/sdk/java/LICENSE b/sdk/coordinators/java/LICENSE similarity index 100% rename from providers/sdk/java/LICENSE rename to sdk/coordinators/java/LICENSE diff --git a/providers/sdk/java/NOTICE b/sdk/coordinators/java/NOTICE similarity index 100% rename from providers/sdk/java/NOTICE rename to sdk/coordinators/java/NOTICE diff --git a/sdk/coordinators/java/README.rst b/sdk/coordinators/java/README.rst new file mode 100644 index 0000000000000..63f19caa412bb --- /dev/null +++ b/sdk/coordinators/java/README.rst @@ -0,0 +1,51 @@ + +.. 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. + +Package ``apache-airflow-coordinators-java`` +=========================================== + +Java runtime coordinator for the Apache Airflow Task SDK. + +This distribution contributes the ``airflow.sdk.coordinators.java.JavaCoordinator`` +class, which spawns a JVM subprocess to parse Java DAG bundles (``.jar``) +and execute Java tasks. It is loaded via the ``[sdk] coordinators`` configuration +and is *not* a standard Airflow provider — it does not register hooks, operators, +or any other provider-managed resources. + +Configure it in ``airflow.cfg``:: + + [sdk] + coordinators = [ + { + "name": "jdk-17", + "classpath": "airflow.sdk.coordinators.java.JavaCoordinator", + "kwargs": { + "java_executable": "/usr/lib/jvm/java-17-openjdk/bin/java", + "jvm_args": ["-Xmx1024m"], + "bundles_folder": "~/airflow/java-bundles" + } + } + ] + queue_to_coordinator = {"java-queue": "jdk-17"} + +Installation +------------ + +:: + + pip install apache-airflow-coordinators-java diff --git a/sdk/coordinators/java/pyproject.toml b/sdk/coordinators/java/pyproject.toml new file mode 100644 index 0000000000000..f4e0dd31f5284 --- /dev/null +++ b/sdk/coordinators/java/pyproject.toml @@ -0,0 +1,108 @@ +# 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. + +[build-system] +requires = ["hatchling==1.29.0"] +build-backend = "hatchling.build" + +[project] +name = "apache-airflow-coordinators-java" +version = "0.1.0" +description = "Java runtime coordinator for the Apache Airflow Task SDK" +readme = "README.rst" +license = "Apache-2.0" +license-files = ["LICENSE", "NOTICE"] +authors = [ + {name="Apache Software Foundation", email="dev@airflow.apache.org"}, +] +maintainers = [ + {name="Apache Software Foundation", email="dev@airflow.apache.org"}, +] +keywords = ["airflow", "coordinator", "java", "sdk"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Framework :: Apache Airflow", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: System :: Monitoring", +] +requires-python = ">=3.10,!=3.15" + +dependencies = [ + "apache-airflow-task-sdk>=1.3.0", + "PyYAML>=6.0.2", +] + +[dependency-groups] +dev = [ + "apache-airflow", + "apache-airflow-task-sdk", + "apache-airflow-devel-common", +] + +docs = [ + "apache-airflow-devel-common[docs]" +] + +[tool.uv.sources] +apache-airflow = {workspace = true} +apache-airflow-devel-common = {workspace = true} +apache-airflow-task-sdk = {workspace = true} + +[project.urls] +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-coordinators-java/0.1.0" +"Bug Tracker" = "https://github.com/apache/airflow/issues" +"Source Code" = "https://github.com/apache/airflow" +"Slack Chat" = "https://s.apache.org/airflow-slack" + +[tool.hatch.build.targets.wheel] +packages = ["src/airflow"] +# Do not ship the airflow / airflow.sdk / airflow.sdk.coordinators package roots +# -- those are owned by airflow-core / task-sdk. This distribution only contributes +# the airflow.sdk.coordinators.java sub-package. +exclude = [ + "src/airflow/__init__.py", + "src/airflow/sdk/__init__.py", + "src/airflow/sdk/coordinators/__init__.py", +] + +[tool.hatch.build.targets.sdist] +include = [ + "src/airflow", + "docs", + "tests", + "LICENSE", + "NOTICE", + "README.rst", +] + +[tool.ruff] +extend = "../../../pyproject.toml" +src = ["src"] +namespace-packages = ["src/airflow"] + +[tool.ruff.lint.per-file-ignores] +# Ignore Doc rules et al for anything outside of tests +"!src/*" = ["D", "TID253", "S101", "TRY002"] +# Ignore the pytest rules outside the tests folder +"!tests/*" = ["PT"] diff --git a/providers/sdk/java/tests/conftest.py b/sdk/coordinators/java/src/airflow/sdk/coordinators/java/__init__.py similarity index 79% rename from providers/sdk/java/tests/conftest.py rename to sdk/coordinators/java/src/airflow/sdk/coordinators/java/__init__.py index f56ccce0a3f69..b458478e328ed 100644 --- a/providers/sdk/java/tests/conftest.py +++ b/sdk/coordinators/java/src/airflow/sdk/coordinators/java/__init__.py @@ -14,6 +14,14 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +""" +Java runtime coordinator for the Apache Airflow Task SDK. + +See :class:`JavaCoordinator` for details. +""" + from __future__ import annotations -pytest_plugins = "tests_common.pytest_plugin" +from airflow.sdk.coordinators.java.coordinator import JavaCoordinator + +__all__ = ["JavaCoordinator"] diff --git a/providers/sdk/java/src/airflow/providers/sdk/java/bundle_scanner.py b/sdk/coordinators/java/src/airflow/sdk/coordinators/java/bundle_scanner.py similarity index 95% rename from providers/sdk/java/src/airflow/providers/sdk/java/bundle_scanner.py rename to sdk/coordinators/java/src/airflow/sdk/coordinators/java/bundle_scanner.py index 87bbf518b8e5b..9e2c1c1ab46fc 100644 --- a/providers/sdk/java/src/airflow/providers/sdk/java/bundle_scanner.py +++ b/sdk/coordinators/java/src/airflow/sdk/coordinators/java/bundle_scanner.py @@ -18,7 +18,7 @@ """ Scan directories for Airflow Java SDK bundle JARs. -Mirrors the Java SDK's ``BundleScanner`` — checks each JAR's manifest for +Mirrors the Java SDK's ``BundleScanner`` -- checks each JAR's manifest for ``Airflow-Java-SDK-Metadata``, reads the embedded metadata YAML, and resolves the main class and classpath needed to launch the bundle process. """ @@ -53,8 +53,8 @@ class BundleScanner: Supports two directory layouts: - - **Nested** - each immediate subdirectory of *bundles_dir* is a bundle home. - - **Flat** — *bundles_dir* itself contains the bundle JARs. + - **Nested** -- each immediate subdirectory of *bundles_dir* is a bundle home. + - **Flat** -- *bundles_dir* itself contains the bundle JARs. Within a bundle home the JVM convention of a ``lib/`` subdirectory for dependency JARs is respected automatically. @@ -104,13 +104,11 @@ def _candidate_homes(self) -> list[Path]: """Return normalised bundle-home directories to inspect.""" candidates: list[Path] = [] - # Each subdirectory is a potential bundle home (nested layout). if self._bundles_dir.is_dir(): for child in sorted(self._bundles_dir.iterdir()): if child.is_dir(): candidates.append(_normalize_bundle_home(child)) - # The directory itself (flat layout). candidates.append(_normalize_bundle_home(self._bundles_dir)) return candidates diff --git a/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py b/sdk/coordinators/java/src/airflow/sdk/coordinators/java/coordinator.py similarity index 62% rename from providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py rename to sdk/coordinators/java/src/airflow/sdk/coordinators/java/coordinator.py index 11833f166ce4b..4f93fcc93fd88 100644 --- a/providers/sdk/java/src/airflow/providers/sdk/java/coordinator.py +++ b/sdk/coordinators/java/src/airflow/sdk/coordinators/java/coordinator.py @@ -25,7 +25,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from airflow.providers.sdk.java.bundle_scanner import BundleScanner, read_dag_code +from airflow.sdk.coordinators.java.bundle_scanner import BundleScanner, read_dag_code from airflow.sdk.execution_time.coordinator import BaseCoordinator if TYPE_CHECKING: @@ -34,31 +34,61 @@ class JavaCoordinator(BaseCoordinator): - """Coordinator that launches a JVM subprocess for DAG parsing and task execution.""" + """ + Coordinator that launches a JVM subprocess for DAG parsing and task execution. + + Configuration is taken from the ``[sdk] coordinators`` entry that constructs + this instance:: + + { + "name": "jdk-17", + "classpath": "airflow.sdk.coordinators.java.JavaCoordinator", + "kwargs": { + "java_executable": "/usr/lib/jvm/java-17-openjdk/bin/java", + "jvm_args": ["-Xmx1024m"], + "bundles_folder": "~/airflow/java-bundles", + }, + } + + :param java_executable: Path to the ``java`` binary (defaults to ``"java"``, + which relies on ``$PATH``). + :param jvm_args: Extra arguments passed to the JVM (e.g. ``["-Xmx512m"]``). + :param bundles_folder: Directory scanned for JAR bundles when a Python + stub DAG delegates task execution to Java. Required for the stub-DAG + flow; unused for pure-Java DAGs. + """ sdk = "java" file_extension = ".jar" - @classmethod - def can_handle_dag_file(cls, bundle_name: str, path: str | os.PathLike[str]) -> bool: + def __init__( + self, + *, + java_executable: str = "java", + jvm_args: list[str] | None = None, + bundles_folder: str | None = None, + ) -> None: + self.java_executable = java_executable + self.jvm_args = list(jvm_args) if jvm_args else [] + self.bundles_folder = bundles_folder + + def can_handle_dag_file(self, bundle_name: str, path: str | os.PathLike[str]) -> bool: """Return ``True`` when *path* is a JAR with valid Airflow Java SDK manifest attributes.""" - if not os.fspath(path).endswith(cls.file_extension): + if not os.fspath(path).endswith(self.file_extension): return False with contextlib.suppress(FileNotFoundError, NotADirectoryError, zipfile.BadZipFile, KeyError): return BundleScanner.resolve_jar(Path(path)) is not None return False - @classmethod - def get_code_from_file(cls, fileloc: str) -> str: + def get_code_from_file(self, fileloc: str) -> str: """Read embedded DAG source code from a JAR bundle.""" code = read_dag_code(Path(fileloc)) if code is None: raise FileNotFoundError(f"No DAG source code found in JAR: {fileloc}") return code - @classmethod def dag_parsing_cmd( - cls, + self, *, dag_file_path: str, bundle_name: str, @@ -74,7 +104,8 @@ def dag_parsing_cmd( # it. Using ``/*`` lets the JVM load every JAR in the directory. classpath = f"{bundle_path}/*" return [ - "java", + self.java_executable, + *self.jvm_args, "-classpath", classpath, BundleScanner.resolve_jar(jar_path), @@ -82,9 +113,8 @@ def dag_parsing_cmd( f"--logs={logs_addr}", ] - @classmethod def task_execution_cmd( - cls, + self, *, what: TaskInstanceDTO, dag_file_path: str, @@ -95,12 +125,13 @@ def task_execution_cmd( ) -> list[str]: """Build the ``java`` command for executing a task in a JAR bundle.""" if dag_file_path.endswith(".jar"): - # Case 1: Pure Java Dag — the dag_file_path points directly to a + # Case 1: Pure Java Dag -- the dag_file_path points directly to a # bundle JAR inside the Airflow Core Dag Bundle. jar_path = Path(dag_file_path) classpath = f"{bundle_path}/*" return [ - "java", + self.java_executable, + *self.jvm_args, "-classpath", classpath, BundleScanner.resolve_jar(jar_path), @@ -108,21 +139,20 @@ def task_execution_cmd( f"--logs={logs_addr}", ] - # Case 2: Python Stub Dag — the dag_file_path is a Python file but + # Case 2: Python Stub Dag -- the dag_file_path is a Python file but # the task delegates to a Java runtime. The actual JAR bundle lives - # in the provider's configured ``[java] bundles_folder``. - from airflow.providers.common.compat.sdk import conf - - bundles_folder = conf.get("java", "bundles_folder", fallback=None) - if not bundles_folder: + # in ``bundles_folder`` (passed to __init__ from the [sdk] coordinators + # config entry). + if not self.bundles_folder: raise ValueError( - "The [java] bundles_folder config must be set for Python stub DAGs " + "JavaCoordinator: bundles_folder kwarg must be set for Python stub DAGs " "that delegate to Java task execution." ) - resolved = BundleScanner(Path(bundles_folder)).resolve(dag_id=what.dag_id) + resolved = BundleScanner(Path(self.bundles_folder)).resolve(dag_id=what.dag_id) return [ - "java", + self.java_executable, + *self.jvm_args, "-classpath", resolved.classpath, resolved.main_class, diff --git a/providers/sdk/java/tests/unit/sdk/__init__.py b/sdk/coordinators/java/tests/__init__.py similarity index 100% rename from providers/sdk/java/tests/unit/sdk/__init__.py rename to sdk/coordinators/java/tests/__init__.py diff --git a/providers/sdk/java/tests/unit/sdk/java/__init__.py b/sdk/coordinators/java/tests/unit/__init__.py similarity index 100% rename from providers/sdk/java/tests/unit/sdk/java/__init__.py rename to sdk/coordinators/java/tests/unit/__init__.py diff --git a/providers/sdk/java/tests/unit/__init__.py b/sdk/coordinators/java/tests/unit/coordinators/__init__.py similarity index 92% rename from providers/sdk/java/tests/unit/__init__.py rename to sdk/coordinators/java/tests/unit/coordinators/__init__.py index 5966d6b1d5261..13a83393a9124 100644 --- a/providers/sdk/java/tests/unit/__init__.py +++ b/sdk/coordinators/java/tests/unit/coordinators/__init__.py @@ -14,4 +14,3 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/providers/sdk/java/src/airflow/__init__.py b/sdk/coordinators/java/tests/unit/coordinators/java/__init__.py similarity index 92% rename from providers/sdk/java/src/airflow/__init__.py rename to sdk/coordinators/java/tests/unit/coordinators/java/__init__.py index 5966d6b1d5261..13a83393a9124 100644 --- a/providers/sdk/java/src/airflow/__init__.py +++ b/sdk/coordinators/java/tests/unit/coordinators/java/__init__.py @@ -14,4 +14,3 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/providers/sdk/java/tests/unit/sdk/java/test_bundle_scanner.py b/sdk/coordinators/java/tests/unit/coordinators/java/test_bundle_scanner.py similarity index 96% rename from providers/sdk/java/tests/unit/sdk/java/test_bundle_scanner.py rename to sdk/coordinators/java/tests/unit/coordinators/java/test_bundle_scanner.py index 5c042036143da..93457aa1a9755 100644 --- a/providers/sdk/java/tests/unit/sdk/java/test_bundle_scanner.py +++ b/sdk/coordinators/java/tests/unit/coordinators/java/test_bundle_scanner.py @@ -24,7 +24,7 @@ import pytest import yaml -from airflow.providers.sdk.java.bundle_scanner import ( +from airflow.sdk.coordinators.java.bundle_scanner import ( DAG_CODE_MANIFEST_KEY, MAIN_CLASS_MANIFEST_KEY, MANIFEST_PATH, @@ -252,7 +252,6 @@ def test_nested_layout(self, tmp_path: Path): scanner = BundleScanner(tmp_path) homes = scanner._candidate_homes() - # Nested subdirs + the bundles_dir itself assert len(homes) == 3 assert sub_a.resolve() in homes assert sub_b.resolve() in homes @@ -262,7 +261,6 @@ def test_flat_layout(self, tmp_path: Path): (tmp_path / "app.jar").touch() scanner = BundleScanner(tmp_path) homes = scanner._candidate_homes() - # Only the directory itself (no subdirectories) assert homes == [tmp_path.resolve()] def test_nested_with_lib_subdir(self, tmp_path: Path): @@ -274,7 +272,6 @@ def test_nested_with_lib_subdir(self, tmp_path: Path): scanner = BundleScanner(tmp_path) homes = scanner._candidate_homes() - # _normalize_bundle_home should redirect to lib/ assert lib.resolve() in homes @@ -303,9 +300,8 @@ def test_classpath_includes_all_jars(self, tmp_path: Path): bundle_dir = tmp_path / "my_bundle" bundle_dir.mkdir() _create_bundle_jar(bundle_dir / "app.jar", dag_ids=["my_dag"]) - # Create a dependency JAR (no SDK metadata, just a plain JAR) with zipfile.ZipFile(bundle_dir / "dep.jar", "w") as zf: - zf.writestr("dummy.class", b"") + zf.writestr("placeholder.class", b"") scanner = BundleScanner(tmp_path) result = scanner.resolve("my_dag") @@ -322,9 +318,8 @@ def test_flat_layout_resolve(self, tmp_path: Path): def test_skips_non_bundle_jars(self, tmp_path: Path): bundle_dir = tmp_path / "my_bundle" bundle_dir.mkdir() - # Non-bundle JAR (no manifest) with zipfile.ZipFile(bundle_dir / "plain.jar", "w") as zf: - zf.writestr("dummy.class", b"") + zf.writestr("placeholder.class", b"") _create_bundle_jar(bundle_dir / "real.jar", dag_ids=["real_dag"]) scanner = BundleScanner(tmp_path) diff --git a/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py b/sdk/coordinators/java/tests/unit/coordinators/java/test_coordinator.py similarity index 65% rename from providers/sdk/java/tests/unit/sdk/java/test_coordinator.py rename to sdk/coordinators/java/tests/unit/coordinators/java/test_coordinator.py index d5dc053a822bf..3a101f30cba88 100644 --- a/providers/sdk/java/tests/unit/sdk/java/test_coordinator.py +++ b/sdk/coordinators/java/tests/unit/coordinators/java/test_coordinator.py @@ -20,19 +20,18 @@ import uuid import zipfile from pathlib import Path -from unittest.mock import patch import pytest import yaml -from airflow.providers.sdk.java.bundle_scanner import ( +from airflow.sdk.api.datamodels._generated import BundleInfo +from airflow.sdk.coordinators.java.bundle_scanner import ( MAIN_CLASS_MANIFEST_KEY, MANIFEST_PATH, METADATA_MANIFEST_KEY, SDK_VERSION_MANIFEST_KEY, ) -from airflow.providers.sdk.java.coordinator import JavaCoordinator -from airflow.sdk.api.datamodels._generated import BundleInfo +from airflow.sdk.coordinators.java.coordinator import JavaCoordinator from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO from tests_common.test_utils.version_compat import AIRFLOW_V_3_3_PLUS @@ -102,49 +101,65 @@ def test_sdk(self): def test_file_extension(self): assert JavaCoordinator.file_extension == ".jar" + def test_default_kwargs(self): + coordinator = JavaCoordinator() + assert coordinator.java_executable == "java" + assert coordinator.jvm_args == [] + assert coordinator.bundles_folder is None + + def test_custom_kwargs(self): + coordinator = JavaCoordinator( + java_executable="/opt/java/bin/java", + jvm_args=["-Xmx512m", "-Xms256m"], + bundles_folder="/airflow/java-bundles", + ) + assert coordinator.java_executable == "/opt/java/bin/java" + assert coordinator.jvm_args == ["-Xmx512m", "-Xms256m"] + assert coordinator.bundles_folder == "/airflow/java-bundles" + class TestCanHandleDagFile: def test_valid_jar_returns_true(self, tmp_path: Path): jar = _create_bundle_jar(tmp_path / "valid.jar", dag_ids=["d"]) - assert JavaCoordinator.can_handle_dag_file("bundle", str(jar)) is True + assert JavaCoordinator().can_handle_dag_file("bundle", str(jar)) is True def test_non_jar_file_returns_false(self, tmp_path: Path): py_file = tmp_path / "dag.py" py_file.write_text("from airflow import DAG") - assert JavaCoordinator.can_handle_dag_file("bundle", str(py_file)) is False + assert JavaCoordinator().can_handle_dag_file("bundle", str(py_file)) is False def test_missing_file_returns_false(self, tmp_path: Path): - assert JavaCoordinator.can_handle_dag_file("bundle", str(tmp_path / "missing.jar")) is False + assert JavaCoordinator().can_handle_dag_file("bundle", str(tmp_path / "missing.jar")) is False def test_bad_zip_returns_false(self, tmp_path: Path): bad = tmp_path / "bad.jar" bad.write_text("not a zip") - assert JavaCoordinator.can_handle_dag_file("bundle", str(bad)) is False + assert JavaCoordinator().can_handle_dag_file("bundle", str(bad)) is False def test_jar_without_sdk_manifest_returns_false(self, tmp_path: Path): jar = tmp_path / "plain.jar" with zipfile.ZipFile(jar, "w") as zf: - zf.writestr("dummy.class", b"") - assert JavaCoordinator.can_handle_dag_file("bundle", str(jar)) is False + zf.writestr("placeholder.class", b"") + assert JavaCoordinator().can_handle_dag_file("bundle", str(jar)) is False class TestGetCodeFromFile: def test_returns_embedded_code(self, tmp_path: Path): code = "from airflow import DAG\ndag = DAG('my_dag')" jar = _create_bundle_jar(tmp_path / "with_code.jar", dag_ids=["d"], dag_code=code) - assert JavaCoordinator.get_code_from_file(str(jar)) == code + assert JavaCoordinator().get_code_from_file(str(jar)) == code def test_raises_when_no_code(self, tmp_path: Path): jar = _create_bundle_jar(tmp_path / "no_code.jar", dag_ids=["d"]) with pytest.raises(FileNotFoundError, match="No DAG source code found in JAR"): - JavaCoordinator.get_code_from_file(str(jar)) + JavaCoordinator().get_code_from_file(str(jar)) class TestDagParsingCmd: - def test_builds_java_command(self, tmp_path: Path): + def test_builds_default_java_command(self, tmp_path: Path): jar = _create_bundle_jar(tmp_path / "app.jar", dag_ids=["d"]) bundle_path = str(tmp_path) - cmd = JavaCoordinator.dag_parsing_cmd( + cmd = JavaCoordinator().dag_parsing_cmd( dag_file_path=str(jar), bundle_name="my_bundle", bundle_path=bundle_path, @@ -160,6 +175,31 @@ def test_builds_java_command(self, tmp_path: Path): "--logs=localhost:5678", ] + def test_uses_custom_executable_and_jvm_args(self, tmp_path: Path): + jar = _create_bundle_jar(tmp_path / "app.jar", dag_ids=["d"]) + bundle_path = str(tmp_path) + coordinator = JavaCoordinator( + java_executable="/opt/jdk-17/bin/java", + jvm_args=["-Xmx1024m", "-Xms256m"], + ) + cmd = coordinator.dag_parsing_cmd( + dag_file_path=str(jar), + bundle_name="my_bundle", + bundle_path=bundle_path, + comm_addr="localhost:1234", + logs_addr="localhost:5678", + ) + assert cmd == [ + "/opt/jdk-17/bin/java", + "-Xmx1024m", + "-Xms256m", + "-classpath", + f"{bundle_path}/*", + TEST_MAIN_CLASS, + "--comm=localhost:1234", + "--logs=localhost:5678", + ] + class TestTaskExecutionCmd: def test_pure_java_dag(self, tmp_path: Path): @@ -168,7 +208,7 @@ def test_pure_java_dag(self, tmp_path: Path): ti = _make_ti() bundle_info = BundleInfo(name="my_bundle") - cmd = JavaCoordinator.task_execution_cmd( + cmd = JavaCoordinator().task_execution_cmd( what=ti, # type: ignore[arg-type] dag_file_path=str(jar), bundle_path=bundle_path, @@ -185,7 +225,7 @@ def test_pure_java_dag(self, tmp_path: Path): "--logs=localhost:5678", ] - def test_python_stub_dag_with_bundles_folder(self, tmp_path: Path): + def test_python_stub_dag_uses_bundles_folder_kwarg(self, tmp_path: Path): bundles_folder = tmp_path / "java_bundles" bundle_sub = bundles_folder / "my_bundle" bundle_sub.mkdir(parents=True) @@ -194,11 +234,31 @@ def test_python_stub_dag_with_bundles_folder(self, tmp_path: Path): ti = _make_ti(dag_id="stub_dag") bundle_info = BundleInfo(name="my_bundle") - with patch( - "airflow.providers.common.compat.sdk.conf.get", - return_value=str(bundles_folder), - ): - cmd = JavaCoordinator.task_execution_cmd( + coordinator = JavaCoordinator(bundles_folder=str(bundles_folder)) + cmd = coordinator.task_execution_cmd( + what=ti, # type: ignore[arg-type] + dag_file_path="/dags/stub_dag.py", + bundle_path="/some/bundle/path", + bundle_info=bundle_info, + comm_addr="localhost:1234", + logs_addr="localhost:5678", + ) + + assert cmd == [ + "java", + "-classpath", + f"{bundles_folder}/my_bundle/app.jar", + TEST_MAIN_CLASS, + "--comm=localhost:1234", + "--logs=localhost:5678", + ] + + def test_python_stub_dag_without_bundles_folder_raises(self): + ti = _make_ti() + bundle_info = BundleInfo(name="my_bundle") + + with pytest.raises(ValueError, match="bundles_folder kwarg must be set"): + JavaCoordinator().task_execution_cmd( what=ti, # type: ignore[arg-type] dag_file_path="/dags/stub_dag.py", bundle_path="/some/bundle/path", @@ -206,37 +266,3 @@ def test_python_stub_dag_with_bundles_folder(self, tmp_path: Path): comm_addr="localhost:1234", logs_addr="localhost:5678", ) - - assert cmd == [ - "java", - "-classpath", - f"{bundles_folder}/my_bundle/app.jar", - TEST_MAIN_CLASS, - "--comm=localhost:1234", - "--logs=localhost:5678", - ] - - @pytest.mark.parametrize( - "config_value", - [ - pytest.param(None, id="none"), - pytest.param("", id="empty_string"), - ], - ) - def test_python_stub_dag_invalid_config_raises(self, config_value): - ti = _make_ti() - bundle_info = BundleInfo(name="my_bundle") - - with patch( - "airflow.providers.common.compat.sdk.conf.get", - return_value=config_value, - ): - with pytest.raises(ValueError, match="bundles_folder config must be set"): - JavaCoordinator.task_execution_cmd( - what=ti, # type: ignore[arg-type] - dag_file_path="/dags/stub_dag.py", - bundle_path="/some/bundle/path", - bundle_info=bundle_info, - comm_addr="localhost:1234", - logs_addr="localhost:5678", - ) diff --git a/task-sdk/src/airflow/sdk/__init__.py b/task-sdk/src/airflow/sdk/__init__.py index eeae86f1eb3d0..7a5ed26747312 100644 --- a/task-sdk/src/airflow/sdk/__init__.py +++ b/task-sdk/src/airflow/sdk/__init__.py @@ -16,6 +16,11 @@ # under the License. from __future__ import annotations +# Make ``airflow.sdk`` a namespace-extending package so sibling distributions +# (e.g. ``apache-airflow-coordinators-java`` shipping +# ``airflow/sdk/coordinators/java/``) can contribute sub-packages. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) + from typing import TYPE_CHECKING __all__ = [ diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 5f411e643ed5f..fb842b0929f84 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -20,7 +20,9 @@ Provides :class:`BaseCoordinator`, the base class for SDK-specific coordinators that bridge subprocess I/O between the -Airflow supervisor and an external-SDK runtime (Java, Go, Rust, etc.). +Airflow supervisor and an external-SDK runtime (Java, Go, Rust, etc.), +and :class:`CoordinatorManager`, the registry that loads coordinator +instances from the ``[sdk] coordinators`` configuration. The coordinator's :meth:`~BaseCoordinator.run_dag_parsing` method handles the full lifecycle: @@ -41,12 +43,15 @@ from __future__ import annotations import contextlib +import functools import os import selectors import socket import subprocess import time -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, ClassVar, NamedTuple + +from airflow.sdk._shared.module_loading import import_string if TYPE_CHECKING: from structlog.typing import FilteringBoundLogger @@ -79,7 +84,7 @@ def _send_startup_details(runtime_comm: socket.socket, startup_details: StartupD from airflow.sdk.execution_time.comms import _ResponseFrame # Use mode="json" so that datetime, UUID, and other complex Python - # types are serialized as plain strings/numbers in msgpack — avoiding + # types are serialized as plain strings/numbers in msgpack -- avoiding # msgpack extension types (e.g. Timestamp) that non-Python decoders # may not support. frame = _ResponseFrame(id=0, body=startup_details.model_dump(mode="json")) @@ -171,20 +176,20 @@ class BaseCoordinator: """ Base coordinator for runtime-specific DAG file processing and task execution. - Providers register subclasses in their ``provider.yaml`` under - ``coordinators``. Both :class:`ProvidersManager` (airflow-core) - and :class:`ProvidersManagerTaskRuntime` (task-sdk) discover registered - coordinators through this single extension point. + Coordinators are instantiated from the ``[sdk] coordinators`` configuration + (see :class:`CoordinatorManager`) — each entry's ``classpath`` is resolved + via :func:`~airflow.sdk._shared.module_loading.import_string` and + constructed with the entry's ``kwargs``. - Subclasses represent a specific SDK runtime (Java, Go, etc.) and - only need to implement :meth:`can_handle_dag_file`, - :meth:`dag_parsing_cmd` and :meth:`task_execution_cmd`. - The base class owns the entire bridge lifecycle: TCP servers, - subprocess management, selector-based I/O loop, and cleanup. + Subclasses represent a specific SDK runtime (Java, Go, etc.) and only + need to implement :meth:`can_handle_dag_file`, :meth:`dag_parsing_cmd` + and :meth:`task_execution_cmd`. The base class owns the entire bridge + lifecycle: TCP servers, subprocess management, selector-based I/O loop, + and cleanup. """ - sdk: str - file_extension: str + sdk: ClassVar[str] + file_extension: ClassVar[str] class DagParsingInfo(NamedTuple): """Information needed for runtime Dag parsing.""" @@ -203,8 +208,7 @@ class TaskExecutionInfo(NamedTuple): startup_details: StartupDetails mode: str = "task-execution" - @classmethod - def can_handle_dag_file(cls, bundle_name: str, path: str | os.PathLike[str]) -> bool: + def can_handle_dag_file(self, bundle_name: str, path: str | os.PathLike[str]) -> bool: """ Return ``True`` if this coordinator should handle DAG-file parsing for *path*. @@ -216,8 +220,7 @@ def can_handle_dag_file(cls, bundle_name: str, path: str | os.PathLike[str]) -> """ return False - @classmethod - def get_code_from_file(cls, fileloc: str) -> str: + def get_code_from_file(self, fileloc: str) -> str: """ Return the human-readable source code for a DAG file managed by this coordinator. @@ -233,9 +236,8 @@ def get_code_from_file(cls, fileloc: str) -> str: """ raise NotImplementedError - @classmethod def dag_parsing_cmd( - cls, + self, *, dag_file_path: str, bundle_name: str, @@ -257,9 +259,8 @@ def dag_parsing_cmd( """ raise NotImplementedError - @classmethod def task_execution_cmd( - cls, + self, *, what: TaskInstanceDTO, dag_file_path: str, @@ -283,28 +284,26 @@ def task_execution_cmd( """ raise NotImplementedError - @classmethod - def run_dag_parsing(cls, *, path: str, bundle_name: str, bundle_path: str) -> None: + def run_dag_parsing(self, *, path: str, bundle_name: str, bundle_path: str) -> None: """Entry point for running runtime-specific Dag File Processing.""" - cls._runtime_subprocess_entrypoint( - cls.DagParsingInfo( + self._runtime_subprocess_entrypoint( + self.DagParsingInfo( dag_file_path=path, bundle_name=bundle_name, bundle_path=bundle_path, ) ) - @classmethod def run_task_execution( - cls, + self, *, what: TaskInstanceDTO, dag_rel_path: str | os.PathLike[str], bundle_info: BundleInfo, startup_details: StartupDetails, ) -> None: - cls._runtime_subprocess_entrypoint( - cls.TaskExecutionInfo( + self._runtime_subprocess_entrypoint( + self.TaskExecutionInfo( what=what, dag_rel_path=dag_rel_path, bundle_info=bundle_info, @@ -312,8 +311,7 @@ def run_task_execution( ) ) - @classmethod - def _runtime_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskExecutionInfo) -> None: + def _runtime_subprocess_entrypoint(self, entrypoint_info: DagParsingInfo | TaskExecutionInfo) -> None: """ Spawn the runtime subprocess and bridge I/O with the supervisor. @@ -343,7 +341,7 @@ def _runtime_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskEx log = structlog.get_logger(logger_name="task") log.info( "Starting runtime subprocess", - sdk=cls.sdk, + sdk=self.sdk, mode=entrypoint_info.mode, ) @@ -365,22 +363,22 @@ def _runtime_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskEx # garbage-collected while the runtime process is still running. bundle_version_lock: contextlib.AbstractContextManager = contextlib.nullcontext() - if isinstance(entrypoint_info, cls.DagParsingInfo): - cmd = cls.dag_parsing_cmd( + if isinstance(entrypoint_info, self.DagParsingInfo): + cmd = self.dag_parsing_cmd( dag_file_path=entrypoint_info.dag_file_path, bundle_name=entrypoint_info.bundle_name, bundle_path=entrypoint_info.bundle_path, comm_addr=comm_addr, logs_addr=logs_addr, ) - elif isinstance(entrypoint_info, cls.TaskExecutionInfo): + elif isinstance(entrypoint_info, self.TaskExecutionInfo): from airflow.dag_processing.bundles.base import BundleVersionLock from airflow.sdk.execution_time.task_runner import resolve_bundle bundle_instance = resolve_bundle(entrypoint_info.bundle_info, log) resolved_dag_file_path = bundle_instance.path / entrypoint_info.dag_rel_path - cmd = cls.task_execution_cmd( + cmd = self.task_execution_cmd( what=entrypoint_info.what, dag_file_path=os.fspath(resolved_dag_file_path), bundle_path=os.fspath(bundle_instance.path), @@ -415,7 +413,7 @@ def _runtime_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskEx # on fd 0 and ``task_runner.main()`` consumed it before delegating # here. Re-encode and forward it to the runtime subprocess so it # knows which task to execute. - if isinstance(entrypoint_info, cls.TaskExecutionInfo): + if isinstance(entrypoint_info, self.TaskExecutionInfo): _send_startup_details(runtime_comm, entrypoint_info.startup_details) # fd 0 is the bidirectional comms socket to the supervisor. @@ -424,39 +422,113 @@ def _runtime_subprocess_entrypoint(cls, entrypoint_info: DagParsingInfo | TaskEx _bridge(supervisor_comm, runtime_comm, runtime_logs, read_stderr, proc, log) -class QueueToCoordinatorMapper: +class CoordinatorManager: """ - Map queue names to coordinator names. + Registry of coordinator instances loaded from the ``[sdk] coordinators`` config. - Users often use queues as environment/isolation identifiers (e.g. ``"java-11"``, - ``"java-12"``). This mapper lets them reuse existing queue assignments to route - tasks to the correct coordinator. + Each entry in the JSON list takes the form:: - The mapping is read from the ``[sdk] queue_to_sdk`` - configuration option, which is a JSON dict of ``queue -> sdk``. + { + "name": "jdk-11", + "classpath": "airflow.sdk.coordinators.java.JavaCoordinator", + "kwargs": {"java_executable": "/usr/lib/jvm/jdk-11/bin/java", ...} + } - Example configuration:: + The ``classpath`` is resolved via + :func:`~airflow.sdk._shared.module_loading.import_string` (no + :class:`ProvidersManager` involvement) and constructed with ``kwargs``. - [sdk] - queue_to_sdk = {"java-11": "java", "java-12": "java"} + The ``[sdk] queue_to_coordinator`` config maps queue names to a coordinator + ``name`` from that list, which lets users reuse existing queue assignments + to route tasks to a specific coordinator instance (for example, a + ``"legacy-java"`` queue routed to a JDK 11 coordinator and a + ``"modern-java"`` queue routed to a JDK 17 coordinator). """ - def __init__(self, mapping: dict[str, str]) -> None: - self._mapping = mapping + def __init__( + self, + instances_by_name: dict[str, BaseCoordinator], + queue_to_coordinator: dict[str, str], + ) -> None: + self._instances_by_name = instances_by_name + self._queue_to_coordinator = queue_to_coordinator @classmethod def from_config(cls) -> Self: - """Load the queue-to-runtime mapping from airflow configuration.""" + """Load coordinator instances from the ``[sdk]`` configuration.""" from airflow.sdk.configuration import conf - mapping = conf.getjson("sdk", "queue_to_sdk", fallback={}) - if not isinstance(mapping, dict): - return cls({}) - return cls(mapping) - - def resolve(self, queue: str) -> str | None: - """Return the runtime coordinator name for *queue*, or ``None`` if unmapped.""" - return self._mapping.get(queue) - - -__all__ = ["BaseCoordinator", "QueueToCoordinatorMapper"] + entries = conf.getjson("sdk", "coordinators", fallback=[]) + if not isinstance(entries, list): + entries = [] + + instances: dict[str, BaseCoordinator] = {} + for entry in entries: + if not isinstance(entry, dict): + continue + name = entry.get("name") + classpath = entry.get("classpath") + if not name or not classpath: + continue + kwargs = entry.get("kwargs") or {} + coordinator_cls = import_string(classpath) + instances[name] = coordinator_cls(**kwargs) + + queue_mapping = conf.getjson("sdk", "queue_to_coordinator", fallback={}) + if not isinstance(queue_mapping, dict): + queue_mapping = {} + + return cls(instances, queue_mapping) + + def all(self) -> list[BaseCoordinator]: + """Return all loaded coordinator instances, sorted by configured name.""" + return [self._instances_by_name[name] for name in sorted(self._instances_by_name)] + + def get(self, name: str) -> BaseCoordinator | None: + """Return the coordinator instance registered under *name*, or ``None``.""" + return self._instances_by_name.get(name) + + def for_queue(self, queue: str) -> BaseCoordinator | None: + """Return the coordinator instance routed to *queue*, or ``None``.""" + name = self._queue_to_coordinator.get(queue) + if name is None: + return None + return self._instances_by_name.get(name) + + def for_dag_file(self, bundle_name: str, path: str | os.PathLike[str]) -> BaseCoordinator | None: + """Return the first coordinator whose ``can_handle_dag_file`` matches *path*.""" + for instance in self.all(): + try: + if instance.can_handle_dag_file(bundle_name, path): + return instance + except Exception: + continue + return None + + def file_extensions(self) -> tuple[str, ...]: + """Return the file extensions registered by all loaded coordinators.""" + extensions: list[str] = [] + for instance in self.all(): + ext = getattr(type(instance), "file_extension", None) + if ext: + extensions.append(ext) + return tuple(extensions) + + +@functools.cache +def get_coordinator_manager() -> CoordinatorManager: + """Return the process-wide :class:`CoordinatorManager`, loaded from config on first use.""" + return CoordinatorManager.from_config() + + +def reset_coordinator_manager() -> None: + """Clear the cached :class:`CoordinatorManager` (test helper).""" + get_coordinator_manager.cache_clear() + + +__all__ = [ + "BaseCoordinator", + "CoordinatorManager", + "get_coordinator_manager", + "reset_coordinator_manager", +] diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 35e326dca68b4..bb8b320575f6c 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -2094,84 +2094,60 @@ def flush_spans(): def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> Callable[[], None] | None: """ - Check provider-registered runtime coordinators for a runtime-specific entrypoint. + Check configured runtime coordinators for a runtime-specific entrypoint. Resolution order: - 1. **Queue mapping** -- the ``[sdk] queue_to_sdk`` config maps - the task's ``queue`` to a runtime coordinator name (e.g. ``"java-queue" -> "java"``). - Used by the python-stub pattern where users set ``queue="java-queue"`` explicitly. - 2. **DAG file extension** -- if no queue mapping matches, the DAG file's extension - (e.g. ``.jar``) is compared against each coordinator's ``file_extension`` attribute. - Used by the pure-Java (or pure-) pattern where the entire DAG is authored - in a non-Python language. + 1. **Queue mapping** -- the ``[sdk] queue_to_coordinator`` config maps + the task's ``queue`` to a coordinator name from ``[sdk] coordinators``. + Used by the python-stub pattern where users set the queue explicitly. + 2. **DAG file extension** -- if no queue mapping matches, the DAG file's + extension (e.g. ``.jar``) is compared against each coordinator's + ``file_extension``. Used by the pure-runtime DAG pattern where the + entire DAG is authored in a non-Python language. Returns a no-arg callable that bridges fd 0 to the runtime subprocess, or ``None`` to fall through to the standard Python execution path. """ import functools - from airflow.sdk.execution_time.coordinator import QueueToCoordinatorMapper - from airflow.sdk.providers_manager_runtime import ProvidersManagerTaskRuntime + from airflow.sdk.execution_time.coordinator import get_coordinator_manager - coordinators = ProvidersManagerTaskRuntime().coordinators + manager = get_coordinator_manager() - # Step 1: queue-to-runtime mapping. - queue = startup_details.ti.queue - if (sdk := QueueToCoordinatorMapper.from_config().resolve(queue)) is not None: - for coordinator_cls in coordinators: - if not hasattr(coordinator_cls, "run_task_execution"): - continue - if getattr(coordinator_cls, "sdk", None) != sdk: - continue - - log.debug( - "Resolved sdk-specific entrypoint for task via queue mapping", - coordinator=coordinator_cls, - sdk=sdk, - queue=queue, - task_id=startup_details.ti.task_id, - ) - return functools.partial( - coordinator_cls.run_task_execution, - what=startup_details.ti, - dag_rel_path=startup_details.dag_rel_path, - bundle_info=startup_details.bundle_info, - startup_details=startup_details, - ) + def _build(coordinator) -> Callable[[], None]: + return functools.partial( + coordinator.run_task_execution, + what=startup_details.ti, + dag_rel_path=startup_details.dag_rel_path, + bundle_info=startup_details.bundle_info, + startup_details=startup_details, + ) - log.warning( - "No coordinator found for sdk", - sdk=sdk, + # Step 1: queue-to-coordinator mapping. + queue = startup_details.ti.queue + if (coordinator := manager.for_queue(queue)) is not None: + log.debug( + "Resolved coordinator for task via queue mapping", + coordinator=type(coordinator).__qualname__, queue=queue, task_id=startup_details.ti.task_id, ) - return None + return _build(coordinator) # Step 2: DAG file extension fallback (pure- DAGs). dag_rel_path = startup_details.dag_rel_path - for coordinator_cls in coordinators: - # TODO: Use `can_handle_dag_file` method instead of file_extension attribute for better maintainability. - ext = getattr(coordinator_cls, "file_extension", None) + for coordinator in manager.all(): + ext = getattr(type(coordinator), "file_extension", None) if not ext or not dag_rel_path.endswith(ext): continue - if not hasattr(coordinator_cls, "run_task_execution"): - continue - log.debug( - "Resolved runtime-specific entrypoint for task via DAG file extension", - coordinator=coordinator_cls, - sdk=getattr(coordinator_cls, "sdk", None), + "Resolved coordinator for task via DAG file extension", + coordinator=type(coordinator).__qualname__, dag_rel_path=dag_rel_path, task_id=startup_details.ti.task_id, ) - return functools.partial( - coordinator_cls.run_task_execution, - what=startup_details.ti, - dag_rel_path=startup_details.dag_rel_path, - bundle_info=startup_details.bundle_info, - startup_details=startup_details, - ) + return _build(coordinator) log.debug( "No runtime coordinator matched, using standard Python execution path", @@ -2209,9 +2185,10 @@ def main(): # startup message as a ResendLoggingFD response. if os.environ.pop("_AIRFLOW_FORK_EXEC", None) == "1": reinit_supervisor_comms() - # Check if a provider-registered runtime coordinator should - # handle this task (e.g. Java, Go) instead of the standard - # Python execution path. + # Check if a configured runtime coordinator should handle this + # task (e.g. Java, Go) instead of the standard Python + # execution path. + log.debug("Checking for runtime-specific entrypoint") runtime_entrypoint = _resolve_runtime_entrypoint(startup_details, log) if runtime_entrypoint is not None: runtime_entrypoint() diff --git a/task-sdk/src/airflow/sdk/providers_manager_runtime.py b/task-sdk/src/airflow/sdk/providers_manager_runtime.py index 63c8c97f816ef..e28ed3fe14a83 100644 --- a/task-sdk/src/airflow/sdk/providers_manager_runtime.py +++ b/task-sdk/src/airflow/sdk/providers_manager_runtime.py @@ -51,7 +51,6 @@ from airflow.sdk import BaseHook from airflow.sdk.bases.decorator import TaskDecorator from airflow.sdk.definitions.asset import Asset - from airflow.sdk.execution_time.coordinator import BaseCoordinator log = structlog.getLogger(__name__) @@ -151,7 +150,6 @@ def __init__(self): # Keeps dict of hooks keyed by connection type. They are lazy evaluated at access time self._hooks_lazy_dict: LazyDictWithCache[str, HookInfo | Callable] = LazyDictWithCache() self._plugins_set: set[PluginInfo] = set() - self._coordinators: list[type[BaseCoordinator]] = [] self._provider_schema_validator = _create_provider_info_schema_validator() self._init_airflow_core_hooks() # Populated by initialize_provider_configs(); holds provider-contributed config sections. @@ -222,12 +220,6 @@ def initialize_providers_taskflow_decorator(self): self.initialize_providers_list() self._discover_taskflow_decorators() - @provider_info_cache("coordinators") - def initialize_providers_coordinators(self): - """Lazy initialization of providers runtime coordinators.""" - self.initialize_providers_list() - self._discover_coordinators() - @provider_info_cache("provider_configs") def initialize_provider_configs(self): """Lazy initialization of provider configuration metadata and merge it into SDK ``conf``.""" @@ -472,19 +464,6 @@ def _import_hook( connection_testable=hasattr(hook_class, "test_connection"), ) - def _discover_coordinators(self) -> None: - """Retrieve and pre-load all coordinators defined in the providers.""" - seen: set[str] = set() - for provider_package, provider in self._provider_dict.items(): - for coordinator_class_path in provider.data.get("coordinators", []): - if coordinator_class_path in seen: - continue - coordinator_cls = _correctness_check(provider_package, coordinator_class_path, provider) - if coordinator_cls: - seen.add(coordinator_class_path) - self._coordinators.append(coordinator_cls) - self._coordinators = sorted(self._coordinators, key=lambda c: c.__qualname__) - def _discover_filesystems(self) -> None: """Retrieve all filesystems defined in the providers.""" for provider_package, provider in self._provider_dict.items(): @@ -632,12 +611,6 @@ def plugins(self) -> list[PluginInfo]: self.initialize_providers_plugins() return sorted(self._plugins_set, key=lambda x: x.plugin_class) - @property - def coordinators(self) -> list[type[BaseCoordinator]]: - """Returns pre-loaded runtime coordinator classes available in providers.""" - self.initialize_providers_coordinators() - return self._coordinators - @property def provider_configs(self) -> list[tuple[str, dict[str, Any]]]: self.initialize_provider_configs() @@ -670,7 +643,6 @@ def _cleanup(self): self._asset_uri_handlers.clear() self._asset_factories.clear() self._asset_to_openlineage_converters.clear() - self._coordinators.clear() self._provider_configs.clear() # Imported lazily to preserve SDK conf lazy initialization and avoid a configuration/runtime cycle. diff --git a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py index 082cfaf6051b9..7c11022755d9f 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py +++ b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py @@ -18,6 +18,7 @@ from __future__ import annotations import contextlib +import json import os import socket import subprocess @@ -28,9 +29,12 @@ from airflow.sdk.execution_time.coordinator import ( BaseCoordinator, + CoordinatorManager, _bridge, _send_startup_details, _start_server, + get_coordinator_manager, + reset_coordinator_manager, ) @@ -50,7 +54,6 @@ def test_assigns_random_port(self): try: _, port1 = s1.getsockname() _, port2 = s2.getsockname() - # Two servers should get different ports assert port1 != port2 finally: s1.close() @@ -73,7 +76,6 @@ def test_accepts_connection(self): class TestSendStartupDetails: def test_sends_frame_bytes_to_socket(self): - """Verify _send_startup_details calls sendall with a length-prefixed msgpack frame.""" mock_startup = MagicMock() mock_startup.model_dump.return_value = {"type": "StartupDetails", "ti": {}} @@ -85,13 +87,11 @@ def test_sends_frame_bytes_to_socket(self): mock_socket.sendall.assert_called_once() sent_bytes = mock_socket.sendall.call_args[0][0] - # First 4 bytes are the big-endian length prefix assert len(sent_bytes) > 4 length = int.from_bytes(sent_bytes[:4], "big") assert length == len(sent_bytes) - 4 def test_frame_contains_response_id_zero(self): - """The frame should use id=0.""" import msgpack mock_startup = MagicMock() @@ -102,12 +102,10 @@ def test_frame_contains_response_id_zero(self): _send_startup_details(mock_socket, mock_startup) sent_bytes = mock_socket.sendall.call_args[0][0] - # Frame is encoded as [id, body, error] frame = msgpack.unpackb(sent_bytes[4:]) assert frame[0] == 0 def test_frame_body_matches_model_dump(self): - """The frame body should be the model_dump(mode='json') output.""" import msgpack body = {"type": "StartupDetails", "ti": {"task_id": "t1"}, "dag_rel_path": "test.jar"} @@ -119,12 +117,10 @@ def test_frame_body_matches_model_dump(self): _send_startup_details(mock_socket, mock_startup) sent_bytes = mock_socket.sendall.call_args[0][0] - # Frame is encoded as [id, body, error] frame = msgpack.unpackb(sent_bytes[4:]) assert frame[1] == body def test_real_socket_roundtrip(self): - """Send through real sockets and verify the frame is receivable.""" import msgpack server = socket.socket() @@ -143,11 +139,9 @@ def test_real_socket_roundtrip(self): _send_startup_details(conn, mock_startup) - # Read the length prefix length_bytes = client.recv(4) length = int.from_bytes(length_bytes, "big") - # Read the payload — frame is [id, body, error] data = client.recv(length) frame = msgpack.unpackb(data) assert frame[0] == 0 @@ -160,15 +154,15 @@ def test_real_socket_roundtrip(self): class TestBaseCoordinatorDefaults: def test_can_handle_dag_file_returns_false(self): - assert BaseCoordinator.can_handle_dag_file("bundle", "/path/to/dag.py") is False + assert BaseCoordinator().can_handle_dag_file("bundle", "/path/to/dag.py") is False def test_get_code_from_file_raises_not_implemented(self): with pytest.raises(NotImplementedError): - BaseCoordinator.get_code_from_file("/path/to/dag.jar") + BaseCoordinator().get_code_from_file("/path/to/dag.jar") def test_dag_parsing_cmd_raises_not_implemented(self): with pytest.raises(NotImplementedError): - BaseCoordinator.dag_parsing_cmd( + BaseCoordinator().dag_parsing_cmd( dag_file_path="/dag.jar", bundle_name="b", bundle_path="/path", @@ -178,7 +172,7 @@ def test_dag_parsing_cmd_raises_not_implemented(self): def test_task_execution_cmd_raises_not_implemented(self): with pytest.raises(NotImplementedError): - BaseCoordinator.task_execution_cmd( + BaseCoordinator().task_execution_cmd( what=MagicMock(), dag_file_path="/dag.jar", bundle_path="/path", @@ -217,53 +211,43 @@ def test_task_execution_info_defaults(self): class TestBridge: def test_bridge_forwards_comm_bidirectionally(self): - """Verify _bridge sets up bidirectional forwarding and processes all channels.""" - # Use real socketpairs for the 4 channels sup_send, sup_recv = socket.socketpair() rt_send, rt_recv = socket.socketpair() log_send, log_recv = socket.socketpair() stderr_send, stderr_recv = socket.socketpair() mock_proc = MagicMock(spec=subprocess.Popen) - # Make the process "exit" immediately so the bridge drains and stops mock_proc.poll.return_value = 0 mock_log = MagicMock() try: - # Send data before starting the bridge sup_send.sendall(b"from_supervisor") rt_send.sendall(b"from_runtime") log_send.sendall(b'{"event":"hello","level":"info"}\n') stderr_send.sendall(b"stderr line\n") - # Close sending sides so the bridge will see EOF sup_send.close() rt_send.close() log_send.close() stderr_send.close() _bridge(sup_recv, rt_recv, log_recv, stderr_recv, mock_proc, mock_log) - - # If we got here without hanging, the bridge correctly processed all channels finally: for s in (sup_send, rt_send, log_send, stderr_send, sup_recv, rt_recv, log_recv, stderr_recv): with contextlib.suppress(OSError): s.close() def test_bridge_drains_after_process_exit(self): - """Verify _bridge drains remaining data after the subprocess exits.""" sup_local, sup_remote = socket.socketpair() rt_local, rt_remote = socket.socketpair() log_local, log_remote = socket.socketpair() stderr_local, stderr_remote = socket.socketpair() mock_proc = MagicMock(spec=subprocess.Popen) - # First poll: still running; subsequent: exited mock_proc.poll.side_effect = [None, 0, 0, 0, 0, 0, 0, 0, 0, 0] mock_log = MagicMock() try: - # Send data after bridge starts its first iteration stderr_local.sendall(b"error output\n") stderr_local.close() sup_local.close() @@ -286,7 +270,6 @@ def test_bridge_drains_after_process_exit(self): s.close() def test_bridge_closes_all_sockets(self): - """Verify _bridge closes all four sockets when done.""" sup = MagicMock(spec=socket.socket) rt = MagicMock(spec=socket.socket) logs = MagicMock(spec=socket.socket) @@ -296,15 +279,12 @@ def test_bridge_closes_all_sockets(self): mock_proc.poll.return_value = 0 mock_log = MagicMock() - # Patch the selector to avoid real I/O; service_selector is imported inside - # _bridge so we patch it on the selector_loop module with ( patch("airflow.sdk.execution_time.coordinator.selectors.DefaultSelector") as mock_sel_cls, patch("airflow.sdk.execution_time.selector_loop.service_selector"), ): mock_sel = MagicMock() mock_sel_cls.return_value = mock_sel - # Empty selector map so the while loop exits immediately mock_sel.get_map.return_value = {} _bridge(sup, rt, logs, stderr, mock_proc, mock_log) @@ -316,10 +296,26 @@ def test_bridge_closes_all_sockets(self): mock_sel.close.assert_called_once() +class _StubCoordinator(BaseCoordinator): + sdk = "test" + file_extension = ".test" + + def __init__(self, *, parse_cmd: list[str] | None = None, exec_cmd: list[str] | None = None): + self._parse_cmd = parse_cmd or ["test-runtime", "--parse"] + self._exec_cmd = exec_cmd or ["test-runtime", "--execute"] + + def dag_parsing_cmd(self, *, dag_file_path, **_): + return [*self._parse_cmd, dag_file_path] + + def task_execution_cmd(self, *, dag_file_path, **_): + return [*self._exec_cmd, dag_file_path] + + class TestRunDagParsing: @patch.object(BaseCoordinator, "_runtime_subprocess_entrypoint") def test_run_dag_parsing_creates_dag_parsing_info(self, mock_entrypoint): - BaseCoordinator.run_dag_parsing( + coordinator = _StubCoordinator() + coordinator.run_dag_parsing( path="/bundles/my-bundle/dags/example.jar", bundle_name="my-bundle", bundle_path="/bundles/my-bundle", @@ -341,7 +337,8 @@ def test_run_task_execution_creates_task_execution_info(self, mock_entrypoint): mock_bundle_info = MagicMock() mock_startup = MagicMock() - BaseCoordinator.run_task_execution( + coordinator = _StubCoordinator() + coordinator.run_task_execution( what=mock_ti, dag_rel_path="dags/example.jar", bundle_info=mock_bundle_info, @@ -361,9 +358,6 @@ def test_run_task_execution_creates_task_execution_info(self, mock_entrypoint): class TestRuntimeSubprocessEntrypoint: @pytest.fixture(autouse=True) def _restore_process_context_env(self): - """``_runtime_subprocess_entrypoint`` runs inside a forked child in production - and sets ``_AIRFLOW_PROCESS_CONTEXT`` for the runtime subprocess. When tests - invoke it in-process, the env var leaks into other tests — restore it.""" old = os.environ.get("_AIRFLOW_PROCESS_CONTEXT") try: yield @@ -374,17 +368,12 @@ def _restore_process_context_env(self): os.environ["_AIRFLOW_PROCESS_CONTEXT"] = old def test_unknown_entrypoint_info_type_raises(self): - class TestCoordinator(BaseCoordinator): - sdk = "test" - file_extension = ".test" - - # Needs a 'mode' attribute (accessed during logging) but must not be - # an instance of DagParsingInfo or TaskExecutionInfo. + coordinator = _StubCoordinator() fake_info = MagicMock() fake_info.mode = "unknown" with pytest.raises(ValueError, match="Unknown entrypoint_info type"): - TestCoordinator._runtime_subprocess_entrypoint(fake_info) # type: ignore[arg-type] + coordinator._runtime_subprocess_entrypoint(fake_info) # type: ignore[arg-type] @patch("airflow.sdk.execution_time.coordinator._bridge") @patch("airflow.sdk.execution_time.coordinator._send_startup_details") @@ -392,36 +381,24 @@ class TestCoordinator(BaseCoordinator): @patch("airflow.sdk.execution_time.coordinator._start_server") @patch("os.dup", return_value=99) def test_dag_parsing_flow(self, mock_dup, mock_start_server, mock_popen, mock_send_startup, mock_bridge): - """Verify the dag-parsing entrypoint wires up servers, spawns subprocess, and bridges.""" - # Set up mock servers comm_server = MagicMock(spec=socket.socket) comm_server.getsockname.return_value = ("127.0.0.1", 5000) logs_server = MagicMock(spec=socket.socket) logs_server.getsockname.return_value = ("127.0.0.1", 5001) mock_start_server.side_effect = [comm_server, logs_server] - # The runtime connects back runtime_comm = MagicMock(spec=socket.socket) runtime_logs = MagicMock(spec=socket.socket) comm_server.accept.return_value = (runtime_comm, ("127.0.0.1", 9000)) logs_server.accept.return_value = (runtime_logs, ("127.0.0.1", 9001)) - # Mock socketpair for stderr child_stderr = MagicMock(spec=socket.socket) read_stderr = MagicMock(spec=socket.socket) child_stderr.fileno.return_value = 10 - # Mock supervisor_comm created from os.dup(0) supervisor_comm = MagicMock(spec=socket.socket) - class TestCoordinator(BaseCoordinator): - sdk = "test" - file_extension = ".test" - - @classmethod - def dag_parsing_cmd(cls, **kwargs): - return ["test-runtime", "--parse", kwargs["dag_file_path"]] - + coordinator = _StubCoordinator(parse_cmd=["test-runtime", "--parse"]) info = BaseCoordinator.DagParsingInfo( dag_file_path="/dag.test", bundle_name="test-bundle", @@ -432,26 +409,20 @@ def dag_parsing_cmd(cls, **kwargs): patch("socket.socketpair", return_value=(child_stderr, read_stderr)), patch("airflow.sdk.execution_time.coordinator.socket.socket", return_value=supervisor_comm), ): - TestCoordinator._runtime_subprocess_entrypoint(info) + coordinator._runtime_subprocess_entrypoint(info) - # Subprocess spawned mock_popen.assert_called_once() cmd = mock_popen.call_args[0][0] assert cmd == ["test-runtime", "--parse", "/dag.test"] - # Servers accepted and closed comm_server.accept.assert_called_once() logs_server.accept.assert_called_once() comm_server.close.assert_called_once() logs_server.close.assert_called_once() - # stderr child side closed after Popen child_stderr.close.assert_called_once() - - # _send_startup_details NOT called for dag parsing mock_send_startup.assert_not_called() - # _bridge called with the supervisor_comm socket mock_bridge.assert_called_once() assert mock_bridge.call_args[0][0] is supervisor_comm @@ -472,8 +443,6 @@ def test_task_execution_flow( mock_send_startup, mock_bridge, ): - """Verify the task-execution entrypoint resolves bundle, sends startup details, and bridges.""" - # Mock servers comm_server = MagicMock(spec=socket.socket) comm_server.getsockname.return_value = ("127.0.0.1", 6000) logs_server = MagicMock(spec=socket.socket) @@ -489,12 +458,10 @@ def test_task_execution_flow( read_stderr = MagicMock(spec=socket.socket) child_stderr.fileno.return_value = 10 - # Mock resolved bundle mock_bundle_instance = MagicMock() mock_bundle_instance.path = Path("/resolved/bundles/test-bundle") mock_resolve_bundle.return_value = mock_bundle_instance - # BundleVersionLock as context manager mock_lock_instance = MagicMock() mock_bundle_lock.return_value = mock_lock_instance mock_lock_instance.__enter__ = MagicMock(return_value=mock_lock_instance) @@ -506,14 +473,7 @@ def test_task_execution_flow( mock_bundle_info.version = "v1" mock_startup = MagicMock() - class TestCoordinator(BaseCoordinator): - sdk = "test" - file_extension = ".test" - - @classmethod - def task_execution_cmd(cls, **kwargs): - return ["test-runtime", "--execute", kwargs["dag_file_path"]] - + coordinator = _StubCoordinator(exec_cmd=["test-runtime", "--execute"]) info = BaseCoordinator.TaskExecutionInfo( what=mock_ti, dag_rel_path="dags/example.test", @@ -527,23 +487,16 @@ def task_execution_cmd(cls, **kwargs): patch("socket.socketpair", return_value=(child_stderr, read_stderr)), patch("airflow.sdk.execution_time.coordinator.socket.socket", return_value=supervisor_comm), ): - TestCoordinator._runtime_subprocess_entrypoint(info) + coordinator._runtime_subprocess_entrypoint(info) - # Bundle resolved mock_resolve_bundle.assert_called_once() - - # BundleVersionLock used mock_bundle_lock.assert_called_once_with(bundle_name="test-bundle", bundle_version="v1") - # Subprocess spawned with resolved path mock_popen.assert_called_once() cmd = mock_popen.call_args[0][0] assert cmd == ["test-runtime", "--execute", "/resolved/bundles/test-bundle/dags/example.test"] - # StartupDetails forwarded to the runtime subprocess mock_send_startup.assert_called_once_with(runtime_comm, mock_startup) - - # _bridge called mock_bridge.assert_called_once() @patch("airflow.sdk.execution_time.coordinator._bridge") @@ -551,7 +504,6 @@ def task_execution_cmd(cls, **kwargs): @patch("airflow.sdk.execution_time.coordinator._start_server") @patch("os.dup", return_value=99) def test_sets_process_context_env_var(self, mock_dup, mock_start_server, mock_popen, mock_bridge): - """Verify _AIRFLOW_PROCESS_CONTEXT is set to 'client'.""" comm_server = MagicMock(spec=socket.socket) comm_server.getsockname.return_value = ("127.0.0.1", 7000) logs_server = MagicMock(spec=socket.socket) @@ -567,14 +519,7 @@ def test_sets_process_context_env_var(self, mock_dup, mock_start_server, mock_po read_stderr = MagicMock(spec=socket.socket) child_stderr.fileno.return_value = 10 - class TestCoordinator(BaseCoordinator): - sdk = "test" - file_extension = ".test" - - @classmethod - def dag_parsing_cmd(cls, **kwargs): - return ["echo", "test"] - + coordinator = _StubCoordinator(parse_cmd=["echo", "test"]) info = BaseCoordinator.DagParsingInfo( dag_file_path="/dag.test", bundle_name="b", @@ -589,10 +534,117 @@ def dag_parsing_cmd(cls, **kwargs): patch("socket.socketpair", return_value=(child_stderr, read_stderr)), patch("airflow.sdk.execution_time.coordinator.socket.socket", return_value=supervisor_comm), ): - TestCoordinator._runtime_subprocess_entrypoint(info) + coordinator._runtime_subprocess_entrypoint(info) assert os.environ["_AIRFLOW_PROCESS_CONTEXT"] == "client" finally: if old_val is None: os.environ.pop("_AIRFLOW_PROCESS_CONTEXT", None) else: os.environ["_AIRFLOW_PROCESS_CONTEXT"] = old_val + + +class _CoordinatorA(BaseCoordinator): + sdk = "a" + file_extension = ".a" + + def __init__(self, *, label: str = "a"): + self.label = label + + def can_handle_dag_file(self, bundle_name, path): + return os.fspath(path).endswith(".a") + + +class _CoordinatorB(BaseCoordinator): + sdk = "b" + file_extension = ".b" + + def can_handle_dag_file(self, bundle_name, path): + return os.fspath(path).endswith(".b") + + +class TestCoordinatorManager: + @pytest.fixture(autouse=True) + def _reset_cache(self): + reset_coordinator_manager() + yield + reset_coordinator_manager() + + def test_from_config_loads_instances(self, monkeypatch): + coordinators_json = json.dumps( + [ + { + "name": "alpha", + "classpath": f"{_CoordinatorA.__module__}._CoordinatorA", + "kwargs": {"label": "alpha-label"}, + }, + { + "name": "beta", + "classpath": f"{_CoordinatorB.__module__}._CoordinatorB", + }, + ] + ) + queue_json = json.dumps({"queue-a": "alpha"}) + + monkeypatch.setenv("AIRFLOW__SDK__COORDINATORS", coordinators_json) + monkeypatch.setenv("AIRFLOW__SDK__QUEUE_TO_COORDINATOR", queue_json) + + from airflow.sdk.configuration import conf + + conf.invalidate_cache() + + manager = CoordinatorManager.from_config() + + alpha = manager.get("alpha") + beta = manager.get("beta") + assert isinstance(alpha, _CoordinatorA) + assert isinstance(beta, _CoordinatorB) + assert alpha.label == "alpha-label" + assert {type(c) for c in manager.all()} == {_CoordinatorA, _CoordinatorB} + + def test_from_config_empty(self, monkeypatch): + monkeypatch.delenv("AIRFLOW__SDK__COORDINATORS", raising=False) + monkeypatch.delenv("AIRFLOW__SDK__QUEUE_TO_COORDINATOR", raising=False) + + from airflow.sdk.configuration import conf + + conf.invalidate_cache() + + manager = CoordinatorManager.from_config() + assert manager.all() == [] + assert manager.get("missing") is None + + def test_for_queue_resolves_via_mapping(self): + coordinator_a = _CoordinatorA() + coordinator_b = _CoordinatorB() + manager = CoordinatorManager( + {"alpha": coordinator_a, "beta": coordinator_b}, + {"queue-a": "alpha", "queue-b": "beta"}, + ) + + assert manager.for_queue("queue-a") is coordinator_a + assert manager.for_queue("queue-b") is coordinator_b + assert manager.for_queue("queue-missing") is None + + def test_for_dag_file_picks_first_match(self): + coordinator_a = _CoordinatorA() + coordinator_b = _CoordinatorB() + manager = CoordinatorManager({"alpha": coordinator_a, "beta": coordinator_b}, {}) + + assert manager.for_dag_file("bundle", "dag.a") is coordinator_a + assert manager.for_dag_file("bundle", "dag.b") is coordinator_b + assert manager.for_dag_file("bundle", "dag.py") is None + + def test_file_extensions(self): + manager = CoordinatorManager({"a": _CoordinatorA(), "b": _CoordinatorB()}, {}) + assert set(manager.file_extensions()) == {".a", ".b"} + + def test_get_coordinator_manager_is_cached(self, monkeypatch): + monkeypatch.delenv("AIRFLOW__SDK__COORDINATORS", raising=False) + + from airflow.sdk.configuration import conf + + conf.invalidate_cache() + + m1 = get_coordinator_manager() + m2 = get_coordinator_manager() + assert m1 is m2 diff --git a/task-sdk/tests/task_sdk/test_providers_manager_runtime.py b/task-sdk/tests/task_sdk/test_providers_manager_runtime.py index 6e775f790be89..1cae21d53c764 100644 --- a/task-sdk/tests/task_sdk/test_providers_manager_runtime.py +++ b/task-sdk/tests/task_sdk/test_providers_manager_runtime.py @@ -243,33 +243,6 @@ def test_already_initialized_provider_configs_emits_deprecation_warning(self): with pytest.warns(DeprecationWarning, match="already_initialized_provider_configs.*deprecated"): pm.already_initialized_provider_configs - @patch("airflow.sdk.providers_manager_runtime.import_string") - def test_coordinators(self, mock_import_string): - class ACoordinator: - pass - - class ZCoordinator: - pass - - mock_import_string.side_effect = lambda path: { - "airflow.providers.sdk.java.coordinator.ACoordinator": ACoordinator, - "airflow.providers.sdk.java.coordinator.ZCoordinator": ZCoordinator, - }[path] - providers_manager = ProvidersManagerTaskRuntime() - providers_manager._provider_dict["apache-airflow-providers-sdk-java"] = ProviderInfo( - version="0.0.1", - data={ - "coordinators": [ - "airflow.providers.sdk.java.coordinator.ZCoordinator", - "airflow.providers.sdk.java.coordinator.ACoordinator", - "airflow.providers.sdk.java.coordinator.ZCoordinator", - ] - }, - ) - - with patch.object(providers_manager, "initialize_providers_list"): - assert providers_manager.coordinators == [ACoordinator, ZCoordinator] - def test_initialize_provider_configs_can_reload_sdk_conf(self): from airflow.sdk.configuration import conf diff --git a/uv.lock b/uv.lock index bca09add09083..efed773ad2c32 100644 --- a/uv.lock +++ b/uv.lock @@ -158,6 +158,7 @@ apache-aurflow-docker-stack = false members = [ "apache-airflow", "apache-airflow-breeze", + "apache-airflow-coordinators-java", "apache-airflow-core", "apache-airflow-ctl", "apache-airflow-ctl-tests", @@ -1890,6 +1891,39 @@ requires-dist = [ { name = "twine", specifier = ">=4.0.2" }, ] +[[package]] +name = "apache-airflow-coordinators-java" +version = "0.1.0" +source = { editable = "sdk/coordinators/java" } +dependencies = [ + { name = "apache-airflow-task-sdk" }, + { name = "pyyaml" }, +] + +[package.dev-dependencies] +dev = [ + { name = "apache-airflow" }, + { name = "apache-airflow-devel-common" }, + { name = "apache-airflow-task-sdk" }, +] +docs = [ + { name = "apache-airflow-devel-common", extra = ["docs"] }, +] + +[package.metadata] +requires-dist = [ + { name = "apache-airflow-task-sdk", editable = "task-sdk" }, + { name = "pyyaml", specifier = ">=6.0.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "apache-airflow", editable = "." }, + { name = "apache-airflow-devel-common", editable = "devel-common" }, + { name = "apache-airflow-task-sdk", editable = "task-sdk" }, +] +docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "devel-common" }] + [[package]] name = "apache-airflow-core" version = "3.3.0" From 14c7dc2c404356788e961efcd3d938e6dc97f81c Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 7 May 2026 15:50:50 +0800 Subject: [PATCH 49/75] Enhance documentation for BaseCoordinator lifecycle methods and IPC mechanisms --- .../airflow/sdk/execution_time/coordinator.py | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index fb842b0929f84..7e8e0f685a349 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -24,16 +24,17 @@ and :class:`CoordinatorManager`, the registry that loads coordinator instances from the ``[sdk] coordinators`` configuration. -The coordinator's :meth:`~BaseCoordinator.run_dag_parsing` method -handles the full lifecycle: +The coordinator's :meth:`~BaseCoordinator.run_dag_parsing` and +:meth:`~BaseCoordinator.run_task_execution` methods handle the full lifecycle: -1. Creates TCP servers for comm and logs channels. -2. Calls :meth:`~BaseCoordinator.dag_parsing_cmd` (provided - by the subclass) to obtain the subprocess command. +1. Creates TCP servers for comm and logs channels, and a socketpair for stderr. +2. Calls :meth:`~BaseCoordinator.dag_parsing_cmd` or + :meth:`~BaseCoordinator.task_execution_cmd` (provided by the subclass) to + obtain the subprocess command. 3. Spawns the subprocess and accepts TCP connections from it. 4. Runs a selector-based bridge that transparently forwards bytes between fd 0 (supervisor) and the subprocess comm socket, and - re-emits the subprocess's log output through structlog. + re-emits the subprocess's log and stderr output through structlog. I/O multiplexing uses the same selector-based loop as :class:`~airflow.sdk.execution_time.supervisor.WatchedSubprocess`, @@ -324,7 +325,20 @@ def _runtime_subprocess_entrypoint(self, entrypoint_info: DagParsingInfo | TaskE captured via a socketpair. 4. Runs the selector-based bridge until the subprocess exits. - fd layout (set up by ``_reopen_std_io_handles`` before this runs): + Two distinct IPC mechanisms are used because each channel has a + different initiator: + + - The runtime subprocess actively *connects* to the comm and logs + TCP servers using ``host:port`` strings passed via the command line + -- portable across every language's stdlib socket API. + - stderr is *inherited*: the subprocess writes to fd 2 transparently + (its native logging framework targets stderr by default), so we + replace fd 2 with one end of a socketpair instead of teaching the + runtime about an address. ``subprocess.PIPE`` would not work + because :func:`make_buffered_socket_reader` requires a real socket. + + fd layout of *this* coordinator process (set up by + ``_reopen_std_io_handles`` before this runs): - fd 0 -- bidirectional comms socket to the supervisor (``DagFileParseRequest`` <-> ``DagFileParsingResult``, @@ -333,6 +347,10 @@ def _runtime_subprocess_entrypoint(self, entrypoint_info: DagParsingInfo | TaskE - fd 2 -- stderr socket to the supervisor - fd N -- structured JSON log channel (``log_fd``, configured by ``_configure_logs_over_json_channel`` -> structlog) + + The runtime subprocess gets ``stdin=DEVNULL``, inherits fd 1 (so its + stdout flows straight to the supervisor), and has its fd 2 replaced + by the coordinator-owned end of the stderr socketpair. """ os.environ["_AIRFLOW_PROCESS_CONTEXT"] = "client" From ce4f9f43e04599b8f35a8f4d915a0d93a73a09ad Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 7 May 2026 16:07:31 +0800 Subject: [PATCH 50/75] CI: Fix docs spellcheck and code-block indent for sdk coordinators example Add JavaCoordinator, jvm, openjdk, Xmx to the docs spelling wordlist so the rendered configurations-ref doesn't fail Sphinx spellcheck on the [sdk] coordinators example. Also indent multi-line example/default values by 8 spaces in the shared sections-and-options template so the rendered RST code-block keeps consistent indentation and doesn't break the field list. --- .../src/sphinx_exts/includes/sections-and-options.rst | 4 ++-- docs/spelling_wordlist.txt | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/devel-common/src/sphinx_exts/includes/sections-and-options.rst b/devel-common/src/sphinx_exts/includes/sections-and-options.rst index 59d65b4dc776f..b0d84a1bd8a5a 100644 --- a/devel-common/src/sphinx_exts/includes/sections-and-options.rst +++ b/devel-common/src/sphinx_exts/includes/sections-and-options.rst @@ -65,7 +65,7 @@ {% if default and "\n" in default %} .. code-block:: - {{ default }} + {{ default | indent(width=8) }} {% else %} ``{{ "''" if default == "" else default }}`` {% endif %} @@ -85,7 +85,7 @@ {% if "\n" in example %} .. code-block:: - {{ example | indent(8) }} + {{ example | indent(width=8) }} {% else %} ``{{ example }}`` {% endif %} diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 0c1b1fc373fca..869f06770b3cf 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -860,6 +860,7 @@ iTerm iterm itertools Jarek +JavaCoordinator javascript jaydebeapi Jdbc @@ -897,6 +898,7 @@ jsonl juli Jupyter jupyter +jvm jwks JWT jwt @@ -1131,6 +1133,7 @@ openai openapi openfaas OpenID +openjdk openlineage OpenSearch opensearch @@ -1863,6 +1866,7 @@ XComs Xiaodong xlarge xml +Xmx xpath XSS xyz From d9ba5af44b3d920ea2625dcf9219eaaea223dc77 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Thu, 14 May 2026 13:06:22 +0800 Subject: [PATCH 51/75] Remove pure dag parsing from coordinator (#1578) Reflecting current AIP-108 scope. We can add this later if we want to. --- .../src/airflow/dag_processing/manager.py | 19 +-- .../src/airflow/dag_processing/processor.py | 39 +---- airflow-core/src/airflow/models/dagcode.py | 8 - .../providers/standard/decorators/stub.py | 1 + .../sdk/coordinators/java/coordinator.py | 40 ----- .../coordinators/java/test_coordinator.py | 77 --------- .../airflow/sdk/execution_time/coordinator.py | 101 ++---------- .../execution_time/test_coordinator.py | 151 +----------------- 8 files changed, 16 insertions(+), 420 deletions(-) diff --git a/airflow-core/src/airflow/dag_processing/manager.py b/airflow-core/src/airflow/dag_processing/manager.py index 1283e313a5ad3..ed5c61c604c79 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -276,9 +276,6 @@ class DagFileProcessorManager(LoggingMixin): factory=_config_get_factory("dag_processor", "file_parsing_sort_mode") ) - _runtime_file_extensions: tuple[str, ...] | None = attrs.field(default=None, init=False) - """File extensions registered by runtime coordinators (e.g. ".jar"). Lazily populated.""" - _api_server: InProcessExecutionAPI = attrs.field(init=False, factory=InProcessExecutionAPI) """API server to interact with Metadata DB""" @@ -868,16 +865,6 @@ def _find_files_in_bundle(self, bundle: BaseDagBundle) -> list[Path]: return rel_paths - def _get_runtime_file_extensions(self) -> tuple[str, ...]: - """Collect file extensions from configured runtime coordinators (cached after first call).""" - if self._runtime_file_extensions is not None: - return self._runtime_file_extensions - - from airflow.sdk.execution_time.coordinator import get_coordinator_manager - - self._runtime_file_extensions = get_coordinator_manager().file_extensions() - return self._runtime_file_extensions - def _get_observed_filelocs(self, present: set[DagFileInfo]) -> set[str]: """ Return observed DAG source paths for bundle entries. @@ -885,11 +872,7 @@ def _get_observed_filelocs(self, present: set[DagFileInfo]) -> set[str]: For regular files this includes the relative file path. For ZIP archives this includes DAG-like inner paths such as ``archive.zip/dag.py``. - - Runtime coordinator file extensions (e.g. ``.jar``) are treated as - opaque files rather than ZIP archives. """ - runtime_extensions = self._get_runtime_file_extensions() def find_zipped_dags(abs_path: os.PathLike) -> Iterator[str]: """Yield absolute paths for DAG-like files inside a ZIP archive.""" @@ -904,7 +887,7 @@ def find_zipped_dags(abs_path: os.PathLike) -> Iterator[str]: observed_filelocs: set[str] = set() for info in present: abs_path = str(info.absolute_path) - if abs_path.endswith((".py", *runtime_extensions)) or not zipfile.is_zipfile(abs_path): + if abs_path.endswith(".py") or not zipfile.is_zipfile(abs_path): observed_filelocs.add(str(info.rel_path)) else: if TYPE_CHECKING: diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index 2c0f80e9e3712..4974517ca3b90 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -17,7 +17,6 @@ from __future__ import annotations import contextlib -import functools import importlib import logging import os @@ -572,14 +571,7 @@ def start( # type: ignore[override] ) -> Self: logger = kwargs["logger"] - # Check if a configured runtime coordinator should handle this file - logger.debug("Checking for runtime coordinator entrypoint for file", path=path) - resolved_target = cls._resolve_processor_target(path, bundle_name, bundle_path, logger) - if resolved_target is not None: - target = resolved_target - logger.debug("Resolved runtime coordinator entrypoint for file", path=path) - else: - _pre_import_airflow_modules(os.fspath(path), logger) + _pre_import_airflow_modules(os.fspath(path), logger) proc: Self = super().start( target=target, @@ -592,35 +584,6 @@ def start( # type: ignore[override] proc._on_child_started(callbacks, path, bundle_path, bundle_name) return proc - @staticmethod - def _resolve_processor_target( - path: str | os.PathLike[str], - bundle_name: str, - bundle_path: Path, - log: FilteringBoundLogger, - ) -> Callable[[], None] | None: - """ - Return the entrypoint of the first runtime coordinator that can handle *path*. - - The returned callable is a ``functools.partial`` that binds *path*, *bundle_name* - and *bundle_path* so the supervisor can pass it as a no-arg ``target`` to - ``WatchedSubprocess.start``. - """ - from airflow.sdk.execution_time.coordinator import get_coordinator_manager - - coordinator = get_coordinator_manager().for_dag_file(bundle_name, path) - if coordinator is None: - log.debug("No runtime coordinator found for file %s, using default processor", path) - return None - - log.debug("Using runtime coordinator %s for file %s", type(coordinator).__qualname__, path) - return functools.partial( - coordinator.run_dag_parsing, - path=os.fspath(path), - bundle_name=bundle_name, - bundle_path=os.fspath(bundle_path), - ) - def _on_child_started( self, callbacks: list[CallbackRequest], diff --git a/airflow-core/src/airflow/models/dagcode.py b/airflow-core/src/airflow/models/dagcode.py index cdec5baa95717..60ee91c8b59b5 100644 --- a/airflow-core/src/airflow/models/dagcode.py +++ b/airflow-core/src/airflow/models/dagcode.py @@ -119,14 +119,6 @@ def code(cls, dag_id, session: Session = NEW_SESSION) -> str: @staticmethod def get_code_from_file(fileloc): - # Try from runtime coordinator first. - from airflow.sdk.execution_time.coordinator import get_coordinator_manager - - coordinator = get_coordinator_manager().for_dag_file("", fileloc) - if coordinator is not None: - return coordinator.get_code_from_file(fileloc) - - # Then fallback to python native try: with open_maybe_zipped(fileloc, "r") as f: code = f.read() diff --git a/providers/standard/src/airflow/providers/standard/decorators/stub.py b/providers/standard/src/airflow/providers/standard/decorators/stub.py index a5e63d925f795..f29d123c740c1 100644 --- a/providers/standard/src/airflow/providers/standard/decorators/stub.py +++ b/providers/standard/src/airflow/providers/standard/decorators/stub.py @@ -85,6 +85,7 @@ def stub( Stub tasks exist in the Dag graph only, but the execution must happen in an external environment via the Task Execution Interface. + """ return task_decorator_factory( decorated_operator_class=_StubOperator, diff --git a/sdk/coordinators/java/src/airflow/sdk/coordinators/java/coordinator.py b/sdk/coordinators/java/src/airflow/sdk/coordinators/java/coordinator.py index 4f93fcc93fd88..3e8a2a2c4389e 100644 --- a/sdk/coordinators/java/src/airflow/sdk/coordinators/java/coordinator.py +++ b/sdk/coordinators/java/src/airflow/sdk/coordinators/java/coordinator.py @@ -19,9 +19,6 @@ from __future__ import annotations -import contextlib -import os -import zipfile from pathlib import Path from typing import TYPE_CHECKING @@ -58,9 +55,6 @@ class JavaCoordinator(BaseCoordinator): flow; unused for pure-Java DAGs. """ - sdk = "java" - file_extension = ".jar" - def __init__( self, *, @@ -72,14 +66,6 @@ def __init__( self.jvm_args = list(jvm_args) if jvm_args else [] self.bundles_folder = bundles_folder - def can_handle_dag_file(self, bundle_name: str, path: str | os.PathLike[str]) -> bool: - """Return ``True`` when *path* is a JAR with valid Airflow Java SDK manifest attributes.""" - if not os.fspath(path).endswith(self.file_extension): - return False - with contextlib.suppress(FileNotFoundError, NotADirectoryError, zipfile.BadZipFile, KeyError): - return BundleScanner.resolve_jar(Path(path)) is not None - return False - def get_code_from_file(self, fileloc: str) -> str: """Read embedded DAG source code from a JAR bundle.""" code = read_dag_code(Path(fileloc)) @@ -87,32 +73,6 @@ def get_code_from_file(self, fileloc: str) -> str: raise FileNotFoundError(f"No DAG source code found in JAR: {fileloc}") return code - def dag_parsing_cmd( - self, - *, - dag_file_path: str, - bundle_name: str, - bundle_path: str, - comm_addr: str, - logs_addr: str, - ) -> list[str]: - """Build the ``java`` command for parsing a JAR bundle.""" - jar_path = Path(dag_file_path) - # Java bundles are typically thin JARs: the main JAR only contains - # the bundle's own classes while its dependencies (the Airflow Java - # SDK, logging libraries, etc.) are separate JARs that live alongside - # it. Using ``/*`` lets the JVM load every JAR in the directory. - classpath = f"{bundle_path}/*" - return [ - self.java_executable, - *self.jvm_args, - "-classpath", - classpath, - BundleScanner.resolve_jar(jar_path), - f"--comm={comm_addr}", - f"--logs={logs_addr}", - ] - def task_execution_cmd( self, *, diff --git a/sdk/coordinators/java/tests/unit/coordinators/java/test_coordinator.py b/sdk/coordinators/java/tests/unit/coordinators/java/test_coordinator.py index 3a101f30cba88..f9f63439d84c3 100644 --- a/sdk/coordinators/java/tests/unit/coordinators/java/test_coordinator.py +++ b/sdk/coordinators/java/tests/unit/coordinators/java/test_coordinator.py @@ -95,12 +95,6 @@ def _make_ti(dag_id: str = "test_dag") -> TaskInstanceDTO: class TestJavaCoordinatorAttributes: - def test_sdk(self): - assert JavaCoordinator.sdk == "java" - - def test_file_extension(self): - assert JavaCoordinator.file_extension == ".jar" - def test_default_kwargs(self): coordinator = JavaCoordinator() assert coordinator.java_executable == "java" @@ -118,31 +112,6 @@ def test_custom_kwargs(self): assert coordinator.bundles_folder == "/airflow/java-bundles" -class TestCanHandleDagFile: - def test_valid_jar_returns_true(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "valid.jar", dag_ids=["d"]) - assert JavaCoordinator().can_handle_dag_file("bundle", str(jar)) is True - - def test_non_jar_file_returns_false(self, tmp_path: Path): - py_file = tmp_path / "dag.py" - py_file.write_text("from airflow import DAG") - assert JavaCoordinator().can_handle_dag_file("bundle", str(py_file)) is False - - def test_missing_file_returns_false(self, tmp_path: Path): - assert JavaCoordinator().can_handle_dag_file("bundle", str(tmp_path / "missing.jar")) is False - - def test_bad_zip_returns_false(self, tmp_path: Path): - bad = tmp_path / "bad.jar" - bad.write_text("not a zip") - assert JavaCoordinator().can_handle_dag_file("bundle", str(bad)) is False - - def test_jar_without_sdk_manifest_returns_false(self, tmp_path: Path): - jar = tmp_path / "plain.jar" - with zipfile.ZipFile(jar, "w") as zf: - zf.writestr("placeholder.class", b"") - assert JavaCoordinator().can_handle_dag_file("bundle", str(jar)) is False - - class TestGetCodeFromFile: def test_returns_embedded_code(self, tmp_path: Path): code = "from airflow import DAG\ndag = DAG('my_dag')" @@ -155,52 +124,6 @@ def test_raises_when_no_code(self, tmp_path: Path): JavaCoordinator().get_code_from_file(str(jar)) -class TestDagParsingCmd: - def test_builds_default_java_command(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "app.jar", dag_ids=["d"]) - bundle_path = str(tmp_path) - cmd = JavaCoordinator().dag_parsing_cmd( - dag_file_path=str(jar), - bundle_name="my_bundle", - bundle_path=bundle_path, - comm_addr="localhost:1234", - logs_addr="localhost:5678", - ) - assert cmd == [ - "java", - "-classpath", - f"{bundle_path}/*", - TEST_MAIN_CLASS, - "--comm=localhost:1234", - "--logs=localhost:5678", - ] - - def test_uses_custom_executable_and_jvm_args(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "app.jar", dag_ids=["d"]) - bundle_path = str(tmp_path) - coordinator = JavaCoordinator( - java_executable="/opt/jdk-17/bin/java", - jvm_args=["-Xmx1024m", "-Xms256m"], - ) - cmd = coordinator.dag_parsing_cmd( - dag_file_path=str(jar), - bundle_name="my_bundle", - bundle_path=bundle_path, - comm_addr="localhost:1234", - logs_addr="localhost:5678", - ) - assert cmd == [ - "/opt/jdk-17/bin/java", - "-Xmx1024m", - "-Xms256m", - "-classpath", - f"{bundle_path}/*", - TEST_MAIN_CLASS, - "--comm=localhost:1234", - "--logs=localhost:5678", - ] - - class TestTaskExecutionCmd: def test_pure_java_dag(self, tmp_path: Path): jar = _create_bundle_jar(tmp_path / "app.jar", dag_ids=["test_dag"]) diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 7e8e0f685a349..3857ef3ed95f5 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -24,13 +24,12 @@ and :class:`CoordinatorManager`, the registry that loads coordinator instances from the ``[sdk] coordinators`` configuration. -The coordinator's :meth:`~BaseCoordinator.run_dag_parsing` and -:meth:`~BaseCoordinator.run_task_execution` methods handle the full lifecycle: +The coordinator's :meth:`~BaseCoordinator.run_task_execution` handles the full +lifecycle: 1. Creates TCP servers for comm and logs channels, and a socketpair for stderr. -2. Calls :meth:`~BaseCoordinator.dag_parsing_cmd` or - :meth:`~BaseCoordinator.task_execution_cmd` (provided by the subclass) to - obtain the subprocess command. +2. Calls :meth:`~BaseCoordinator.task_execution_cmd` (provided by the subclass) + to obtain the subprocess command. 3. Spawns the subprocess and accepts TCP connections from it. 4. Runs a selector-based bridge that transparently forwards bytes between fd 0 (supervisor) and the subprocess comm socket, and @@ -50,9 +49,9 @@ import socket import subprocess import time -from typing import TYPE_CHECKING, ClassVar, NamedTuple +from typing import TYPE_CHECKING, NamedTuple -from airflow.sdk._shared.module_loading import import_string +from airflow.sdk._shared.module_loading import import_string, qualname if TYPE_CHECKING: from structlog.typing import FilteringBoundLogger @@ -183,15 +182,11 @@ class BaseCoordinator: constructed with the entry's ``kwargs``. Subclasses represent a specific SDK runtime (Java, Go, etc.) and only - need to implement :meth:`can_handle_dag_file`, :meth:`dag_parsing_cmd` - and :meth:`task_execution_cmd`. The base class owns the entire bridge - lifecycle: TCP servers, subprocess management, selector-based I/O loop, - and cleanup. + need to implement :meth:`task_execution_cmd`. The class owns the entire + bridge lifecycle: TCP servers, subprocess management, selector-based I/O + loop, and cleanup. """ - sdk: ClassVar[str] - file_extension: ClassVar[str] - class DagParsingInfo(NamedTuple): """Information needed for runtime Dag parsing.""" @@ -209,18 +204,6 @@ class TaskExecutionInfo(NamedTuple): startup_details: StartupDetails mode: str = "task-execution" - def can_handle_dag_file(self, bundle_name: str, path: str | os.PathLike[str]) -> bool: - """ - Return ``True`` if this coordinator should handle DAG-file parsing for *path*. - - Called by :meth:`DagFileProcessorProcess._resolve_processor_target` to - decide whether to delegate parsing to this coordinator's - :meth:`run_dag_parsing` instead of the default Python entrypoint. - - The default implementation returns ``False``; subclasses must override. - """ - return False - def get_code_from_file(self, fileloc: str) -> str: """ Return the human-readable source code for a DAG file managed by this coordinator. @@ -237,29 +220,6 @@ def get_code_from_file(self, fileloc: str) -> str: """ raise NotImplementedError - def dag_parsing_cmd( - self, - *, - dag_file_path: str, - bundle_name: str, - bundle_path: str, - comm_addr: str, - logs_addr: str, - ) -> list[str]: - """ - Return the subprocess command for DAG file parsing. - - :param dag_file_path: Absolute path to the DAG file to parse. - :param bundle_name: Name of the DAG bundle. - :param bundle_path: Root path of the DAG bundle. - :param comm_addr: ``host:port`` the subprocess must connect to - for the bidirectional msgpack comm channel. - :param logs_addr: ``host:port`` the subprocess must connect to - for the structured JSON log channel. - :returns: Full command list (e.g. ``["java", "-cp", "...", ...]`` based on each runtime). - """ - raise NotImplementedError - def task_execution_cmd( self, *, @@ -285,16 +245,6 @@ def task_execution_cmd( """ raise NotImplementedError - def run_dag_parsing(self, *, path: str, bundle_name: str, bundle_path: str) -> None: - """Entry point for running runtime-specific Dag File Processing.""" - self._runtime_subprocess_entrypoint( - self.DagParsingInfo( - dag_file_path=path, - bundle_name=bundle_name, - bundle_path=bundle_path, - ) - ) - def run_task_execution( self, *, @@ -320,7 +270,7 @@ def _runtime_subprocess_entrypoint(self, entrypoint_info: DagParsingInfo | TaskE bidirectional comms socket to the supervisor. The method: 1. Creates TCP servers for comm and logs. - 2. Calls :meth:`dag_parsing_cmd` or :meth:`task_execution_cmd` to get the command. + 2. Calls :meth:`task_execution_cmd` to get the command. 3. Spawns the subprocess with ``stdin=/dev/null`` and stderr captured via a socketpair. 4. Runs the selector-based bridge until the subprocess exits. @@ -359,7 +309,7 @@ def _runtime_subprocess_entrypoint(self, entrypoint_info: DagParsingInfo | TaskE log = structlog.get_logger(logger_name="task") log.info( "Starting runtime subprocess", - sdk=self.sdk, + type=qualname(self), mode=entrypoint_info.mode, ) @@ -381,15 +331,7 @@ def _runtime_subprocess_entrypoint(self, entrypoint_info: DagParsingInfo | TaskE # garbage-collected while the runtime process is still running. bundle_version_lock: contextlib.AbstractContextManager = contextlib.nullcontext() - if isinstance(entrypoint_info, self.DagParsingInfo): - cmd = self.dag_parsing_cmd( - dag_file_path=entrypoint_info.dag_file_path, - bundle_name=entrypoint_info.bundle_name, - bundle_path=entrypoint_info.bundle_path, - comm_addr=comm_addr, - logs_addr=logs_addr, - ) - elif isinstance(entrypoint_info, self.TaskExecutionInfo): + if isinstance(entrypoint_info, self.TaskExecutionInfo): from airflow.dag_processing.bundles.base import BundleVersionLock from airflow.sdk.execution_time.task_runner import resolve_bundle @@ -513,25 +455,6 @@ def for_queue(self, queue: str) -> BaseCoordinator | None: return None return self._instances_by_name.get(name) - def for_dag_file(self, bundle_name: str, path: str | os.PathLike[str]) -> BaseCoordinator | None: - """Return the first coordinator whose ``can_handle_dag_file`` matches *path*.""" - for instance in self.all(): - try: - if instance.can_handle_dag_file(bundle_name, path): - return instance - except Exception: - continue - return None - - def file_extensions(self) -> tuple[str, ...]: - """Return the file extensions registered by all loaded coordinators.""" - extensions: list[str] = [] - for instance in self.all(): - ext = getattr(type(instance), "file_extension", None) - if ext: - extensions.append(ext) - return tuple(extensions) - @functools.cache def get_coordinator_manager() -> CoordinatorManager: diff --git a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py index 7c11022755d9f..507140c98f9cf 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py +++ b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py @@ -153,23 +153,10 @@ def test_real_socket_roundtrip(self): class TestBaseCoordinatorDefaults: - def test_can_handle_dag_file_returns_false(self): - assert BaseCoordinator().can_handle_dag_file("bundle", "/path/to/dag.py") is False - def test_get_code_from_file_raises_not_implemented(self): with pytest.raises(NotImplementedError): BaseCoordinator().get_code_from_file("/path/to/dag.jar") - def test_dag_parsing_cmd_raises_not_implemented(self): - with pytest.raises(NotImplementedError): - BaseCoordinator().dag_parsing_cmd( - dag_file_path="/dag.jar", - bundle_name="b", - bundle_path="/path", - comm_addr="127.0.0.1:1234", - logs_addr="127.0.0.1:1235", - ) - def test_task_execution_cmd_raises_not_implemented(self): with pytest.raises(NotImplementedError): BaseCoordinator().task_execution_cmd( @@ -300,36 +287,13 @@ class _StubCoordinator(BaseCoordinator): sdk = "test" file_extension = ".test" - def __init__(self, *, parse_cmd: list[str] | None = None, exec_cmd: list[str] | None = None): - self._parse_cmd = parse_cmd or ["test-runtime", "--parse"] + def __init__(self, *, exec_cmd: list[str] | None = None): self._exec_cmd = exec_cmd or ["test-runtime", "--execute"] - def dag_parsing_cmd(self, *, dag_file_path, **_): - return [*self._parse_cmd, dag_file_path] - def task_execution_cmd(self, *, dag_file_path, **_): return [*self._exec_cmd, dag_file_path] -class TestRunDagParsing: - @patch.object(BaseCoordinator, "_runtime_subprocess_entrypoint") - def test_run_dag_parsing_creates_dag_parsing_info(self, mock_entrypoint): - coordinator = _StubCoordinator() - coordinator.run_dag_parsing( - path="/bundles/my-bundle/dags/example.jar", - bundle_name="my-bundle", - bundle_path="/bundles/my-bundle", - ) - - mock_entrypoint.assert_called_once() - info = mock_entrypoint.call_args[0][0] - assert isinstance(info, BaseCoordinator.DagParsingInfo) - assert info.dag_file_path == "/bundles/my-bundle/dags/example.jar" - assert info.bundle_name == "my-bundle" - assert info.bundle_path == "/bundles/my-bundle" - assert info.mode == "dag-parsing" - - class TestRunTaskExecution: @patch.object(BaseCoordinator, "_runtime_subprocess_entrypoint") def test_run_task_execution_creates_task_execution_info(self, mock_entrypoint): @@ -375,57 +339,6 @@ def test_unknown_entrypoint_info_type_raises(self): with pytest.raises(ValueError, match="Unknown entrypoint_info type"): coordinator._runtime_subprocess_entrypoint(fake_info) # type: ignore[arg-type] - @patch("airflow.sdk.execution_time.coordinator._bridge") - @patch("airflow.sdk.execution_time.coordinator._send_startup_details") - @patch("subprocess.Popen", autospec=True) - @patch("airflow.sdk.execution_time.coordinator._start_server") - @patch("os.dup", return_value=99) - def test_dag_parsing_flow(self, mock_dup, mock_start_server, mock_popen, mock_send_startup, mock_bridge): - comm_server = MagicMock(spec=socket.socket) - comm_server.getsockname.return_value = ("127.0.0.1", 5000) - logs_server = MagicMock(spec=socket.socket) - logs_server.getsockname.return_value = ("127.0.0.1", 5001) - mock_start_server.side_effect = [comm_server, logs_server] - - runtime_comm = MagicMock(spec=socket.socket) - runtime_logs = MagicMock(spec=socket.socket) - comm_server.accept.return_value = (runtime_comm, ("127.0.0.1", 9000)) - logs_server.accept.return_value = (runtime_logs, ("127.0.0.1", 9001)) - - child_stderr = MagicMock(spec=socket.socket) - read_stderr = MagicMock(spec=socket.socket) - child_stderr.fileno.return_value = 10 - - supervisor_comm = MagicMock(spec=socket.socket) - - coordinator = _StubCoordinator(parse_cmd=["test-runtime", "--parse"]) - info = BaseCoordinator.DagParsingInfo( - dag_file_path="/dag.test", - bundle_name="test-bundle", - bundle_path="/bundles/test-bundle", - ) - - with ( - patch("socket.socketpair", return_value=(child_stderr, read_stderr)), - patch("airflow.sdk.execution_time.coordinator.socket.socket", return_value=supervisor_comm), - ): - coordinator._runtime_subprocess_entrypoint(info) - - mock_popen.assert_called_once() - cmd = mock_popen.call_args[0][0] - assert cmd == ["test-runtime", "--parse", "/dag.test"] - - comm_server.accept.assert_called_once() - logs_server.accept.assert_called_once() - comm_server.close.assert_called_once() - logs_server.close.assert_called_once() - - child_stderr.close.assert_called_once() - mock_send_startup.assert_not_called() - - mock_bridge.assert_called_once() - assert mock_bridge.call_args[0][0] is supervisor_comm - @patch("airflow.sdk.execution_time.coordinator._bridge") @patch("airflow.sdk.execution_time.coordinator._send_startup_details") @patch("subprocess.Popen", autospec=True) @@ -499,49 +412,6 @@ def test_task_execution_flow( mock_send_startup.assert_called_once_with(runtime_comm, mock_startup) mock_bridge.assert_called_once() - @patch("airflow.sdk.execution_time.coordinator._bridge") - @patch("subprocess.Popen", autospec=True) - @patch("airflow.sdk.execution_time.coordinator._start_server") - @patch("os.dup", return_value=99) - def test_sets_process_context_env_var(self, mock_dup, mock_start_server, mock_popen, mock_bridge): - comm_server = MagicMock(spec=socket.socket) - comm_server.getsockname.return_value = ("127.0.0.1", 7000) - logs_server = MagicMock(spec=socket.socket) - logs_server.getsockname.return_value = ("127.0.0.1", 7001) - mock_start_server.side_effect = [comm_server, logs_server] - - runtime_comm = MagicMock(spec=socket.socket) - runtime_logs = MagicMock(spec=socket.socket) - comm_server.accept.return_value = (runtime_comm, ("127.0.0.1", 9000)) - logs_server.accept.return_value = (runtime_logs, ("127.0.0.1", 9001)) - - child_stderr = MagicMock(spec=socket.socket) - read_stderr = MagicMock(spec=socket.socket) - child_stderr.fileno.return_value = 10 - - coordinator = _StubCoordinator(parse_cmd=["echo", "test"]) - info = BaseCoordinator.DagParsingInfo( - dag_file_path="/dag.test", - bundle_name="b", - bundle_path="/path", - ) - - supervisor_comm = MagicMock(spec=socket.socket) - - old_val = os.environ.get("_AIRFLOW_PROCESS_CONTEXT") - try: - with ( - patch("socket.socketpair", return_value=(child_stderr, read_stderr)), - patch("airflow.sdk.execution_time.coordinator.socket.socket", return_value=supervisor_comm), - ): - coordinator._runtime_subprocess_entrypoint(info) - assert os.environ["_AIRFLOW_PROCESS_CONTEXT"] == "client" - finally: - if old_val is None: - os.environ.pop("_AIRFLOW_PROCESS_CONTEXT", None) - else: - os.environ["_AIRFLOW_PROCESS_CONTEXT"] = old_val - class _CoordinatorA(BaseCoordinator): sdk = "a" @@ -550,17 +420,11 @@ class _CoordinatorA(BaseCoordinator): def __init__(self, *, label: str = "a"): self.label = label - def can_handle_dag_file(self, bundle_name, path): - return os.fspath(path).endswith(".a") - class _CoordinatorB(BaseCoordinator): sdk = "b" file_extension = ".b" - def can_handle_dag_file(self, bundle_name, path): - return os.fspath(path).endswith(".b") - class TestCoordinatorManager: @pytest.fixture(autouse=True) @@ -625,19 +489,6 @@ def test_for_queue_resolves_via_mapping(self): assert manager.for_queue("queue-b") is coordinator_b assert manager.for_queue("queue-missing") is None - def test_for_dag_file_picks_first_match(self): - coordinator_a = _CoordinatorA() - coordinator_b = _CoordinatorB() - manager = CoordinatorManager({"alpha": coordinator_a, "beta": coordinator_b}, {}) - - assert manager.for_dag_file("bundle", "dag.a") is coordinator_a - assert manager.for_dag_file("bundle", "dag.b") is coordinator_b - assert manager.for_dag_file("bundle", "dag.py") is None - - def test_file_extensions(self): - manager = CoordinatorManager({"a": _CoordinatorA(), "b": _CoordinatorB()}, {}) - assert set(manager.file_extensions()) == {".a", ".b"} - def test_get_coordinator_manager_is_cached(self, monkeypatch): monkeypatch.delenv("AIRFLOW__SDK__COORDINATORS", raising=False) From 8de8ff89227ef30c2ece7f506818a2d6f6f70cf3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Fri, 15 May 2026 11:56:53 +0800 Subject: [PATCH 52/75] Move coordinator implementation back into SDK (#1580) The base class is still in execution_time since the interface needs to be known by the task runner. --- sdk/coordinators/java/.gitignore | 1 - sdk/coordinators/java/LICENSE | 201 ------------------ sdk/coordinators/java/NOTICE | 5 - sdk/coordinators/java/README.rst | 51 ----- sdk/coordinators/java/pyproject.toml | 108 ---------- .../java/tests/unit/coordinators/__init__.py | 16 -- .../tests/unit/coordinators/java/__init__.py | 16 -- .../airflow/sdk/coordinators/java/__init__.py | 0 .../sdk/coordinators/java/bundle_scanner.py | 0 .../sdk/coordinators/java/coordinator.py | 0 .../tests/task_sdk/coordinators}/__init__.py | 0 .../task_sdk/coordinators/java}/__init__.py | 0 .../coordinators/java/test_bundle_scanner.py | 0 .../coordinators/java/test_coordinator.py | 0 14 files changed, 398 deletions(-) delete mode 100644 sdk/coordinators/java/.gitignore delete mode 100644 sdk/coordinators/java/LICENSE delete mode 100644 sdk/coordinators/java/NOTICE delete mode 100644 sdk/coordinators/java/README.rst delete mode 100644 sdk/coordinators/java/pyproject.toml delete mode 100644 sdk/coordinators/java/tests/unit/coordinators/__init__.py delete mode 100644 sdk/coordinators/java/tests/unit/coordinators/java/__init__.py rename {sdk/coordinators/java => task-sdk}/src/airflow/sdk/coordinators/java/__init__.py (100%) rename {sdk/coordinators/java => task-sdk}/src/airflow/sdk/coordinators/java/bundle_scanner.py (100%) rename {sdk/coordinators/java => task-sdk}/src/airflow/sdk/coordinators/java/coordinator.py (100%) rename {sdk/coordinators/java/tests => task-sdk/tests/task_sdk/coordinators}/__init__.py (100%) rename {sdk/coordinators/java/tests/unit => task-sdk/tests/task_sdk/coordinators/java}/__init__.py (100%) rename {sdk/coordinators/java/tests/unit => task-sdk/tests/task_sdk}/coordinators/java/test_bundle_scanner.py (100%) rename {sdk/coordinators/java/tests/unit => task-sdk/tests/task_sdk}/coordinators/java/test_coordinator.py (100%) diff --git a/sdk/coordinators/java/.gitignore b/sdk/coordinators/java/.gitignore deleted file mode 100644 index bff2d7629604d..0000000000000 --- a/sdk/coordinators/java/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.iml diff --git a/sdk/coordinators/java/LICENSE b/sdk/coordinators/java/LICENSE deleted file mode 100644 index 11069edd79019..0000000000000 --- a/sdk/coordinators/java/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed 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. diff --git a/sdk/coordinators/java/NOTICE b/sdk/coordinators/java/NOTICE deleted file mode 100644 index a51bd9390d030..0000000000000 --- a/sdk/coordinators/java/NOTICE +++ /dev/null @@ -1,5 +0,0 @@ -Apache Airflow -Copyright 2016-2026 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). diff --git a/sdk/coordinators/java/README.rst b/sdk/coordinators/java/README.rst deleted file mode 100644 index 63f19caa412bb..0000000000000 --- a/sdk/coordinators/java/README.rst +++ /dev/null @@ -1,51 +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. - -Package ``apache-airflow-coordinators-java`` -=========================================== - -Java runtime coordinator for the Apache Airflow Task SDK. - -This distribution contributes the ``airflow.sdk.coordinators.java.JavaCoordinator`` -class, which spawns a JVM subprocess to parse Java DAG bundles (``.jar``) -and execute Java tasks. It is loaded via the ``[sdk] coordinators`` configuration -and is *not* a standard Airflow provider — it does not register hooks, operators, -or any other provider-managed resources. - -Configure it in ``airflow.cfg``:: - - [sdk] - coordinators = [ - { - "name": "jdk-17", - "classpath": "airflow.sdk.coordinators.java.JavaCoordinator", - "kwargs": { - "java_executable": "/usr/lib/jvm/java-17-openjdk/bin/java", - "jvm_args": ["-Xmx1024m"], - "bundles_folder": "~/airflow/java-bundles" - } - } - ] - queue_to_coordinator = {"java-queue": "jdk-17"} - -Installation ------------- - -:: - - pip install apache-airflow-coordinators-java diff --git a/sdk/coordinators/java/pyproject.toml b/sdk/coordinators/java/pyproject.toml deleted file mode 100644 index f4e0dd31f5284..0000000000000 --- a/sdk/coordinators/java/pyproject.toml +++ /dev/null @@ -1,108 +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. - -[build-system] -requires = ["hatchling==1.29.0"] -build-backend = "hatchling.build" - -[project] -name = "apache-airflow-coordinators-java" -version = "0.1.0" -description = "Java runtime coordinator for the Apache Airflow Task SDK" -readme = "README.rst" -license = "Apache-2.0" -license-files = ["LICENSE", "NOTICE"] -authors = [ - {name="Apache Software Foundation", email="dev@airflow.apache.org"}, -] -maintainers = [ - {name="Apache Software Foundation", email="dev@airflow.apache.org"}, -] -keywords = ["airflow", "coordinator", "java", "sdk"] -classifiers = [ - "Development Status :: 4 - Beta", - "Environment :: Console", - "Framework :: Apache Airflow", - "Intended Audience :: Developers", - "Intended Audience :: System Administrators", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: System :: Monitoring", -] -requires-python = ">=3.10,!=3.15" - -dependencies = [ - "apache-airflow-task-sdk>=1.3.0", - "PyYAML>=6.0.2", -] - -[dependency-groups] -dev = [ - "apache-airflow", - "apache-airflow-task-sdk", - "apache-airflow-devel-common", -] - -docs = [ - "apache-airflow-devel-common[docs]" -] - -[tool.uv.sources] -apache-airflow = {workspace = true} -apache-airflow-devel-common = {workspace = true} -apache-airflow-task-sdk = {workspace = true} - -[project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-coordinators-java/0.1.0" -"Bug Tracker" = "https://github.com/apache/airflow/issues" -"Source Code" = "https://github.com/apache/airflow" -"Slack Chat" = "https://s.apache.org/airflow-slack" - -[tool.hatch.build.targets.wheel] -packages = ["src/airflow"] -# Do not ship the airflow / airflow.sdk / airflow.sdk.coordinators package roots -# -- those are owned by airflow-core / task-sdk. This distribution only contributes -# the airflow.sdk.coordinators.java sub-package. -exclude = [ - "src/airflow/__init__.py", - "src/airflow/sdk/__init__.py", - "src/airflow/sdk/coordinators/__init__.py", -] - -[tool.hatch.build.targets.sdist] -include = [ - "src/airflow", - "docs", - "tests", - "LICENSE", - "NOTICE", - "README.rst", -] - -[tool.ruff] -extend = "../../../pyproject.toml" -src = ["src"] -namespace-packages = ["src/airflow"] - -[tool.ruff.lint.per-file-ignores] -# Ignore Doc rules et al for anything outside of tests -"!src/*" = ["D", "TID253", "S101", "TRY002"] -# Ignore the pytest rules outside the tests folder -"!tests/*" = ["PT"] diff --git a/sdk/coordinators/java/tests/unit/coordinators/__init__.py b/sdk/coordinators/java/tests/unit/coordinators/__init__.py deleted file mode 100644 index 13a83393a9124..0000000000000 --- a/sdk/coordinators/java/tests/unit/coordinators/__init__.py +++ /dev/null @@ -1,16 +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. diff --git a/sdk/coordinators/java/tests/unit/coordinators/java/__init__.py b/sdk/coordinators/java/tests/unit/coordinators/java/__init__.py deleted file mode 100644 index 13a83393a9124..0000000000000 --- a/sdk/coordinators/java/tests/unit/coordinators/java/__init__.py +++ /dev/null @@ -1,16 +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. diff --git a/sdk/coordinators/java/src/airflow/sdk/coordinators/java/__init__.py b/task-sdk/src/airflow/sdk/coordinators/java/__init__.py similarity index 100% rename from sdk/coordinators/java/src/airflow/sdk/coordinators/java/__init__.py rename to task-sdk/src/airflow/sdk/coordinators/java/__init__.py diff --git a/sdk/coordinators/java/src/airflow/sdk/coordinators/java/bundle_scanner.py b/task-sdk/src/airflow/sdk/coordinators/java/bundle_scanner.py similarity index 100% rename from sdk/coordinators/java/src/airflow/sdk/coordinators/java/bundle_scanner.py rename to task-sdk/src/airflow/sdk/coordinators/java/bundle_scanner.py diff --git a/sdk/coordinators/java/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py similarity index 100% rename from sdk/coordinators/java/src/airflow/sdk/coordinators/java/coordinator.py rename to task-sdk/src/airflow/sdk/coordinators/java/coordinator.py diff --git a/sdk/coordinators/java/tests/__init__.py b/task-sdk/tests/task_sdk/coordinators/__init__.py similarity index 100% rename from sdk/coordinators/java/tests/__init__.py rename to task-sdk/tests/task_sdk/coordinators/__init__.py diff --git a/sdk/coordinators/java/tests/unit/__init__.py b/task-sdk/tests/task_sdk/coordinators/java/__init__.py similarity index 100% rename from sdk/coordinators/java/tests/unit/__init__.py rename to task-sdk/tests/task_sdk/coordinators/java/__init__.py diff --git a/sdk/coordinators/java/tests/unit/coordinators/java/test_bundle_scanner.py b/task-sdk/tests/task_sdk/coordinators/java/test_bundle_scanner.py similarity index 100% rename from sdk/coordinators/java/tests/unit/coordinators/java/test_bundle_scanner.py rename to task-sdk/tests/task_sdk/coordinators/java/test_bundle_scanner.py diff --git a/sdk/coordinators/java/tests/unit/coordinators/java/test_coordinator.py b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py similarity index 100% rename from sdk/coordinators/java/tests/unit/coordinators/java/test_coordinator.py rename to task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py From 43581c9ede5cc5767b8c1d8ee25c2ba31e13799b Mon Sep 17 00:00:00 2001 From: "Jason(Zhe-You) Liu" <68415893+jason810496@users.noreply.github.com> Date: Mon, 18 May 2026 21:04:49 +0800 Subject: [PATCH 53/75] CI: Fix uv.lock by removing coordinator distribution (#1585) * CI: Fix uv.lock by removing coordinator distribution * Remove the remaining entry catch by codex bot --- pyproject.toml | 1 - scripts/ci/docker-compose/remove-sources.yml | 1 - scripts/ci/docker-compose/tests-sources.yml | 1 - task-sdk/src/airflow/sdk/__init__.py | 5 --- uv.lock | 38 ++------------------ 5 files changed, 2 insertions(+), 44 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b183ec7f6f408..a0202dca38377 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1841,7 +1841,6 @@ members = [ "chart", "kubernetes-tests", "task-sdk", - "sdk/coordinators/java", "providers-summary-docs", "docker-stack-docs", "shared/configuration", diff --git a/scripts/ci/docker-compose/remove-sources.yml b/scripts/ci/docker-compose/remove-sources.yml index cf78e9258f39f..a2f7d3a035766 100644 --- a/scripts/ci/docker-compose/remove-sources.yml +++ b/scripts/ci/docker-compose/remove-sources.yml @@ -107,7 +107,6 @@ services: - ../../../empty:/opt/airflow/providers/redis/src - ../../../empty:/opt/airflow/providers/salesforce/src - ../../../empty:/opt/airflow/providers/samba/src - - ../../../empty:/opt/airflow/sdk/coordinators/java/src - ../../../empty:/opt/airflow/providers/segment/src - ../../../empty:/opt/airflow/providers/sendgrid/src - ../../../empty:/opt/airflow/providers/sftp/src diff --git a/scripts/ci/docker-compose/tests-sources.yml b/scripts/ci/docker-compose/tests-sources.yml index 67b2590f69b61..9c02d1c271412 100644 --- a/scripts/ci/docker-compose/tests-sources.yml +++ b/scripts/ci/docker-compose/tests-sources.yml @@ -120,7 +120,6 @@ services: - ../../../providers/redis/tests:/opt/airflow/providers/redis/tests - ../../../providers/salesforce/tests:/opt/airflow/providers/salesforce/tests - ../../../providers/samba/tests:/opt/airflow/providers/samba/tests - - ../../../sdk/coordinators/java/tests:/opt/airflow/sdk/coordinators/java/tests - ../../../providers/segment/tests:/opt/airflow/providers/segment/tests - ../../../providers/sendgrid/tests:/opt/airflow/providers/sendgrid/tests - ../../../providers/sftp/tests:/opt/airflow/providers/sftp/tests diff --git a/task-sdk/src/airflow/sdk/__init__.py b/task-sdk/src/airflow/sdk/__init__.py index 7a5ed26747312..eeae86f1eb3d0 100644 --- a/task-sdk/src/airflow/sdk/__init__.py +++ b/task-sdk/src/airflow/sdk/__init__.py @@ -16,11 +16,6 @@ # under the License. from __future__ import annotations -# Make ``airflow.sdk`` a namespace-extending package so sibling distributions -# (e.g. ``apache-airflow-coordinators-java`` shipping -# ``airflow/sdk/coordinators/java/``) can contribute sub-packages. -__path__ = __import__("pkgutil").extend_path(__path__, __name__) - from typing import TYPE_CHECKING __all__ = [ diff --git a/uv.lock b/uv.lock index efed773ad2c32..3a097187f7d56 100644 --- a/uv.lock +++ b/uv.lock @@ -158,7 +158,6 @@ apache-aurflow-docker-stack = false members = [ "apache-airflow", "apache-airflow-breeze", - "apache-airflow-coordinators-java", "apache-airflow-core", "apache-airflow-ctl", "apache-airflow-ctl-tests", @@ -1891,39 +1890,6 @@ requires-dist = [ { name = "twine", specifier = ">=4.0.2" }, ] -[[package]] -name = "apache-airflow-coordinators-java" -version = "0.1.0" -source = { editable = "sdk/coordinators/java" } -dependencies = [ - { name = "apache-airflow-task-sdk" }, - { name = "pyyaml" }, -] - -[package.dev-dependencies] -dev = [ - { name = "apache-airflow" }, - { name = "apache-airflow-devel-common" }, - { name = "apache-airflow-task-sdk" }, -] -docs = [ - { name = "apache-airflow-devel-common", extra = ["docs"] }, -] - -[package.metadata] -requires-dist = [ - { name = "apache-airflow-task-sdk", editable = "task-sdk" }, - { name = "pyyaml", specifier = ">=6.0.2" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "apache-airflow", editable = "." }, - { name = "apache-airflow-devel-common", editable = "devel-common" }, - { name = "apache-airflow-task-sdk", editable = "task-sdk" }, -] -docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "devel-common" }] - [[package]] name = "apache-airflow-core" version = "3.3.0" @@ -21126,8 +21092,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "python_full_version >= '3.14' or platform_machine != 'arm64' or sys_platform != 'darwin'" }, - { name = "jeepney", marker = "python_full_version >= '3.14' or platform_machine != 'arm64' or sys_platform != 'darwin'" }, + { name = "cryptography", marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version < '3.15' and sys_platform == 'emscripten') or (python_full_version < '3.15' and sys_platform == 'win32') or (platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "jeepney", marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version < '3.15' and sys_platform == 'emscripten') or (python_full_version < '3.15' and sys_platform == 'win32') or (platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ From 92af8ad0244550d1d66c98161904347ddefe5050 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 19 May 2026 11:31:58 +0800 Subject: [PATCH 54/75] Fix import ignore test --- .../tests/task_sdk/docs/test_public_api.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/task-sdk/tests/task_sdk/docs/test_public_api.py b/task-sdk/tests/task_sdk/docs/test_public_api.py index a21424ea101f6..0961458131735 100644 --- a/task-sdk/tests/task_sdk/docs/test_public_api.py +++ b/task-sdk/tests/task_sdk/docs/test_public_api.py @@ -42,30 +42,31 @@ def test_airflow_sdk_no_unexpected_exports(): ignore = { "__getattr__", "__lazy_imports", - "SecretCache", "TYPE_CHECKING", + "SecretCache", "annotations", "api", "bases", + "configuration", + "coordinators", + "crypto", "definitions", + "exceptions", "execution_time", "io", + "lineage", + "listener", "log", - "exceptions", - "timezone", - "secrets_masker", - "configuration", "module_loading", - "yaml", - "serde", "observability", "plugins_manager", - "listener", - "crypto", "providers_manager_runtime", - "lineage", + "secrets_masker", + "serde", + "timezone", "types", "state", + "yaml", } unexpected = actual - public - ignore assert not unexpected, f"Unexpected exports in airflow.sdk: {sorted(unexpected)}" From d124f842a691df81a41b7f51f2cfee01f0e4998e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Mon, 18 May 2026 18:27:43 +0800 Subject: [PATCH 55/75] Rewrite Java coordinator --- .../sdk/coordinators/java/coordinator.py | 241 +++++-- .../airflow/sdk/execution_time/coordinator.py | 425 +++--------- .../airflow/sdk/execution_time/supervisor.py | 30 +- .../airflow/sdk/execution_time/task_runner.py | 102 +-- .../coordinators/java/test_coordinator.py | 639 ++++++++++++++---- .../execution_time/test_coordinator.py | 412 +---------- 6 files changed, 824 insertions(+), 1025 deletions(-) diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py index 3e8a2a2c4389e..0c752f06ed3f3 100644 --- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py +++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py @@ -19,17 +19,168 @@ from __future__ import annotations -from pathlib import Path -from typing import TYPE_CHECKING +import email +import os +import pathlib +import selectors +import socket +import subprocess +import time +import zipfile +from typing import TYPE_CHECKING, cast + +import attrs +import psutil +import structlog -from airflow.sdk.coordinators.java.bundle_scanner import BundleScanner, read_dag_code from airflow.sdk.execution_time.coordinator import BaseCoordinator +from airflow.sdk.execution_time.supervisor import ActivitySubprocess if TYPE_CHECKING: + from structlog.typing import FilteringBoundLogger + from typing_extensions import Self + + from airflow.sdk.api.client import Client from airflow.sdk.api.datamodels._generated import BundleInfo from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO +log: FilteringBoundLogger = structlog.get_logger(logger_name="coordinators.java") + + +def _start_server() -> socket.socket: + server = socket.socket() + server.bind(("127.0.0.1", 0)) + server.setblocking(True) + server.listen(1) # Just need to listen to the child process. + return server + + +def _calculate_classpath(app_home: pathlib.Path) -> str: + jars = (p.as_posix() for p in app_home.iterdir() if p.suffix == ".jar") + return os.pathsep.join(jars) + + +def _find_main_class(app_home: pathlib.Path) -> str: + for p in app_home.iterdir(): + if p.suffix != ".jar": + continue + with zipfile.ZipFile(p) as zf: + with zf.open("META-INF/MANIFEST.MF") as f: + if main_class := email.message_from_binary_file(f)["Main-Class"]: + return main_class + raise FileNotFoundError(f"cannot fine main class in {app_home.resolve()}") + +def _accept_connections( + servers: dict[str, socket.socket], + proc: subprocess.Popen, + *, + max_wait: float = 10.0, +) -> dict[str, socket.socket]: + """Block until the Java process connects to servers.""" + accepted: dict[str, socket.socket] = {} + with selectors.DefaultSelector() as sel: + for key, soc in servers.items(): + sel.register(soc, selectors.EVENT_READ, data=key) + deadline = time.monotonic() + max_wait + while len(accepted) < len(servers): + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError("process did not connect within timeout") + if proc.poll() is not None: + raise RuntimeError(f"process exited with {proc.returncode} before connecting") + for event, _ in sel.select(timeout=min(remaining, 1.0)): + log.debug("Accepting child process connection", key=(key := event.data)) + conn, _ = cast("socket.socket", event.fileobj).accept() + sel.unregister(servers[key]) + accepted[key] = conn + return accepted + + +@attrs.define(kw_only=True) +class _JavaActivitySubprocess(ActivitySubprocess): + """Java task runner process.""" + + _comm_server: socket.socket + _logs_server: socket.socket + _child_process: subprocess.Popen + + # Keep track of channels used to pipe subprocess stdout and stderr so we can + # close them on exit. The "read" side is handled by _register_pipe_readers + # callbacks so we don't need to worry about them. + _stdout_w: socket.socket + _stderr_w: socket.socket + + @classmethod + def start( # type: ignore[override] + cls, + *, + what: TaskInstanceDTO, + dag_rel_path: str | os.PathLike[str], + bundle_info, + logger: FilteringBoundLogger | None = None, + sentry_integration: str = "", + java_executable: str, + jvm_args: list[str], + bundles_folder: pathlib.Path, + **kwargs, + ) -> Self: + comm_server = _start_server() + logs_server = _start_server() + + stdout_r, stdout_w = socket.socketpair() + stderr_r, stderr_w = socket.socketpair() + + comm_host, comm_port = comm_server.getsockname() + logs_host, logs_port = logs_server.getsockname() + + proc = subprocess.Popen( + [ + java_executable, + "-classpath", + _calculate_classpath(bundles_folder), + *jvm_args, + _find_main_class(bundles_folder), + # Arguments to MainClass... + f"--comm={comm_host}:{comm_port}", + f"--logs={logs_host}:{logs_port}", + ], + stdout=stdout_w.makefile("wb", buffering=0).fileno(), + stderr=stderr_w.makefile("wb", buffering=0).fileno(), + ) + log.info("Starting subprocess", pid=proc.pid) + socks = _accept_connections({"comm": comm_server, "logs": logs_server}, proc) + + self = cls( + id=what.id, + pid=proc.pid, + process=psutil.Process(proc.pid), + process_log=logger or structlog.get_logger(logger_name="task").bind(), + start_time=time.monotonic(), + stdin=socks["comm"], + child_process=proc, + comm_server=comm_server, + logs_server=logs_server, + stdout_w=stdout_w, + stderr_w=stderr_w, + **kwargs, + ) + self._register_pipe_readers(stdout_r, stderr_r, socks["comm"], socks["logs"]) + self._on_child_started( + ti=what, + dag_rel_path=dag_rel_path, + bundle_info=bundle_info, + sentry_integration=sentry_integration, + ) + return self + + def wait(self) -> int: + code = super().wait() + self._close_unused_sockets(self._comm_server, self._logs_server, self._stdout_w, self._stderr_w) + return code + + +@attrs.define(kw_only=True) class JavaCoordinator(BaseCoordinator): """ Coordinator that launches a JVM subprocess for DAG parsing and task execution. @@ -55,67 +206,33 @@ class JavaCoordinator(BaseCoordinator): flow; unused for pure-Java DAGs. """ - def __init__( - self, - *, - java_executable: str = "java", - jvm_args: list[str] | None = None, - bundles_folder: str | None = None, - ) -> None: - self.java_executable = java_executable - self.jvm_args = list(jvm_args) if jvm_args else [] - self.bundles_folder = bundles_folder - - def get_code_from_file(self, fileloc: str) -> str: - """Read embedded DAG source code from a JAR bundle.""" - code = read_dag_code(Path(fileloc)) - if code is None: - raise FileNotFoundError(f"No DAG source code found in JAR: {fileloc}") - return code + java_executable: str = "java" + jvm_args: list[str] = attrs.field(factory=list) + bundles_folder: pathlib.Path = attrs.field(converter=pathlib.Path) - def task_execution_cmd( + def execute_task( self, *, what: TaskInstanceDTO, - dag_file_path: str, - bundle_path: str, + dag_rel_path: str | os.PathLike[str], bundle_info: BundleInfo, - comm_addr: str, - logs_addr: str, - ) -> list[str]: - """Build the ``java`` command for executing a task in a JAR bundle.""" - if dag_file_path.endswith(".jar"): - # Case 1: Pure Java Dag -- the dag_file_path points directly to a - # bundle JAR inside the Airflow Core Dag Bundle. - jar_path = Path(dag_file_path) - classpath = f"{bundle_path}/*" - return [ - self.java_executable, - *self.jvm_args, - "-classpath", - classpath, - BundleScanner.resolve_jar(jar_path), - f"--comm={comm_addr}", - f"--logs={logs_addr}", - ] - - # Case 2: Python Stub Dag -- the dag_file_path is a Python file but - # the task delegates to a Java runtime. The actual JAR bundle lives - # in ``bundles_folder`` (passed to __init__ from the [sdk] coordinators - # config entry). - if not self.bundles_folder: - raise ValueError( - "JavaCoordinator: bundles_folder kwarg must be set for Python stub DAGs " - "that delegate to Java task execution." - ) - - resolved = BundleScanner(Path(self.bundles_folder)).resolve(dag_id=what.dag_id) - return [ - self.java_executable, - *self.jvm_args, - "-classpath", - resolved.classpath, - resolved.main_class, - f"--comm={comm_addr}", - f"--logs={logs_addr}", - ] + client: Client, + logger: FilteringBoundLogger | None = None, + sentry_integration: str = "", + subprocess_logs_to_stdout: bool, + **kwargs, + ) -> BaseCoordinator.ExecutionResult: + process = _JavaActivitySubprocess.start( + what=what, + dag_rel_path=dag_rel_path, + bundle_info=bundle_info, + client=client, + logger=logger, + subprocess_logs_to_stdout=subprocess_logs_to_stdout, + sentry_integration=sentry_integration, + java_executable=self.java_executable, + jvm_args=self.jvm_args, + bundles_folder=self.bundles_folder, + ) + exit_code = process.wait() + return self.ExecutionResult(exit_code, process.final_state) diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 3857ef3ed95f5..852c70d77a203 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -42,136 +42,24 @@ from __future__ import annotations -import contextlib import functools -import os -import selectors -import socket -import subprocess -import time -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any -from airflow.sdk._shared.module_loading import import_string, qualname +import attrs + +from airflow.sdk._shared.module_loading import import_string if TYPE_CHECKING: + from collections.abc import Mapping + from os import PathLike + from structlog.typing import FilteringBoundLogger from typing_extensions import Self - from airflow.sdk.api.datamodels._generated import BundleInfo - from airflow.sdk.execution_time.comms import StartupDetails + from airflow.sdk.api.client import Client from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO -def _start_server() -> socket.socket: - """Create a TCP server socket bound to a random port on localhost.""" - server = socket.socket() - server.bind(("127.0.0.1", 0)) - server.setblocking(True) - server.listen(1) - return server - - -def _send_startup_details(runtime_comm: socket.socket, startup_details: StartupDetails) -> None: - """ - Re-encode and send the ``StartupDetails`` frame to the runtime subprocess. - - In the task execution flow, ``task_runner.main()`` consumes the - ``StartupDetails`` message from fd 0 (to determine routing) before - delegating to the runtime coordinator. This function re-serializes - the message and writes it to the runtime subprocess's comm socket so - the subprocess receives it as if it came directly from the supervisor. - """ - from airflow.sdk.execution_time.comms import _ResponseFrame - - # Use mode="json" so that datetime, UUID, and other complex Python - # types are serialized as plain strings/numbers in msgpack -- avoiding - # msgpack extension types (e.g. Timestamp) that non-Python decoders - # may not support. - frame = _ResponseFrame(id=0, body=startup_details.model_dump(mode="json")) - runtime_comm.sendall(frame.as_bytes()) - - -def _bridge( - supervisor_comm: socket.socket, - runtime_comm: socket.socket, - runtime_logs: socket.socket, - runtime_stderr: socket.socket, - proc: subprocess.Popen, - log: FilteringBoundLogger, -) -> None: - """ - Multiplex I/O between the supervisor and a runtime subprocess. - - Four channels are registered with the selector: - - - ``supervisor_comm`` -> ``runtime_comm`` (raw byte forwarding) - - ``runtime_comm`` -> ``supervisor_comm`` (raw byte forwarding) - - ``runtime_logs`` -> structlog (line-buffered JSON logs) - - ``runtime_stderr`` -> structlog (line-buffered stderr output) - - Uses the same ``(handler, on_close)`` callback contract as - :class:`~airflow.sdk.execution_time.supervisor.WatchedSubprocess`, - driven by :func:`~airflow.sdk.execution_time.selector_loop.service_selector`. - """ - from airflow.sdk.execution_time.selector_loop import ( - make_buffered_socket_reader, - make_raw_forwarder, - service_selector, - ) - from airflow.sdk.execution_time.supervisor import ( - forward_to_log, - process_log_messages_from_subprocess, - ) - - sel = selectors.DefaultSelector() - - def on_close(sock: socket.socket) -> None: - with contextlib.suppress(KeyError): - sel.unregister(sock) - - target_loggers = (log,) - - # Comm: bidirectional raw byte forwarding. - sel.register(supervisor_comm, selectors.EVENT_READ, make_raw_forwarder(runtime_comm, on_close)) - sel.register(runtime_comm, selectors.EVENT_READ, make_raw_forwarder(supervisor_comm, on_close)) - - # TCP logs channel: line-buffered JSON from the runtime SDK's LogSender, - # processed with the same handler as WatchedSubprocess (level mapping, - # timestamp parsing, exception extraction). - sel.register( - runtime_logs, - selectors.EVENT_READ, - make_buffered_socket_reader(process_log_messages_from_subprocess(target_loggers), on_close), - ) - # stderr: plain-text output from the runtime process's logging framework - # (e.g. SLF4J simple logger). Use forward_to_log which handles raw - # text lines, not process_log_messages_from_subprocess which expects JSON. - import logging - - sel.register( - runtime_stderr, - selectors.EVENT_READ, - make_buffered_socket_reader( - forward_to_log(target_loggers, logger="task.stderr", level=logging.ERROR), on_close - ), - ) - - # Event loop -- runs until the subprocess exits and all sockets are drained. - while sel.get_map(): - service_selector(sel, timeout=1.0) - if proc.poll() is not None: - # Subprocess has exited -- drain remaining data with a short deadline. - deadline = time.monotonic() + 5.0 - while sel.get_map() and time.monotonic() < deadline: - service_selector(sel, timeout=0.5) - break - - sel.close() - for sock in (supervisor_comm, runtime_comm, runtime_logs, runtime_stderr): - with contextlib.suppress(OSError): - sock.close() - - class BaseCoordinator: """ Base coordinator for runtime-specific DAG file processing and task execution. @@ -180,208 +68,80 @@ class BaseCoordinator: (see :class:`CoordinatorManager`) — each entry's ``classpath`` is resolved via :func:`~airflow.sdk._shared.module_loading.import_string` and constructed with the entry's ``kwargs``. - - Subclasses represent a specific SDK runtime (Java, Go, etc.) and only - need to implement :meth:`task_execution_cmd`. The class owns the entire - bridge lifecycle: TCP servers, subprocess management, selector-based I/O - loop, and cleanup. """ - class DagParsingInfo(NamedTuple): - """Information needed for runtime Dag parsing.""" - - dag_file_path: str - bundle_name: str - bundle_path: str - mode: str = "dag-parsing" - - class TaskExecutionInfo(NamedTuple): - """Information needed for runtime task execution.""" - - what: TaskInstanceDTO - dag_rel_path: str | os.PathLike[str] - bundle_info: BundleInfo - startup_details: StartupDetails - mode: str = "task-execution" + @attrs.define(slots=True) + class ExecutionResult: + """Return value for :meth:`BaseCoordinator.execute_task`.""" - def get_code_from_file(self, fileloc: str) -> str: - """ - Return the human-readable source code for a DAG file managed by this coordinator. - - Called by :class:`~airflow.models.dagcode.DagCode` when persisting DAG - source to the metadata database. The default Python path reads ``.py`` - files directly; runtime coordinators must override this to extract source - from their native packaging format (e.g. reading an embedded ``.java`` - file from a JAR bundle). - - :param fileloc: Absolute path to the DAG file (e.g. a ``/path/to/example.jar``). - :return: The source code as a string. - :raises FileNotFoundError: If source code cannot be retrieved from *fileloc*. - """ - raise NotImplementedError + exit_code: Any + final_state: str - def task_execution_cmd( + def execute_task( self, *, what: TaskInstanceDTO, - dag_file_path: str, - bundle_path: str, - bundle_info: BundleInfo, - comm_addr: str, - logs_addr: str, - ) -> list[str]: + dag_rel_path: str | PathLike[str], + bundle_info, + client: Client, + logger: FilteringBoundLogger | None = None, + sentry_integration: str = "", + subprocess_logs_to_stdout: bool, + **kwargs, + ) -> ExecutionResult: """ - Return the subprocess command for task execution. - - :param what: The task instance to execute. - :param dag_file_path: Absolute path to the DAG file. - :param bundle_path: Root path of the DAG bundle. - :param bundle_info: Bundle metadata. - :param comm_addr: ``host:port`` the subprocess must connect to - for the bidirectional msgpack comm channel. - :param logs_addr: ``host:port`` the subprocess must connect to - for the structured JSON log channel. - :returns: Full command list. + Start task execution. + + This should execute the task and return a result. """ raise NotImplementedError - def run_task_execution( - self, - *, - what: TaskInstanceDTO, - dag_rel_path: str | os.PathLike[str], - bundle_info: BundleInfo, - startup_details: StartupDetails, - ) -> None: - self._runtime_subprocess_entrypoint( - self.TaskExecutionInfo( - what=what, - dag_rel_path=dag_rel_path, - bundle_info=bundle_info, - startup_details=startup_details, - ) - ) - def _runtime_subprocess_entrypoint(self, entrypoint_info: DagParsingInfo | TaskExecutionInfo) -> None: - """ - Spawn the runtime subprocess and bridge I/O with the supervisor. - - This is called inside the forked child process where fd 0 is the - bidirectional comms socket to the supervisor. The method: - - 1. Creates TCP servers for comm and logs. - 2. Calls :meth:`task_execution_cmd` to get the command. - 3. Spawns the subprocess with ``stdin=/dev/null`` and stderr - captured via a socketpair. - 4. Runs the selector-based bridge until the subprocess exits. - - Two distinct IPC mechanisms are used because each channel has a - different initiator: - - - The runtime subprocess actively *connects* to the comm and logs - TCP servers using ``host:port`` strings passed via the command line - -- portable across every language's stdlib socket API. - - stderr is *inherited*: the subprocess writes to fd 2 transparently - (its native logging framework targets stderr by default), so we - replace fd 2 with one end of a socketpair instead of teaching the - runtime about an address. ``subprocess.PIPE`` would not work - because :func:`make_buffered_socket_reader` requires a real socket. - - fd layout of *this* coordinator process (set up by - ``_reopen_std_io_handles`` before this runs): - - - fd 0 -- bidirectional comms socket to the supervisor - (``DagFileParseRequest`` <-> ``DagFileParsingResult``, - length-prefixed msgpack frames) - - fd 1 -- stdout socket to the supervisor - - fd 2 -- stderr socket to the supervisor - - fd N -- structured JSON log channel (``log_fd``, configured by - ``_configure_logs_over_json_channel`` -> structlog) - - The runtime subprocess gets ``stdin=DEVNULL``, inherits fd 1 (so its - stdout flows straight to the supervisor), and has its fd 2 replaced - by the coordinator-owned end of the stderr socketpair. - """ - os.environ["_AIRFLOW_PROCESS_CONTEXT"] = "client" +class _PythonCoordinator(BaseCoordinator): + """ + Coordinator implementation to execute Python tasks. - import structlog + This is not supposed to be specified by users directly, but the fallback + used by default when nothing is specified. + """ - log = structlog.get_logger(logger_name="task") - log.info( - "Starting runtime subprocess", - type=qualname(self), - mode=entrypoint_info.mode, + def execute_task( + self, + *, + what: TaskInstanceDTO, + dag_rel_path: str | PathLike[str], + bundle_info, + client: Client, + logger: FilteringBoundLogger | None = None, + sentry_integration: str = "", + subprocess_logs_to_stdout: bool, + **kwargs, + ) -> BaseCoordinator.ExecutionResult: + # TODO: Importing this at the top causes circular imports. + # ActivitySubprocess and WatchedSubprocess should be moved out of the + # supervisor, and maybe with additional refactoring to abstract out + # process handling. + from airflow.sdk.execution_time.supervisor import ActivitySubprocess + + process = ActivitySubprocess.start( + dag_rel_path=dag_rel_path, + what=what, + client=client, + logger=logger, + bundle_info=bundle_info, + subprocess_logs_to_stdout=subprocess_logs_to_stdout, + sentry_integration=sentry_integration, ) + exit_code = process.wait() + return self.ExecutionResult(exit_code, process.final_state) - # TCP servers for the runtime subprocess to connect to. - comm_server = _start_server() - logs_server = _start_server() - comm_host, comm_port = comm_server.getsockname() - logs_host, logs_port = logs_server.getsockname() - - comm_addr = f"{comm_host}:{comm_port}" - logs_addr = f"{logs_host}:{logs_port}" - - # stderr uses a socketpair (instead of ``subprocess.PIPE``) so it - # is a real socket compatible with ``make_buffered_socket_reader``. - child_stderr, read_stderr = socket.socketpair() - - # For task execution, hold a BundleVersionLock for the entire - # subprocess lifetime to prevent the bundle version from being - # garbage-collected while the runtime process is still running. - bundle_version_lock: contextlib.AbstractContextManager = contextlib.nullcontext() - - if isinstance(entrypoint_info, self.TaskExecutionInfo): - from airflow.dag_processing.bundles.base import BundleVersionLock - from airflow.sdk.execution_time.task_runner import resolve_bundle - - bundle_instance = resolve_bundle(entrypoint_info.bundle_info, log) - resolved_dag_file_path = bundle_instance.path / entrypoint_info.dag_rel_path - - cmd = self.task_execution_cmd( - what=entrypoint_info.what, - dag_file_path=os.fspath(resolved_dag_file_path), - bundle_path=os.fspath(bundle_instance.path), - bundle_info=entrypoint_info.bundle_info, - comm_addr=comm_addr, - logs_addr=logs_addr, - ) - bundle_version_lock = BundleVersionLock( - bundle_name=entrypoint_info.bundle_info.name, - bundle_version=entrypoint_info.bundle_info.version, - ) - else: - raise ValueError(f"Unknown entrypoint_info type: {type(entrypoint_info)}") - with bundle_version_lock: - # stdin redirected to /dev/null so the subprocess does not inherit - # fd 0 (the comms socket). - proc = subprocess.Popen( - cmd, - stdin=subprocess.DEVNULL, - stderr=child_stderr.fileno(), - ) - child_stderr.close() - - # Wait for the subprocess to connect to both servers. - runtime_comm, _ = comm_server.accept() - runtime_logs, _ = logs_server.accept() - comm_server.close() - logs_server.close() - - # For task execution the supervisor already sent ``StartupDetails`` - # on fd 0 and ``task_runner.main()`` consumed it before delegating - # here. Re-encode and forward it to the runtime subprocess so it - # knows which task to execute. - if isinstance(entrypoint_info, self.TaskExecutionInfo): - _send_startup_details(runtime_comm, entrypoint_info.startup_details) - - # fd 0 is the bidirectional comms socket to the supervisor. - supervisor_comm = socket.socket(fileno=os.dup(0)) - - _bridge(supervisor_comm, runtime_comm, runtime_logs, read_stderr, proc, log) +@functools.cache +def _build_python_coordinator() -> _PythonCoordinator: + return _PythonCoordinator() +@attrs.define class CoordinatorManager: """ Registry of coordinator instances loaded from the ``[sdk] coordinators`` config. @@ -403,57 +163,46 @@ class CoordinatorManager: to route tasks to a specific coordinator instance (for example, a ``"legacy-java"`` queue routed to a JDK 11 coordinator and a ``"modern-java"`` queue routed to a JDK 17 coordinator). + + :meta private: """ - def __init__( - self, - instances_by_name: dict[str, BaseCoordinator], - queue_to_coordinator: dict[str, str], - ) -> None: - self._instances_by_name = instances_by_name - self._queue_to_coordinator = queue_to_coordinator + _queue_to_coordinator: Mapping[str, BaseCoordinator] @classmethod def from_config(cls) -> Self: """Load coordinator instances from the ``[sdk]`` configuration.""" from airflow.sdk.configuration import conf - entries = conf.getjson("sdk", "coordinators", fallback=[]) - if not isinstance(entries, list): - entries = [] - - instances: dict[str, BaseCoordinator] = {} - for entry in entries: - if not isinstance(entry, dict): - continue - name = entry.get("name") - classpath = entry.get("classpath") - if not name or not classpath: - continue - kwargs = entry.get("kwargs") or {} - coordinator_cls = import_string(classpath) - instances[name] = coordinator_cls(**kwargs) + coordinator_entry_list = conf.getjson("sdk", "coordinators", fallback=[]) + if not isinstance(coordinator_entry_list, list): + coordinator_entries = {} + else: + coordinator_entries = {d["name"]: d for d in coordinator_entry_list if "name" in d} queue_mapping = conf.getjson("sdk", "queue_to_coordinator", fallback={}) if not isinstance(queue_mapping, dict): queue_mapping = {} - return cls(instances, queue_mapping) + def _build_coordinator(key: str) -> BaseCoordinator: + entry = coordinator_entries[key] + coordinator_cls = import_string(entry["classpath"]) + return coordinator_cls(**entry["kwargs"]) - def all(self) -> list[BaseCoordinator]: - """Return all loaded coordinator instances, sorted by configured name.""" - return [self._instances_by_name[name] for name in sorted(self._instances_by_name)] + queue_to_coordinator = { + queue: _build_coordinator(coordinator_key) + for queue, coordinator_key in queue_mapping.items() + if coordinator_key in coordinator_entries + } + return cls(queue_to_coordinator) - def get(self, name: str) -> BaseCoordinator | None: - """Return the coordinator instance registered under *name*, or ``None``.""" - return self._instances_by_name.get(name) + def for_queue(self, queue: str) -> BaseCoordinator: + """ + Find the coordinator for *queue*. - def for_queue(self, queue: str) -> BaseCoordinator | None: - """Return the coordinator instance routed to *queue*, or ``None``.""" - name = self._queue_to_coordinator.get(queue) - if name is None: - return None - return self._instances_by_name.get(name) + If an entry is not registered, a Python coordinator is returned. + """ + return self._queue_to_coordinator.get(queue) or _build_python_coordinator() @functools.cache diff --git a/task-sdk/src/airflow/sdk/execution_time/supervisor.py b/task-sdk/src/airflow/sdk/execution_time/supervisor.py index d505c3111c17b..2e9ef7ded7d29 100644 --- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py +++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py @@ -130,6 +130,7 @@ _RequestFrame, _ResponseFrame, ) +from airflow.sdk.execution_time.coordinator import get_coordinator_manager from airflow.sdk.execution_time.request_handlers import ( handle_delete_variable, handle_delete_xcom, @@ -2244,6 +2245,8 @@ def supervise_task( path to a callable to initialize it (empty means no integration). :return: Exit code of the process. :raises ValueError: If server URL is empty or invalid. + :raises InvalidCoordinatorError: If the coordinator for the task is not + configured correctly. """ _make_process_nondumpable() @@ -2284,6 +2287,17 @@ def supervise_task( if not dag_rel_path: raise ValueError("dag_path is required") + try: + coordinator = get_coordinator_manager().for_queue(ti.queue) + except: + log.exception( + "Failed to initialize coordinator for task", + dag_id=ti.dag_id, + task_id=ti.task_id, + queue=ti.queue, + ) + raise + with _ensure_client(server, token, client=client, dry_run=dry_run) as client: start = time.monotonic() @@ -2303,27 +2317,25 @@ def supervise_task( reset_secrets_masker() try: - process = ActivitySubprocess.start( - dag_rel_path=dag_rel_path, + result = coordinator.execute_task( what=ti, + dag_rel_path=dag_rel_path, + bundle_info=bundle_info, client=client, logger=logger, - bundle_info=bundle_info, - subprocess_logs_to_stdout=subprocess_logs_to_stdout, sentry_integration=sentry_integration, + subprocess_logs_to_stdout=subprocess_logs_to_stdout, ) - - exit_code = process.wait() end = time.monotonic() log.info( "Workload finished", workload_type="ExecuteTask", workload_id=str(ti.id), - exit_code=exit_code, + exit_code=result.exit_code, duration=end - start, - final_state=process.final_state, + final_state=result.final_state, ) - return exit_code + return result.exit_code finally: if log_path and log_file_descriptor: log_file_descriptor.close() diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index bb8b320575f6c..313309fa40100 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -50,7 +50,6 @@ from airflow.sdk.api.client import get_hostname, getuser from airflow.sdk.api.datamodels._generated import ( AssetProfile, - BundleInfo, DagRun, PreviousTIResponse, TaskInstance, @@ -842,7 +841,12 @@ def parse(what: StartupDetails, log: Logger) -> RuntimeTaskInstance: bundle_info = what.bundle_info bundle_prepare_start = time.monotonic() - bundle_instance = resolve_bundle(bundle_info, log) + bundle_instance = DagBundlesManager().get_bundle( + name=bundle_info.name, + version=bundle_info.version, + ) + bundle_instance.initialize() + _verify_bundle_access(bundle_instance, log) bundle_prepare_ms = int((time.monotonic() - bundle_prepare_start) * 1000) dag_absolute_path = os.fspath(Path(bundle_instance.path, what.dag_rel_path)) @@ -969,22 +973,6 @@ def _verify_bundle_access(bundle_instance: BaseDagBundle, log: Logger) -> None: ) -def resolve_bundle(bundle_info: BundleInfo, log: Logger) -> BaseDagBundle: - """ - Resolve, initialize, and verify access to a DAG bundle. - - Used by both the standard Python task execution path and locale - coordinators (Java, Go, etc.) to obtain a ready-to-use bundle instance. - """ - bundle_instance = DagBundlesManager().get_bundle( - name=bundle_info.name, - version=bundle_info.version, - ) - bundle_instance.initialize() - _verify_bundle_access(bundle_instance, log) - return bundle_instance - - def get_startup_details() -> StartupDetails: # The parent sends us a StartupDetails message un-prompted. After this, every single message is only sent # in response to us sending a request. @@ -2092,72 +2080,6 @@ def flush_spans(): provider.force_flush(timeout_millis=timeout_millis) -def _resolve_runtime_entrypoint(startup_details: StartupDetails, log: Logger) -> Callable[[], None] | None: - """ - Check configured runtime coordinators for a runtime-specific entrypoint. - - Resolution order: - - 1. **Queue mapping** -- the ``[sdk] queue_to_coordinator`` config maps - the task's ``queue`` to a coordinator name from ``[sdk] coordinators``. - Used by the python-stub pattern where users set the queue explicitly. - 2. **DAG file extension** -- if no queue mapping matches, the DAG file's - extension (e.g. ``.jar``) is compared against each coordinator's - ``file_extension``. Used by the pure-runtime DAG pattern where the - entire DAG is authored in a non-Python language. - - Returns a no-arg callable that bridges fd 0 to the runtime subprocess, - or ``None`` to fall through to the standard Python execution path. - """ - import functools - - from airflow.sdk.execution_time.coordinator import get_coordinator_manager - - manager = get_coordinator_manager() - - def _build(coordinator) -> Callable[[], None]: - return functools.partial( - coordinator.run_task_execution, - what=startup_details.ti, - dag_rel_path=startup_details.dag_rel_path, - bundle_info=startup_details.bundle_info, - startup_details=startup_details, - ) - - # Step 1: queue-to-coordinator mapping. - queue = startup_details.ti.queue - if (coordinator := manager.for_queue(queue)) is not None: - log.debug( - "Resolved coordinator for task via queue mapping", - coordinator=type(coordinator).__qualname__, - queue=queue, - task_id=startup_details.ti.task_id, - ) - return _build(coordinator) - - # Step 2: DAG file extension fallback (pure- DAGs). - dag_rel_path = startup_details.dag_rel_path - for coordinator in manager.all(): - ext = getattr(type(coordinator), "file_extension", None) - if not ext or not dag_rel_path.endswith(ext): - continue - log.debug( - "Resolved coordinator for task via DAG file extension", - coordinator=type(coordinator).__qualname__, - dag_rel_path=dag_rel_path, - task_id=startup_details.ti.task_id, - ) - return _build(coordinator) - - log.debug( - "No runtime coordinator matched, using standard Python execution path", - queue=queue, - dag_rel_path=dag_rel_path, - task_id=startup_details.ti.task_id, - ) - return None - - @flush_spans() def main(): log = structlog.get_logger(logger_name="task") @@ -2185,16 +2107,8 @@ def main(): # startup message as a ResendLoggingFD response. if os.environ.pop("_AIRFLOW_FORK_EXEC", None) == "1": reinit_supervisor_comms() - # Check if a configured runtime coordinator should handle this - # task (e.g. Java, Go) instead of the standard Python - # execution path. - log.debug("Checking for runtime-specific entrypoint") - runtime_entrypoint = _resolve_runtime_entrypoint(startup_details, log) - if runtime_entrypoint is not None: - runtime_entrypoint() - return - span = _make_task_span(msg=startup_details) - stack.enter_context(span) + span_ctx_mgr = _make_task_span(msg=startup_details) + span = stack.enter_context(span_ctx_mgr) ti, context, log = startup(msg=startup_details) except AirflowRescheduleException as reschedule: log.warning("Rescheduling task during startup, marking task as UP_FOR_RESCHEDULE") diff --git a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py index f9f63439d84c3..d1c1761bfd4c8 100644 --- a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py +++ b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py @@ -15,23 +15,33 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. + from __future__ import annotations -import uuid +import contextlib +import os +import pathlib +import re +import socket +import subprocess +import threading +import time import zipfile -from pathlib import Path +from unittest.mock import MagicMock, patch import pytest -import yaml - -from airflow.sdk.api.datamodels._generated import BundleInfo -from airflow.sdk.coordinators.java.bundle_scanner import ( - MAIN_CLASS_MANIFEST_KEY, - MANIFEST_PATH, - METADATA_MANIFEST_KEY, - SDK_VERSION_MANIFEST_KEY, +from uuid6 import uuid7 + +from airflow.sdk.coordinators.java.coordinator import ( + JavaCoordinator, + _accept_connections, + _calculate_classpath, + _find_main_class, + _JavaActivitySubprocess, + _start_server, ) -from airflow.sdk.coordinators.java.coordinator import JavaCoordinator +from airflow.sdk.execution_time.coordinator import BaseCoordinator +from airflow.sdk.execution_time.supervisor import ActivitySubprocess from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO from tests_common.test_utils.version_compat import AIRFLOW_V_3_3_PLUS @@ -44,62 +54,228 @@ TEST_MAIN_CLASS = "com.example.MyBundle" -def _make_manifest( - *, - main_class: str | None = TEST_MAIN_CLASS, - metadata_path: str | None = METADATA_YAML_PATH, - dag_code_path: str | None = None, -) -> str: - lines = ["Manifest-Version: 1.0"] - if main_class: - lines.append(f"{MAIN_CLASS_MANIFEST_KEY}: {main_class}") - if metadata_path: - lines.append(f"{METADATA_MANIFEST_KEY}: {metadata_path}") - lines.append(f"{SDK_VERSION_MANIFEST_KEY}: 1.0.0") - if dag_code_path: - lines.append(f"Airflow-Java-SDK-Dag-Code: {dag_code_path}") - return "\n".join(lines) + "\n" - - -def _create_bundle_jar( - jar_path: Path, - *, - dag_ids: list[str] | None = None, - dag_code: str | None = None, -) -> Path: - with zipfile.ZipFile(jar_path, "w") as zf: - dag_code_path = DAG_CODE_PATH if dag_code else None - manifest = _make_manifest(dag_code_path=dag_code_path) - zf.writestr(MANIFEST_PATH, manifest) - if dag_ids is not None: - metadata = yaml.dump({"dags": {d: {} for d in dag_ids}}) - zf.writestr(METADATA_YAML_PATH, metadata) - if dag_code: - zf.writestr(DAG_CODE_PATH, dag_code) - return jar_path - - -def _make_ti(dag_id: str = "test_dag") -> TaskInstanceDTO: +def _make_ti(dag_id: str = "test_dag", queue: str = "java") -> TaskInstanceDTO: return TaskInstanceDTO( - id=uuid.uuid4(), - dag_version_id=uuid.uuid4(), + id=uuid7(), + dag_version_id=uuid7(), task_id="task_1", dag_id=dag_id, run_id="run_1", try_number=1, map_index=-1, pool_slots=1, - queue="default", + queue=queue, priority_weight=1, ) +def _make_jar(path: pathlib.Path, *, main_class: str | None = "com.example.Main") -> pathlib.Path: + """Write a minimal JAR with (optionally) a Main-Class manifest entry.""" + lines = ["Manifest-Version: 1.0"] + if main_class: + lines.append(f"Main-Class: {main_class}") + manifest = "\n".join(lines) + "\n\n" + with zipfile.ZipFile(path, "w") as zf: + zf.writestr("META-INF/MANIFEST.MF", manifest) + return path + + +class TestStartServer: + def test_returns_listening_socket(self): + server = _start_server() + try: + _, port = server.getsockname() + finally: + server.close() + assert port > 0 + + def test_two_calls_return_different_ports(self): + s1 = _start_server() + s2 = _start_server() + try: + _, port1 = s1.getsockname() + _, port2 = s2.getsockname() + finally: + s1.close() + s2.close() + assert port1 != port2 + + def test_accepts_connection(self): + conn = client = None + server = _start_server() + try: + _, port = server.getsockname() + client = socket.socket() + client.connect(("127.0.0.1", port)) + conn, _ = server.accept() + conn.sendall(b"ping") + received = client.recv(4) + finally: + if conn: + conn.close() + if client: + client.close() + server.close() + assert received == b"ping" + + +class TestCalculateClasspath: + def test_single_jar(self, tmp_path): + jar = tmp_path.joinpath("app.jar") + jar.write_bytes(b"") + result = _calculate_classpath(tmp_path) + assert result == jar.as_posix() + + def test_multiple_jars_all_included(self, tmp_path): + tmp_path.joinpath("a.jar").write_bytes(b"") + tmp_path.joinpath("b.jar").write_bytes(b"") + tmp_path.joinpath("c.jar").write_bytes(b"") + result = _calculate_classpath(tmp_path) + entries = set(result.split(os.pathsep)) + assert entries == { + tmp_path.joinpath("a.jar").as_posix(), + tmp_path.joinpath("b.jar").as_posix(), + tmp_path.joinpath("c.jar").as_posix(), + } + + def test_non_jar_files_excluded(self, tmp_path): + jar = tmp_path.joinpath("app.jar") + jar.write_bytes(b"") + tmp_path.joinpath("readme.txt").write_bytes(b"") + tmp_path.joinpath("config.yaml").write_bytes(b"") + result = _calculate_classpath(tmp_path) + assert result == jar.as_posix() + + def test_empty_directory_returns_empty_string(self, tmp_path): + result = _calculate_classpath(tmp_path) + assert result == "" + + +class TestFindMainClass: + def test_returns_main_class_from_jar(self, tmp_path): + _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main") + assert _find_main_class(tmp_path) == "com.example.Main" + + def test_no_jars_raises_file_not_found(self, tmp_path): + with pytest.raises(FileNotFoundError, match=re.escape(str(tmp_path.resolve()))): + _find_main_class(tmp_path) + + def test_jar_without_main_class_not_returned(self, tmp_path): + _make_jar(tmp_path.joinpath("app.jar"), main_class=None) + with pytest.raises(FileNotFoundError): + _find_main_class(tmp_path) + + def test_non_jar_files_skipped(self, tmp_path): + tmp_path.joinpath("readme.txt").write_bytes(b"not a jar") + _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main") + assert _find_main_class(tmp_path) == "com.example.Main" + + def test_first_jar_missing_main_class_falls_through_to_second(self, tmp_path): + # Alphabetically: a.jar (no Main-Class), b.jar (has Main-Class). + _make_jar(tmp_path.joinpath("a.jar"), main_class=None) + _make_jar(tmp_path.joinpath("b.jar"), main_class="com.example.Fallback") + assert _find_main_class(tmp_path) == "com.example.Fallback" + + def test_fully_qualified_class_name_preserved(self, tmp_path): + _make_jar(tmp_path.joinpath("app.jar"), main_class="org.apache.airflow.sdk.java.TaskRunner") + assert _find_main_class(tmp_path) == "org.apache.airflow.sdk.java.TaskRunner" + + +class TestAcceptConnections: + def _connect_after_delay(self, addr: tuple[str, int], delay: float = 0.0) -> None: + def _connect(): + time.sleep(delay) + c = socket.socket() + with contextlib.suppress(OSError): # Server may already be closed in teardown. + c.connect(addr) + + threading.Thread(target=_connect, daemon=True).start() + + def test_accepts_single_server(self): + server = _start_server() + _, port = server.getsockname() + self._connect_after_delay(("127.0.0.1", port)) + + mock_proc = MagicMock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + + try: + accepted = _accept_connections({"comm": server}, mock_proc) + assert "comm" in accepted + accepted["comm"].close() + finally: + server.close() + + def test_accepts_multiple_servers(self): + comm_server = _start_server() + logs_server = _start_server() + _, comm_port = comm_server.getsockname() + _, logs_port = logs_server.getsockname() + + self._connect_after_delay(("127.0.0.1", comm_port)) + self._connect_after_delay(("127.0.0.1", logs_port)) + + mock_proc = MagicMock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + + try: + accepted = _accept_connections({"comm": comm_server, "logs": logs_server}, mock_proc) + assert set(accepted) == {"comm", "logs"} + for sock in accepted.values(): + sock.close() + finally: + comm_server.close() + logs_server.close() + + def test_raises_timeout_when_no_connection(self): + server = _start_server() + mock_proc = MagicMock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + try: + with pytest.raises(TimeoutError, match="did not connect within timeout"): + _accept_connections({"comm": server}, mock_proc, max_wait=0.05) + finally: + server.close() + + def test_raises_runtime_error_if_process_exits_before_connecting(self): + server = _start_server() + mock_proc = MagicMock(spec=subprocess.Popen) + # proc has already exited + mock_proc.poll.return_value = 1 + mock_proc.returncode = 1 + try: + with pytest.raises(RuntimeError, match="process exited with 1"): + _accept_connections({"comm": server}, mock_proc) + finally: + server.close() + + def test_returned_sockets_are_connected(self): + """Accepted sockets should be real, usable connections.""" + server = _start_server() + _, port = server.getsockname() + + client = socket.socket() + client.connect(("127.0.0.1", port)) + + mock_proc = MagicMock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + + try: + accepted = _accept_connections({"comm": server}, mock_proc) + accepted["comm"].sendall(b"hello") + assert client.recv(5) == b"hello" + accepted["comm"].close() + client.close() + finally: + server.close() + + class TestJavaCoordinatorAttributes: def test_default_kwargs(self): - coordinator = JavaCoordinator() + coordinator = JavaCoordinator(bundles_folder="/airflow/java-bundles") assert coordinator.java_executable == "java" assert coordinator.jvm_args == [] - assert coordinator.bundles_folder is None + assert coordinator.bundles_folder == pathlib.Path("/airflow/java-bundles") def test_custom_kwargs(self): coordinator = JavaCoordinator( @@ -109,83 +285,306 @@ def test_custom_kwargs(self): ) assert coordinator.java_executable == "/opt/java/bin/java" assert coordinator.jvm_args == ["-Xmx512m", "-Xms256m"] - assert coordinator.bundles_folder == "/airflow/java-bundles" + assert coordinator.bundles_folder == pathlib.Path("/airflow/java-bundles") -class TestGetCodeFromFile: - def test_returns_embedded_code(self, tmp_path: Path): - code = "from airflow import DAG\ndag = DAG('my_dag')" - jar = _create_bundle_jar(tmp_path / "with_code.jar", dag_ids=["d"], dag_code=code) - assert JavaCoordinator().get_code_from_file(str(jar)) == code +@pytest.fixture +def bundle_dir(tmp_path): + _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.TaskRunner") + return tmp_path - def test_raises_when_no_code(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "no_code.jar", dag_ids=["d"]) - with pytest.raises(FileNotFoundError, match="No DAG source code found in JAR"): - JavaCoordinator().get_code_from_file(str(jar)) +@pytest.fixture +def mock_client(make_ti_context): + client = MagicMock() + client.task_instances.start.return_value = make_ti_context() + return client -class TestTaskExecutionCmd: - def test_pure_java_dag(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "app.jar", dag_ids=["test_dag"]) - bundle_path = str(tmp_path) + +class TestJavaCoordinatorExecuteTask: + def _captured_popen_cmd( + self, + bundle_dir: pathlib.Path, + mock_client, + *, + java_executable: str = "java", + jvm_args: list[str] | None = None, + ) -> list[str]: + """Run execute_task with mocked subprocess and return the command list.""" ti = _make_ti() - bundle_info = BundleInfo(name="my_bundle") - - cmd = JavaCoordinator().task_execution_cmd( - what=ti, # type: ignore[arg-type] - dag_file_path=str(jar), - bundle_path=bundle_path, - bundle_info=bundle_info, - comm_addr="localhost:1234", - logs_addr="localhost:5678", + coordinator = JavaCoordinator( + java_executable=java_executable, + jvm_args=jvm_args or [], + bundles_folder=bundle_dir, ) - assert cmd == [ - "java", - "-classpath", - f"{bundle_path}/*", - TEST_MAIN_CLASS, - "--comm=localhost:1234", - "--logs=localhost:5678", - ] - - def test_python_stub_dag_uses_bundles_folder_kwarg(self, tmp_path: Path): - bundles_folder = tmp_path / "java_bundles" - bundle_sub = bundles_folder / "my_bundle" - bundle_sub.mkdir(parents=True) - _create_bundle_jar(bundle_sub / "app.jar", dag_ids=["stub_dag"]) - - ti = _make_ti(dag_id="stub_dag") - bundle_info = BundleInfo(name="my_bundle") - - coordinator = JavaCoordinator(bundles_folder=str(bundles_folder)) - cmd = coordinator.task_execution_cmd( - what=ti, # type: ignore[arg-type] - dag_file_path="/dags/stub_dag.py", - bundle_path="/some/bundle/path", - bundle_info=bundle_info, - comm_addr="localhost:1234", - logs_addr="localhost:5678", + + mock_proc = MagicMock(spec=subprocess.Popen) + mock_proc.pid = 12345 + comm_sock = MagicMock(spec=socket.socket) + logs_sock = MagicMock(spec=socket.socket) + popen_calls: list = [] + + def capture_popen(cmd, **kwargs): + popen_calls.append(cmd) + return mock_proc + + with ( + patch( + "airflow.sdk.coordinators.java.coordinator.subprocess.Popen", + side_effect=capture_popen, + ), + patch( + "airflow.sdk.coordinators.java.coordinator._accept_connections", + return_value={"comm": comm_sock, "logs": logs_sock}, + ), + patch.object(ActivitySubprocess, "_register_pipe_readers"), + patch.object(ActivitySubprocess, "_on_child_started"), + patch.object(ActivitySubprocess, "wait", return_value=0), + patch("psutil.Process"), + ): + coordinator.execute_task( + what=ti, + dag_rel_path="dags/test.jar", + bundle_info=MagicMock(), + client=mock_client, + subprocess_logs_to_stdout=False, + ) + + assert popen_calls, "subprocess.Popen was not called" + return popen_calls[0] + + def test_java_executable_is_first_arg(self, bundle_dir, mock_client): + cmd = self._captured_popen_cmd( + bundle_dir, mock_client, java_executable="/usr/lib/jvm/java-17/bin/java" ) + assert cmd[0] == "/usr/lib/jvm/java-17/bin/java" + + def test_classpath_flag_and_value_present(self, bundle_dir, mock_client): + cmd = self._captured_popen_cmd(bundle_dir, mock_client) + assert "-classpath" in cmd + cp_idx = cmd.index("-classpath") + classpath = cmd[cp_idx + 1] + assert bundle_dir.joinpath("app.jar").as_posix() in classpath + + def test_main_class_present(self, bundle_dir, mock_client): + cmd = self._captured_popen_cmd(bundle_dir, mock_client) + assert "com.example.TaskRunner" in cmd + + def test_comm_and_logs_args_present(self, bundle_dir, mock_client): + cmd = self._captured_popen_cmd(bundle_dir, mock_client) + comm_args = [a for a in cmd if a.startswith("--comm=")] + logs_args = [a for a in cmd if a.startswith("--logs=")] + assert len(comm_args) == 1 + assert len(logs_args) == 1 + + def test_comm_and_logs_contain_port(self, bundle_dir, mock_client): + cmd = self._captured_popen_cmd(bundle_dir, mock_client) + comm_arg = next(a for a in cmd if a.startswith("--comm=")) + logs_arg = next(a for a in cmd if a.startswith("--logs=")) + # format is host:port + assert ":" in comm_arg.split("=", 1)[1] + assert ":" in logs_arg.split("=", 1)[1] + + def test_jvm_args_inserted_before_main_class(self, bundle_dir, mock_client): + cmd = self._captured_popen_cmd(bundle_dir, mock_client, jvm_args=["-Xmx512m", "-Dsome.prop=value"]) + main_idx = cmd.index("com.example.TaskRunner") + for jvm_arg in ["-Xmx512m", "-Dsome.prop=value"]: + assert jvm_arg in cmd + assert cmd.index(jvm_arg) < main_idx + + def test_comm_and_logs_after_main_class(self, bundle_dir, mock_client): + cmd = self._captured_popen_cmd(bundle_dir, mock_client) + main_idx = cmd.index("com.example.TaskRunner") + comm_idx = next(i for i, a in enumerate(cmd) if a.startswith("--comm=")) + logs_idx = next(i for i, a in enumerate(cmd) if a.startswith("--logs=")) + assert comm_idx > main_idx + assert logs_idx > main_idx + + def test_returns_execution_result(self, bundle_dir, mock_client): + ti = _make_ti() + coordinator = JavaCoordinator(bundles_folder=bundle_dir) + + mock_proc = MagicMock(spec=subprocess.Popen) + mock_proc.pid = 99999 + comm_sock = MagicMock(spec=socket.socket) + logs_sock = MagicMock(spec=socket.socket) + + with ( + patch("subprocess.Popen", return_value=mock_proc), + patch( + "airflow.sdk.coordinators.java.coordinator._accept_connections", + return_value={"comm": comm_sock, "logs": logs_sock}, + ), + patch.object(ActivitySubprocess, "_register_pipe_readers"), + patch.object(ActivitySubprocess, "_on_child_started"), + patch.object(ActivitySubprocess, "wait", return_value=0), + patch("psutil.Process"), + ): + result = coordinator.execute_task( + what=ti, + dag_rel_path="dags/test.jar", + bundle_info=MagicMock(), + client=mock_client, + subprocess_logs_to_stdout=False, + ) + + assert isinstance(result, BaseCoordinator.ExecutionResult) + assert result.exit_code == 0 - assert cmd == [ - "java", - "-classpath", - f"{bundles_folder}/my_bundle/app.jar", - TEST_MAIN_CLASS, - "--comm=localhost:1234", - "--logs=localhost:5678", - ] - def test_python_stub_dag_without_bundles_folder_raises(self): +class TestJavaActivitySubprocessStart: + """ + Unit tests for _JavaActivitySubprocess.start(). + + These tests mock subprocess.Popen and _accept_connections to verify that + start() wires up the right command and stores the right sockets, + without requiring a real Java runtime. + """ + + def _start_with_mocks( + self, + bundle_dir: pathlib.Path, + mock_client, + *, + java_executable: str = "java", + jvm_args: list[str] | None = None, + ti: TaskInstanceDTO | None = None, + ): + """Call _JavaActivitySubprocess.start() with all subprocess machinery mocked out.""" + ti = ti or _make_ti() + + mock_proc = MagicMock(spec=subprocess.Popen) + mock_proc.pid = 12345 + comm_sock = MagicMock(spec=socket.socket) + logs_sock = MagicMock(spec=socket.socket) + + with ( + patch( + "airflow.sdk.coordinators.java.coordinator.subprocess.Popen", + return_value=mock_proc, + ) as popen_mock, + patch( + "airflow.sdk.coordinators.java.coordinator._accept_connections", + return_value={"comm": comm_sock, "logs": logs_sock}, + ), + patch.object(ActivitySubprocess, "_register_pipe_readers"), + patch.object(ActivitySubprocess, "_on_child_started"), + patch("psutil.Process"), + ): + proc = _JavaActivitySubprocess.start( + what=ti, + dag_rel_path="dags/test.jar", + bundle_info=MagicMock(), + client=mock_client, + java_executable=java_executable, + jvm_args=jvm_args or [], + bundles_folder=bundle_dir, + subprocess_logs_to_stdout=False, + ) + + return proc, popen_mock + + def test_stdout_write_socket_stored_for_cleanup(self, bundle_dir, mock_client): + proc, _ = self._start_with_mocks(bundle_dir, mock_client) + # _stdout_w must be stored so wait() can close it + assert proc._stdout_w is not None + + def test_stderr_write_socket_stored_for_cleanup(self, bundle_dir, mock_client): + proc, _ = self._start_with_mocks(bundle_dir, mock_client) + assert proc._stderr_w is not None + + def test_stdout_and_stderr_write_sockets_are_distinct(self, bundle_dir, mock_client): + proc, _ = self._start_with_mocks(bundle_dir, mock_client) + assert proc._stdout_w is not proc._stderr_w + + def test_stdin_is_comm_socket(self, bundle_dir, mock_client): + """stdin (used by send_msg) must be the accepted comm socket.""" ti = _make_ti() - bundle_info = BundleInfo(name="my_bundle") - - with pytest.raises(ValueError, match="bundles_folder kwarg must be set"): - JavaCoordinator().task_execution_cmd( - what=ti, # type: ignore[arg-type] - dag_file_path="/dags/stub_dag.py", - bundle_path="/some/bundle/path", - bundle_info=bundle_info, - comm_addr="localhost:1234", - logs_addr="localhost:5678", + comm_sock = MagicMock(spec=socket.socket) + logs_sock = MagicMock(spec=socket.socket) + + with ( + patch("airflow.sdk.coordinators.java.coordinator.subprocess.Popen") as popen_mock, + patch( + "airflow.sdk.coordinators.java.coordinator._accept_connections", + return_value={"comm": comm_sock, "logs": logs_sock}, + ), + patch.object(ActivitySubprocess, "_register_pipe_readers"), + patch.object(ActivitySubprocess, "_on_child_started"), + patch("psutil.Process"), + ): + popen_mock.return_value.pid = 12345 + proc = _JavaActivitySubprocess.start( + what=ti, + dag_rel_path="dags/test.jar", + bundle_info=MagicMock(), + client=MagicMock(), + java_executable="java", + jvm_args=[], + bundles_folder=bundle_dir, + subprocess_logs_to_stdout=False, + ) + + assert proc.stdin is comm_sock + + def test_pid_taken_from_popen(self, bundle_dir, mock_client): + proc, _ = self._start_with_mocks(bundle_dir, mock_client) + assert proc.pid == 12345 + + def test_on_child_started_called(self, bundle_dir, mock_client): + ti = _make_ti() + with ( + patch("airflow.sdk.coordinators.java.coordinator.subprocess.Popen") as popen_mock, + patch( + "airflow.sdk.coordinators.java.coordinator._accept_connections", + return_value={"comm": MagicMock(), "logs": MagicMock()}, + ), + patch.object(ActivitySubprocess, "_register_pipe_readers"), + patch.object(ActivitySubprocess, "_on_child_started") as mock_on_started, + patch("psutil.Process"), + ): + popen_mock.return_value.pid = 12345 + _JavaActivitySubprocess.start( + what=ti, + dag_rel_path="dags/test.jar", + bundle_info=MagicMock(), + client=mock_client, + java_executable="java", + jvm_args=[], + bundles_folder=bundle_dir, + subprocess_logs_to_stdout=False, ) + + mock_on_started.assert_called_once() + kwargs = mock_on_started.call_args.kwargs + assert kwargs["ti"] is ti + assert kwargs["dag_rel_path"] == "dags/test.jar" + + def test_register_pipe_readers_called_with_four_sockets(self, bundle_dir, mock_client): + """Both socketpair read-ends and both TCP sockets must be registered.""" + with ( + patch("airflow.sdk.coordinators.java.coordinator.subprocess.Popen") as popen_mock, + patch( + "airflow.sdk.coordinators.java.coordinator._accept_connections", + return_value={"comm": MagicMock(), "logs": MagicMock()}, + ), + patch.object(ActivitySubprocess, "_register_pipe_readers") as mock_register, + patch.object(ActivitySubprocess, "_on_child_started"), + patch("psutil.Process"), + ): + popen_mock.return_value.pid = 12345 + _JavaActivitySubprocess.start( + what=_make_ti(), + dag_rel_path="dags/test.jar", + bundle_info=MagicMock(), + client=mock_client, + java_executable="java", + jvm_args=[], + bundles_folder=bundle_dir, + subprocess_logs_to_stdout=False, + ) + + mock_register.assert_called_once() + args = mock_register.call_args.args + # positional: stdout, stderr, comm, logs — all four must be sockets + assert len(args) == 4 diff --git a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py index 507140c98f9cf..45800aac6b765 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py +++ b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py @@ -15,415 +15,29 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. + from __future__ import annotations -import contextlib import json -import os -import socket -import subprocess -from pathlib import Path -from unittest.mock import MagicMock, patch import pytest from airflow.sdk.execution_time.coordinator import ( BaseCoordinator, CoordinatorManager, - _bridge, - _send_startup_details, - _start_server, + _PythonCoordinator, get_coordinator_manager, reset_coordinator_manager, ) -class TestStartServer: - def test_binds_to_localhost(self): - server = _start_server() - try: - host, port = server.getsockname() - assert host == "127.0.0.1" - assert port > 0 - finally: - server.close() - - def test_assigns_random_port(self): - s1 = _start_server() - s2 = _start_server() - try: - _, port1 = s1.getsockname() - _, port2 = s2.getsockname() - assert port1 != port2 - finally: - s1.close() - s2.close() - - def test_accepts_connection(self): - server = _start_server() - try: - addr = server.getsockname() - client = socket.socket() - client.connect(addr) - conn, _ = server.accept() - conn.sendall(b"ping") - assert client.recv(4) == b"ping" - conn.close() - client.close() - finally: - server.close() - - -class TestSendStartupDetails: - def test_sends_frame_bytes_to_socket(self): - mock_startup = MagicMock() - mock_startup.model_dump.return_value = {"type": "StartupDetails", "ti": {}} - - mock_socket = MagicMock(spec=socket.socket) - - _send_startup_details(mock_socket, mock_startup) - - mock_startup.model_dump.assert_called_once_with(mode="json") - mock_socket.sendall.assert_called_once() - - sent_bytes = mock_socket.sendall.call_args[0][0] - assert len(sent_bytes) > 4 - length = int.from_bytes(sent_bytes[:4], "big") - assert length == len(sent_bytes) - 4 - - def test_frame_contains_response_id_zero(self): - import msgpack - - mock_startup = MagicMock() - mock_startup.model_dump.return_value = {"type": "StartupDetails"} - - mock_socket = MagicMock(spec=socket.socket) - - _send_startup_details(mock_socket, mock_startup) - - sent_bytes = mock_socket.sendall.call_args[0][0] - frame = msgpack.unpackb(sent_bytes[4:]) - assert frame[0] == 0 - - def test_frame_body_matches_model_dump(self): - import msgpack - - body = {"type": "StartupDetails", "ti": {"task_id": "t1"}, "dag_rel_path": "test.jar"} - mock_startup = MagicMock() - mock_startup.model_dump.return_value = body - - mock_socket = MagicMock(spec=socket.socket) - - _send_startup_details(mock_socket, mock_startup) - - sent_bytes = mock_socket.sendall.call_args[0][0] - frame = msgpack.unpackb(sent_bytes[4:]) - assert frame[1] == body - - def test_real_socket_roundtrip(self): - import msgpack - - server = socket.socket() - server.bind(("127.0.0.1", 0)) - server.listen(1) - addr = server.getsockname() - - client = socket.socket() - client.connect(addr) - conn, _ = server.accept() - - try: - body = {"type": "StartupDetails", "value": 42} - mock_startup = MagicMock() - mock_startup.model_dump.return_value = body - - _send_startup_details(conn, mock_startup) - - length_bytes = client.recv(4) - length = int.from_bytes(length_bytes, "big") - - data = client.recv(length) - frame = msgpack.unpackb(data) - assert frame[0] == 0 - assert frame[1] == body - finally: - conn.close() - client.close() - server.close() - - -class TestBaseCoordinatorDefaults: - def test_get_code_from_file_raises_not_implemented(self): - with pytest.raises(NotImplementedError): - BaseCoordinator().get_code_from_file("/path/to/dag.jar") - - def test_task_execution_cmd_raises_not_implemented(self): - with pytest.raises(NotImplementedError): - BaseCoordinator().task_execution_cmd( - what=MagicMock(), - dag_file_path="/dag.jar", - bundle_path="/path", - bundle_info=MagicMock(), - comm_addr="127.0.0.1:1234", - logs_addr="127.0.0.1:1235", - ) - - -class TestCoordinatorNamedTuples: - def test_dag_parsing_info_defaults(self): - info = BaseCoordinator.DagParsingInfo( - dag_file_path="/dag.jar", - bundle_name="my-bundle", - bundle_path="/bundles/my-bundle", - ) - assert info.mode == "dag-parsing" - assert info.dag_file_path == "/dag.jar" - assert info.bundle_name == "my-bundle" - assert info.bundle_path == "/bundles/my-bundle" - - def test_task_execution_info_defaults(self): - mock_ti = MagicMock() - mock_bundle = MagicMock() - mock_startup = MagicMock() - info = BaseCoordinator.TaskExecutionInfo( - what=mock_ti, - dag_rel_path="dags/example.jar", - bundle_info=mock_bundle, - startup_details=mock_startup, - ) - assert info.mode == "task-execution" - assert info.what is mock_ti - assert info.dag_rel_path == "dags/example.jar" - - -class TestBridge: - def test_bridge_forwards_comm_bidirectionally(self): - sup_send, sup_recv = socket.socketpair() - rt_send, rt_recv = socket.socketpair() - log_send, log_recv = socket.socketpair() - stderr_send, stderr_recv = socket.socketpair() - - mock_proc = MagicMock(spec=subprocess.Popen) - mock_proc.poll.return_value = 0 - mock_log = MagicMock() - - try: - sup_send.sendall(b"from_supervisor") - rt_send.sendall(b"from_runtime") - log_send.sendall(b'{"event":"hello","level":"info"}\n') - stderr_send.sendall(b"stderr line\n") - - sup_send.close() - rt_send.close() - log_send.close() - stderr_send.close() - - _bridge(sup_recv, rt_recv, log_recv, stderr_recv, mock_proc, mock_log) - finally: - for s in (sup_send, rt_send, log_send, stderr_send, sup_recv, rt_recv, log_recv, stderr_recv): - with contextlib.suppress(OSError): - s.close() - - def test_bridge_drains_after_process_exit(self): - sup_local, sup_remote = socket.socketpair() - rt_local, rt_remote = socket.socketpair() - log_local, log_remote = socket.socketpair() - stderr_local, stderr_remote = socket.socketpair() - - mock_proc = MagicMock(spec=subprocess.Popen) - mock_proc.poll.side_effect = [None, 0, 0, 0, 0, 0, 0, 0, 0, 0] - mock_log = MagicMock() - - try: - stderr_local.sendall(b"error output\n") - stderr_local.close() - sup_local.close() - rt_local.close() - log_local.close() - - _bridge(sup_remote, rt_remote, log_remote, stderr_remote, mock_proc, mock_log) - finally: - for s in ( - sup_local, - sup_remote, - rt_local, - rt_remote, - log_local, - log_remote, - stderr_local, - stderr_remote, - ): - with contextlib.suppress(OSError): - s.close() - - def test_bridge_closes_all_sockets(self): - sup = MagicMock(spec=socket.socket) - rt = MagicMock(spec=socket.socket) - logs = MagicMock(spec=socket.socket) - stderr = MagicMock(spec=socket.socket) - - mock_proc = MagicMock(spec=subprocess.Popen) - mock_proc.poll.return_value = 0 - mock_log = MagicMock() - - with ( - patch("airflow.sdk.execution_time.coordinator.selectors.DefaultSelector") as mock_sel_cls, - patch("airflow.sdk.execution_time.selector_loop.service_selector"), - ): - mock_sel = MagicMock() - mock_sel_cls.return_value = mock_sel - mock_sel.get_map.return_value = {} - - _bridge(sup, rt, logs, stderr, mock_proc, mock_log) - - sup.close.assert_called() - rt.close.assert_called() - logs.close.assert_called() - stderr.close.assert_called() - mock_sel.close.assert_called_once() - - -class _StubCoordinator(BaseCoordinator): - sdk = "test" - file_extension = ".test" - - def __init__(self, *, exec_cmd: list[str] | None = None): - self._exec_cmd = exec_cmd or ["test-runtime", "--execute"] - - def task_execution_cmd(self, *, dag_file_path, **_): - return [*self._exec_cmd, dag_file_path] - - -class TestRunTaskExecution: - @patch.object(BaseCoordinator, "_runtime_subprocess_entrypoint") - def test_run_task_execution_creates_task_execution_info(self, mock_entrypoint): - mock_ti = MagicMock() - mock_bundle_info = MagicMock() - mock_startup = MagicMock() - - coordinator = _StubCoordinator() - coordinator.run_task_execution( - what=mock_ti, - dag_rel_path="dags/example.jar", - bundle_info=mock_bundle_info, - startup_details=mock_startup, - ) - - mock_entrypoint.assert_called_once() - info = mock_entrypoint.call_args[0][0] - assert isinstance(info, BaseCoordinator.TaskExecutionInfo) - assert info.what is mock_ti - assert info.dag_rel_path == "dags/example.jar" - assert info.bundle_info is mock_bundle_info - assert info.startup_details is mock_startup - assert info.mode == "task-execution" - - -class TestRuntimeSubprocessEntrypoint: - @pytest.fixture(autouse=True) - def _restore_process_context_env(self): - old = os.environ.get("_AIRFLOW_PROCESS_CONTEXT") - try: - yield - finally: - if old is None: - os.environ.pop("_AIRFLOW_PROCESS_CONTEXT", None) - else: - os.environ["_AIRFLOW_PROCESS_CONTEXT"] = old - - def test_unknown_entrypoint_info_type_raises(self): - coordinator = _StubCoordinator() - fake_info = MagicMock() - fake_info.mode = "unknown" - - with pytest.raises(ValueError, match="Unknown entrypoint_info type"): - coordinator._runtime_subprocess_entrypoint(fake_info) # type: ignore[arg-type] - - @patch("airflow.sdk.execution_time.coordinator._bridge") - @patch("airflow.sdk.execution_time.coordinator._send_startup_details") - @patch("subprocess.Popen", autospec=True) - @patch("airflow.sdk.execution_time.coordinator._start_server") - @patch("os.dup", return_value=99) - @patch("airflow.sdk.execution_time.task_runner.resolve_bundle") - @patch("airflow.dag_processing.bundles.base.BundleVersionLock", autospec=True) - def test_task_execution_flow( - self, - mock_bundle_lock, - mock_resolve_bundle, - mock_dup, - mock_start_server, - mock_popen, - mock_send_startup, - mock_bridge, - ): - comm_server = MagicMock(spec=socket.socket) - comm_server.getsockname.return_value = ("127.0.0.1", 6000) - logs_server = MagicMock(spec=socket.socket) - logs_server.getsockname.return_value = ("127.0.0.1", 6001) - mock_start_server.side_effect = [comm_server, logs_server] - - runtime_comm = MagicMock(spec=socket.socket) - runtime_logs = MagicMock(spec=socket.socket) - comm_server.accept.return_value = (runtime_comm, ("127.0.0.1", 9000)) - logs_server.accept.return_value = (runtime_logs, ("127.0.0.1", 9001)) - - child_stderr = MagicMock(spec=socket.socket) - read_stderr = MagicMock(spec=socket.socket) - child_stderr.fileno.return_value = 10 - - mock_bundle_instance = MagicMock() - mock_bundle_instance.path = Path("/resolved/bundles/test-bundle") - mock_resolve_bundle.return_value = mock_bundle_instance - - mock_lock_instance = MagicMock() - mock_bundle_lock.return_value = mock_lock_instance - mock_lock_instance.__enter__ = MagicMock(return_value=mock_lock_instance) - mock_lock_instance.__exit__ = MagicMock(return_value=False) - - mock_ti = MagicMock() - mock_bundle_info = MagicMock() - mock_bundle_info.name = "test-bundle" - mock_bundle_info.version = "v1" - mock_startup = MagicMock() - - coordinator = _StubCoordinator(exec_cmd=["test-runtime", "--execute"]) - info = BaseCoordinator.TaskExecutionInfo( - what=mock_ti, - dag_rel_path="dags/example.test", - bundle_info=mock_bundle_info, - startup_details=mock_startup, - ) - - supervisor_comm = MagicMock(spec=socket.socket) - - with ( - patch("socket.socketpair", return_value=(child_stderr, read_stderr)), - patch("airflow.sdk.execution_time.coordinator.socket.socket", return_value=supervisor_comm), - ): - coordinator._runtime_subprocess_entrypoint(info) - - mock_resolve_bundle.assert_called_once() - mock_bundle_lock.assert_called_once_with(bundle_name="test-bundle", bundle_version="v1") - - mock_popen.assert_called_once() - cmd = mock_popen.call_args[0][0] - assert cmd == ["test-runtime", "--execute", "/resolved/bundles/test-bundle/dags/example.test"] - - mock_send_startup.assert_called_once_with(runtime_comm, mock_startup) - mock_bridge.assert_called_once() - - class _CoordinatorA(BaseCoordinator): - sdk = "a" - file_extension = ".a" - def __init__(self, *, label: str = "a"): self.label = label class _CoordinatorB(BaseCoordinator): - sdk = "b" - file_extension = ".b" + pass class TestCoordinatorManager: @@ -457,13 +71,11 @@ def test_from_config_loads_instances(self, monkeypatch): conf.invalidate_cache() manager = CoordinatorManager.from_config() + assert list(manager._queue_to_coordinator) == ["queue-a"] - alpha = manager.get("alpha") - beta = manager.get("beta") - assert isinstance(alpha, _CoordinatorA) - assert isinstance(beta, _CoordinatorB) - assert alpha.label == "alpha-label" - assert {type(c) for c in manager.all()} == {_CoordinatorA, _CoordinatorB} + coordinator_for_queue_a = manager.for_queue("queue-a") + assert isinstance(coordinator_for_queue_a, _CoordinatorA) + assert coordinator_for_queue_a.label == "alpha-label" def test_from_config_empty(self, monkeypatch): monkeypatch.delenv("AIRFLOW__SDK__COORDINATORS", raising=False) @@ -474,20 +86,16 @@ def test_from_config_empty(self, monkeypatch): conf.invalidate_cache() manager = CoordinatorManager.from_config() - assert manager.all() == [] - assert manager.get("missing") is None + assert manager._queue_to_coordinator == {} def test_for_queue_resolves_via_mapping(self): coordinator_a = _CoordinatorA() coordinator_b = _CoordinatorB() - manager = CoordinatorManager( - {"alpha": coordinator_a, "beta": coordinator_b}, - {"queue-a": "alpha", "queue-b": "beta"}, - ) + manager = CoordinatorManager({"queue-a": coordinator_a, "queue-b": coordinator_b}) assert manager.for_queue("queue-a") is coordinator_a assert manager.for_queue("queue-b") is coordinator_b - assert manager.for_queue("queue-missing") is None + assert isinstance(manager.for_queue("queue-missing"), _PythonCoordinator) def test_get_coordinator_manager_is_cached(self, monkeypatch): monkeypatch.delenv("AIRFLOW__SDK__COORDINATORS", raising=False) From f8b38825bf5e69ba2d58a73a5958a5268201abf9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 19 May 2026 18:32:44 +0800 Subject: [PATCH 56/75] Accept [sdk] coordinators as dict and lazy init --- .../src/airflow/config_templates/config.yml | 34 +++--- .../airflow/sdk/execution_time/coordinator.py | 101 ++++++++++-------- .../execution_time/test_coordinator.py | 83 ++++++++------ 3 files changed, 120 insertions(+), 98 deletions(-) diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml index e40164194aa67..502934dbc7749 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -2009,35 +2009,35 @@ sdk: options: coordinators: description: | - JSON list of runtime coordinator entries. + JSON object mapping of coordinator keys to coordinator definitions. - Each entry is an object with ``name``, ``classpath`` and optional - ``kwargs``. ``classpath`` is resolved via ``import_string`` and - constructed with ``kwargs`` once per process. Entries are + Each value is an object with ``classpath`` and optional ``kwargs``. + ``classpath`` is resolved via ``import_string`` and constructed with + ``kwargs`` on first use. Entries are independent instances, so the same ``classpath`` can be configured - multiple times with different ``kwargs`` (for example, two - ``JavaCoordinator`` instances pinned to different JDK versions). - version_added: 3.1.7 + multiple times under different names with different ``kwargs`` (for + example, two ``JavaCoordinator`` instances pinned to different JDK + versions). + version_added: 3.3.0 type: string example: | - [ - { - "name": "jdk-17", + { + "jdk-17": { "classpath": "airflow.sdk.coordinators.java.JavaCoordinator", "kwargs": {"java_executable": "/usr/lib/jvm/java-17-openjdk/bin/java", "jvm_args": ["-Xmx1024m"]} } - ] + } default: ~ queue_to_coordinator: description: | - JSON mapping of queue names to coordinator ``name`` from + JSON mapping of queue names to a coordinator key from ``[sdk] coordinators``. - When a task's ``language`` field is not set, this mapping is checked - to route the task to a configured coordinator instance based on its - queue. This is useful when queues are used as environment or - isolation identifiers (e.g. ``legacy-java``, ``modern-java``). - version_added: 3.1.7 + This mapping is checked to route a task to a configured coordinator + instance based on its queue. An entry in this mapping is needed if + tasks of a queue should be handled by a custom coordinator instead of + the default Python task-running mechanism. + version_added: 3.3.0 type: string example: '{"legacy-java": "jdk-11", "modern-java": "jdk-17"}' default: ~ diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 852c70d77a203..cf0dd4e920f0e 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -42,12 +42,15 @@ from __future__ import annotations +import contextlib import functools from typing import TYPE_CHECKING, Any import attrs +import pydantic from airflow.sdk._shared.module_loading import import_string +from airflow.sdk.configuration import conf if TYPE_CHECKING: from collections.abc import Mapping @@ -59,6 +62,13 @@ from airflow.sdk.api.client import Client from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO +__all__ = [ + "BaseCoordinator", + "CoordinatorManager", + "get_coordinator_manager", + "reset_coordinator_manager", +] + class BaseCoordinator: """ @@ -97,6 +107,11 @@ def execute_task( raise NotImplementedError +class _CoordinatorSpec(pydantic.BaseModel): + classpath: str + kwargs: dict[str, Any] + + class _PythonCoordinator(BaseCoordinator): """ Coordinator implementation to execute Python tasks. @@ -141,60 +156,59 @@ def _build_python_coordinator() -> _PythonCoordinator: return _PythonCoordinator() -@attrs.define +@attrs.define(kw_only=True) class CoordinatorManager: """ - Registry of coordinator instances loaded from the ``[sdk] coordinators`` config. + Registry of coordinator instances loaded from ``[sdk]`` configurations. - Each entry in the JSON list takes the form:: + The ``[sdk] coordinators`` value is a JSON object keyed by coordinator name:: { - "name": "jdk-11", - "classpath": "airflow.sdk.coordinators.java.JavaCoordinator", - "kwargs": {"java_executable": "/usr/lib/jvm/jdk-11/bin/java", ...} + "jdk-11": { + "classpath": "airflow.sdk.coordinators.java.JavaCoordinator", + "kwargs": {"java_executable": "/usr/lib/jvm/jdk-11/bin/java", ...}, + } } The ``classpath`` is resolved via - :func:`~airflow.sdk._shared.module_loading.import_string` (no - :class:`ProvidersManager` involvement) and constructed with ``kwargs``. + :func:`~airflow.sdk._shared.module_loading.import_string` and constructed + with ``kwargs`` on first use. A coordinator entry that is never looked up + incurs no startup cost. - The ``[sdk] queue_to_coordinator`` config maps queue names to a coordinator - ``name`` from that list, which lets users reuse existing queue assignments - to route tasks to a specific coordinator instance (for example, a - ``"legacy-java"`` queue routed to a JDK 11 coordinator and a - ``"modern-java"`` queue routed to a JDK 17 coordinator). + The ``[sdk] queue_to_coordinator`` config maps queue names to a key in the + object, which lets users reuse existing queue assignments to route tasks to + a specific coordinator instance (for example, a ``"legacy-java"`` queue + routed to a JDK 11 coordinator, and a ``"modern-java"`` queue routed to a + JDK 17 coordinator). :meta private: """ - _queue_to_coordinator: Mapping[str, BaseCoordinator] + _coordinator_specs: Mapping[str, _CoordinatorSpec] + _queue_to_coordinator: Mapping[str, str] + + _created_coordinators: dict[str, BaseCoordinator] = attrs.field(init=False, factory=dict) @classmethod def from_config(cls) -> Self: - """Load coordinator instances from the ``[sdk]`` configuration.""" - from airflow.sdk.configuration import conf - - coordinator_entry_list = conf.getjson("sdk", "coordinators", fallback=[]) - if not isinstance(coordinator_entry_list, list): - coordinator_entries = {} - else: - coordinator_entries = {d["name"]: d for d in coordinator_entry_list if "name" in d} - - queue_mapping = conf.getjson("sdk", "queue_to_coordinator", fallback={}) - if not isinstance(queue_mapping, dict): - queue_mapping = {} - - def _build_coordinator(key: str) -> BaseCoordinator: - entry = coordinator_entries[key] - coordinator_cls = import_string(entry["classpath"]) - return coordinator_cls(**entry["kwargs"]) - - queue_to_coordinator = { - queue: _build_coordinator(coordinator_key) - for queue, coordinator_key in queue_mapping.items() - if coordinator_key in coordinator_entries + """Load coordinator specs from configuration without initialization.""" + coordinator_specs = { + k: _CoordinatorSpec.model_validate(v) + for k, v in conf.getjson("sdk", "coordinators", fallback={}).items() } - return cls(queue_to_coordinator) + queue_to_coordinator = conf.getjson("sdk", "queue_to_coordinator", fallback={}) + for key in queue_to_coordinator.values(): + if key not in coordinator_specs: + raise ValueError(f"[sdk] queue_to_coordinator references invalid coordinator key: {key!r}") + return cls(coordinator_specs=coordinator_specs, queue_to_coordinator=queue_to_coordinator) + + def _for_queue_internal(self, queue: str) -> BaseCoordinator: + key = self._queue_to_coordinator[queue] + with contextlib.suppress(KeyError): + return self._created_coordinators[key] + spec = self._coordinator_specs[key] + coordinator = self._created_coordinators[key] = import_string(spec.classpath)(**spec.kwargs) + return coordinator def for_queue(self, queue: str) -> BaseCoordinator: """ @@ -202,7 +216,10 @@ def for_queue(self, queue: str) -> BaseCoordinator: If an entry is not registered, a Python coordinator is returned. """ - return self._queue_to_coordinator.get(queue) or _build_python_coordinator() + try: + return self._for_queue_internal(queue) + except KeyError: + return _build_python_coordinator() @functools.cache @@ -214,11 +231,3 @@ def get_coordinator_manager() -> CoordinatorManager: def reset_coordinator_manager() -> None: """Clear the cached :class:`CoordinatorManager` (test helper).""" get_coordinator_manager.cache_clear() - - -__all__ = [ - "BaseCoordinator", - "CoordinatorManager", - "get_coordinator_manager", - "reset_coordinator_manager", -] diff --git a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py index 45800aac6b765..8e3bcfa102172 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py +++ b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py @@ -22,6 +22,7 @@ import pytest +from airflow.sdk.configuration import conf from airflow.sdk.execution_time.coordinator import ( BaseCoordinator, CoordinatorManager, @@ -40,6 +41,32 @@ class _CoordinatorB(BaseCoordinator): pass +@pytest.fixture +def sdk_config(monkeypatch): + """Set the ``[sdk]`` env vars consumed by :meth:`CoordinatorManager.from_config`. + + :return: Callable ``apply(*, coordinators=None, queue_to_coordinator=None)`` -- + each argument is the raw JSON string for the matching env var, or ``None`` + to unset it. The conf cache is invalidated after each call (and again on + teardown) so ``from_config()`` re-reads the values just set. + """ + from airflow.sdk.configuration import conf + + def _apply(*, coordinators: str | None = None, queue_to_coordinator: str | None = None) -> None: + if coordinators is None: + monkeypatch.delenv("AIRFLOW__SDK__COORDINATORS", raising=False) + else: + monkeypatch.setenv("AIRFLOW__SDK__COORDINATORS", coordinators) + if queue_to_coordinator is None: + monkeypatch.delenv("AIRFLOW__SDK__QUEUE_TO_COORDINATOR", raising=False) + else: + monkeypatch.setenv("AIRFLOW__SDK__QUEUE_TO_COORDINATOR", queue_to_coordinator) + conf.invalidate_cache() + + yield _apply + conf.invalidate_cache() + + class TestCoordinatorManager: @pytest.fixture(autouse=True) def _reset_cache(self): @@ -47,56 +74,42 @@ def _reset_cache(self): yield reset_coordinator_manager() - def test_from_config_loads_instances(self, monkeypatch): - coordinators_json = json.dumps( - [ - { - "name": "alpha", - "classpath": f"{_CoordinatorA.__module__}._CoordinatorA", - "kwargs": {"label": "alpha-label"}, - }, + def test_from_config_loads_specs_and_resolves_instances(self, sdk_config): + sdk_config( + coordinators=json.dumps( { - "name": "beta", - "classpath": f"{_CoordinatorB.__module__}._CoordinatorB", - }, - ] + "alpha": { + "classpath": f"{_CoordinatorA.__module__}._CoordinatorA", + "kwargs": {"label": "alpha-label"}, + }, + "beta": {"classpath": f"{_CoordinatorB.__module__}._CoordinatorB", "kwargs": {}}, + } + ), + queue_to_coordinator=json.dumps({"queue-a": "alpha"}), ) - queue_json = json.dumps({"queue-a": "alpha"}) - - monkeypatch.setenv("AIRFLOW__SDK__COORDINATORS", coordinators_json) - monkeypatch.setenv("AIRFLOW__SDK__QUEUE_TO_COORDINATOR", queue_json) - - from airflow.sdk.configuration import conf - - conf.invalidate_cache() - manager = CoordinatorManager.from_config() - assert list(manager._queue_to_coordinator) == ["queue-a"] + assert manager._queue_to_coordinator == {"queue-a": "alpha"} + assert manager._created_coordinators == {} coordinator_for_queue_a = manager.for_queue("queue-a") assert isinstance(coordinator_for_queue_a, _CoordinatorA) - assert coordinator_for_queue_a.label == "alpha-label" + assert manager.for_queue("queue-a") is coordinator_for_queue_a, "instance should be cached" + assert manager._created_coordinators == {"alpha": coordinator_for_queue_a} + + coordinator_for_queue_missing = manager.for_queue("queue-1") + assert isinstance(coordinator_for_queue_missing, _PythonCoordinator) + assert manager.for_queue("queue-1") is coordinator_for_queue_missing + assert manager._created_coordinators == {"alpha": coordinator_for_queue_a} def test_from_config_empty(self, monkeypatch): monkeypatch.delenv("AIRFLOW__SDK__COORDINATORS", raising=False) monkeypatch.delenv("AIRFLOW__SDK__QUEUE_TO_COORDINATOR", raising=False) - - from airflow.sdk.configuration import conf - conf.invalidate_cache() manager = CoordinatorManager.from_config() + assert manager._coordinator_specs == {} assert manager._queue_to_coordinator == {} - def test_for_queue_resolves_via_mapping(self): - coordinator_a = _CoordinatorA() - coordinator_b = _CoordinatorB() - manager = CoordinatorManager({"queue-a": coordinator_a, "queue-b": coordinator_b}) - - assert manager.for_queue("queue-a") is coordinator_a - assert manager.for_queue("queue-b") is coordinator_b - assert isinstance(manager.for_queue("queue-missing"), _PythonCoordinator) - def test_get_coordinator_manager_is_cached(self, monkeypatch): monkeypatch.delenv("AIRFLOW__SDK__COORDINATORS", raising=False) From 24932d0b33be1189f8a6064b8188553e50581c31 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 19 May 2026 19:07:02 +0800 Subject: [PATCH 57/75] Redo JAR lookup A JavaCoordinator can now accept more than one path to look for JARs. This path is also used to populate --class-path when executing the task, so you can now split dependencies and the task-containing JAR into different locations. This should make deployment a bit easier. The already-unused BundleScanner class has been removed. This was for Java-based DAGs, and not used in the current specification. --- .../sdk/coordinators/java/bundle_scanner.py | 218 ------------ .../sdk/coordinators/java/coordinator.py | 52 +-- .../coordinators/java/test_bundle_scanner.py | 332 ------------------ .../coordinators/java/test_coordinator.py | 100 +++--- 4 files changed, 82 insertions(+), 620 deletions(-) delete mode 100644 task-sdk/src/airflow/sdk/coordinators/java/bundle_scanner.py delete mode 100644 task-sdk/tests/task_sdk/coordinators/java/test_bundle_scanner.py diff --git a/task-sdk/src/airflow/sdk/coordinators/java/bundle_scanner.py b/task-sdk/src/airflow/sdk/coordinators/java/bundle_scanner.py deleted file mode 100644 index 9e2c1c1ab46fc..0000000000000 --- a/task-sdk/src/airflow/sdk/coordinators/java/bundle_scanner.py +++ /dev/null @@ -1,218 +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. -""" -Scan directories for Airflow Java SDK bundle JARs. - -Mirrors the Java SDK's ``BundleScanner`` -- checks each JAR's manifest for -``Airflow-Java-SDK-Metadata``, reads the embedded metadata YAML, and -resolves the main class and classpath needed to launch the bundle process. -""" - -from __future__ import annotations - -import email -import os -import zipfile -from pathlib import Path -from typing import NamedTuple - -import yaml - -MANIFEST_PATH = "META-INF/MANIFEST.MF" -METADATA_MANIFEST_KEY = "Airflow-Java-SDK-Metadata" -SDK_VERSION_MANIFEST_KEY = "Airflow-Java-SDK-Version" -DAG_CODE_MANIFEST_KEY = "Airflow-Java-SDK-Dag-Code" -MAIN_CLASS_MANIFEST_KEY = "Main-Class" - - -class ResolvedJarBundle(NamedTuple): - """A resolved Java DAG bundle: everything needed to start the bundle process.""" - - main_class: str - classpath: str - - -class BundleScanner: - """ - Locate Airflow Java SDK bundles inside a directory tree. - - Supports two directory layouts: - - - **Nested** -- each immediate subdirectory of *bundles_dir* is a bundle home. - - **Flat** -- *bundles_dir* itself contains the bundle JARs. - - Within a bundle home the JVM convention of a ``lib/`` subdirectory for - dependency JARs is respected automatically. - """ - - def __init__(self, bundles_dir: Path) -> None: - self._bundles_dir = bundles_dir - - def resolve(self, dag_id: str) -> ResolvedJarBundle: - """ - Find the bundle whose metadata YAML lists *dag_id*. - - :raises FileNotFoundError: if no matching bundle is found. - """ - for bundle_home in self._candidate_homes(): - jars = _jar_files(bundle_home) - if not jars: - continue - - for jar_path in jars: - result = _read_bundle_jar(jar_path) - if result is None: - continue - main_class, dag_ids = result - if dag_id in dag_ids: - classpath = os.pathsep.join(str(j.resolve()) for j in jars) - return ResolvedJarBundle(main_class=main_class, classpath=classpath) - - raise FileNotFoundError(f"No JAR bundle containing dag_id={dag_id!r} found in {self._bundles_dir}") - - @staticmethod - def resolve_jar(jar_path: Path) -> str: - """ - Read ``Main-Class`` from a single bundle JAR, validating SDK attributes. - - :raises FileNotFoundError: if the JAR is not a valid Airflow Java SDK bundle. - """ - result = _read_bundle_jar(jar_path) - if result is None: - raise FileNotFoundError( - f"Not a valid Airflow Java SDK bundle: {jar_path} " - f"(requires {METADATA_MANIFEST_KEY} and {MAIN_CLASS_MANIFEST_KEY})" - ) - return result[0] - - def _candidate_homes(self) -> list[Path]: - """Return normalised bundle-home directories to inspect.""" - candidates: list[Path] = [] - - if self._bundles_dir.is_dir(): - for child in sorted(self._bundles_dir.iterdir()): - if child.is_dir(): - candidates.append(_normalize_bundle_home(child)) - - candidates.append(_normalize_bundle_home(self._bundles_dir)) - return candidates - - -def _jar_files(directory: Path) -> list[Path]: - """List all ``.jar`` files in *directory*, sorted by name.""" - if not directory.is_dir(): - return [] - return sorted(p for p in directory.iterdir() if p.is_file() and p.suffix == ".jar") - - -def _normalize_bundle_home(path: Path) -> Path: - """ - Normalize a bundle path to the directory containing JARs. - - Handles the common JVM distribution layout where dependency JARs - live in a ``lib/`` subdirectory (Gradle ``application`` plugin, - Maven Assembly, sbt-native-packager, etc.). - - - If *path* points to a JAR file, use its parent directory. - - If the directory has a ``lib/`` subdirectory containing JARs, use that. - - Otherwise, return the directory as-is. - """ - normalized = path.resolve() - if normalized.is_file() and normalized.suffix == ".jar": - return normalized.parent - lib = normalized / "lib" - if lib.is_dir() and any(p.suffix == ".jar" for p in lib.iterdir()): - return lib - return normalized - - -def _read_bundle_jar(jar_path: Path) -> tuple[str, set[str]] | None: - """ - Read ``Main-Class`` and dag IDs from a JAR's manifest and embedded metadata. - - Returns ``(main_class, dag_ids)`` when the JAR carries valid - ``Airflow-Java-SDK-Metadata`` and ``Main-Class`` manifest attributes - and the referenced metadata YAML contains at least one dag ID. - Returns ``None`` otherwise. - """ - try: - with zipfile.ZipFile(jar_path) as zf: - try: - with zf.open(MANIFEST_PATH) as f: - manifest = email.message_from_binary_file(f) - except KeyError: - return None - - metadata_file = manifest.get(METADATA_MANIFEST_KEY) - if not metadata_file: - return None - - main_class = manifest.get(MAIN_CLASS_MANIFEST_KEY) - if not main_class: - return None - - try: - with zf.open(metadata_file) as f: - content = f.read().decode() - except KeyError: - return None - except zipfile.BadZipFile: - return None - - dag_ids = _parse_dag_ids_from_metadata(content) - if not dag_ids: - return None - - return main_class, dag_ids - - -def read_dag_code(jar_path: Path) -> str | None: - """ - Read the DAG source code embedded in a JAR bundle. - - Returns the source code string when the JAR carries a valid - ``Airflow-Java-SDK-Dag-Code`` manifest attribute pointing to an - embedded source file. Returns ``None`` otherwise. - """ - try: - with zipfile.ZipFile(jar_path) as zf: - try: - with zf.open(MANIFEST_PATH) as f: - manifest = email.message_from_binary_file(f) - except KeyError: - return None - - dag_code_path = manifest.get(DAG_CODE_MANIFEST_KEY) - if not dag_code_path: - return None - - try: - with zf.open(dag_code_path) as f: - return f.read().decode() - except KeyError: - return None - except zipfile.BadZipFile: - return None - - -def _parse_dag_ids_from_metadata(yaml_content: str) -> set[str]: - """Parse dag IDs from an ``airflow-metadata.yaml`` content string.""" - data = yaml.safe_load(yaml_content) - if not isinstance(data, dict) or "dags" not in data: - return set() - return set(data["dags"].keys()) diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py index 0c752f06ed3f3..b7a56559e4ef1 100644 --- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py +++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py @@ -37,6 +37,8 @@ from airflow.sdk.execution_time.supervisor import ActivitySubprocess if TYPE_CHECKING: + from collections.abc import Sequence + from structlog.typing import FilteringBoundLogger from typing_extensions import Self @@ -55,20 +57,22 @@ def _start_server() -> socket.socket: return server -def _calculate_classpath(app_home: pathlib.Path) -> str: - jars = (p.as_posix() for p in app_home.iterdir() if p.suffix == ".jar") +def _calculate_classpath(jars_root: Sequence[pathlib.Path]) -> str: + jars = (p.as_posix() for root in jars_root for p in root.iterdir() if p.suffix == ".jar") return os.pathsep.join(jars) -def _find_main_class(app_home: pathlib.Path) -> str: - for p in app_home.iterdir(): - if p.suffix != ".jar": - continue - with zipfile.ZipFile(p) as zf: - with zf.open("META-INF/MANIFEST.MF") as f: - if main_class := email.message_from_binary_file(f)["Main-Class"]: - return main_class - raise FileNotFoundError(f"cannot fine main class in {app_home.resolve()}") +def _find_main_class(jars_root: Sequence[pathlib.Path]) -> str: + for root in jars_root: + for p in root.iterdir(): + if p.suffix != ".jar": + continue + with zipfile.ZipFile(p) as zf: + with zf.open("META-INF/MANIFEST.MF") as f: + if main_class := email.message_from_binary_file(f)["Main-Class"]: + return main_class + resolved_paths = os.pathsep.join(str(p.resolve()) for p in jars_root) + raise FileNotFoundError(f"cannot fine main class in {resolved_paths}") def _accept_connections( @@ -122,7 +126,7 @@ def start( # type: ignore[override] sentry_integration: str = "", java_executable: str, jvm_args: list[str], - bundles_folder: pathlib.Path, + jars_root: Sequence[pathlib.Path], **kwargs, ) -> Self: comm_server = _start_server() @@ -138,9 +142,9 @@ def start( # type: ignore[override] [ java_executable, "-classpath", - _calculate_classpath(bundles_folder), + _calculate_classpath(jars_root), *jvm_args, - _find_main_class(bundles_folder), + _find_main_class(jars_root), # Arguments to MainClass... f"--comm={comm_host}:{comm_port}", f"--logs={logs_host}:{logs_port}", @@ -180,6 +184,16 @@ def wait(self) -> int: return code +def _convert_jars_root( + value: None | os.PathLike[str] | pathlib.Path | list[os.PathLike[str] | pathlib.Path], +) -> list[pathlib.Path]: + if value is None: + return [] + if isinstance(value, (str, os.PathLike, pathlib.Path)): + return [pathlib.Path(value)] + return [pathlib.Path(v) for v in value] + + @attrs.define(kw_only=True) class JavaCoordinator(BaseCoordinator): """ @@ -194,21 +208,19 @@ class JavaCoordinator(BaseCoordinator): "kwargs": { "java_executable": "/usr/lib/jvm/java-17-openjdk/bin/java", "jvm_args": ["-Xmx1024m"], - "bundles_folder": "~/airflow/java-bundles", + "jars_root": ["~/airflow/jars"], }, } :param java_executable: Path to the ``java`` binary (defaults to ``"java"``, which relies on ``$PATH``). :param jvm_args: Extra arguments passed to the JVM (e.g. ``["-Xmx512m"]``). - :param bundles_folder: Directory scanned for JAR bundles when a Python - stub DAG delegates task execution to Java. Required for the stub-DAG - flow; unused for pure-Java DAGs. + :param jars_root: A list of directories scanned for JAR bundles. """ java_executable: str = "java" jvm_args: list[str] = attrs.field(factory=list) - bundles_folder: pathlib.Path = attrs.field(converter=pathlib.Path) + jars_root: list[pathlib.Path] = attrs.field(converter=_convert_jars_root, factory=list) def execute_task( self, @@ -232,7 +244,7 @@ def execute_task( sentry_integration=sentry_integration, java_executable=self.java_executable, jvm_args=self.jvm_args, - bundles_folder=self.bundles_folder, + jars_root=self.jars_root, ) exit_code = process.wait() return self.ExecutionResult(exit_code, process.final_state) diff --git a/task-sdk/tests/task_sdk/coordinators/java/test_bundle_scanner.py b/task-sdk/tests/task_sdk/coordinators/java/test_bundle_scanner.py deleted file mode 100644 index 93457aa1a9755..0000000000000 --- a/task-sdk/tests/task_sdk/coordinators/java/test_bundle_scanner.py +++ /dev/null @@ -1,332 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from __future__ import annotations - -import os -import zipfile -from pathlib import Path - -import pytest -import yaml - -from airflow.sdk.coordinators.java.bundle_scanner import ( - DAG_CODE_MANIFEST_KEY, - MAIN_CLASS_MANIFEST_KEY, - MANIFEST_PATH, - METADATA_MANIFEST_KEY, - SDK_VERSION_MANIFEST_KEY, - BundleScanner, - ResolvedJarBundle, - _jar_files, - _normalize_bundle_home, - _parse_dag_ids_from_metadata, - _read_bundle_jar, - read_dag_code, -) - -METADATA_YAML_PATH = "META-INF/airflow-metadata.yaml" -DAG_CODE_PATH = "JavaExample.java" -TEST_MAIN_CLASS = "com.example.MyDag" -TEST_SDK_VERSION = "1.0.0" - - -def _make_manifest( - *, - main_class: str | None = TEST_MAIN_CLASS, - metadata_path: str | None = METADATA_YAML_PATH, - sdk_version: str | None = TEST_SDK_VERSION, - dag_code_path: str | None = None, -) -> str: - lines = ["Manifest-Version: 1.0"] - if main_class: - lines.append(f"{MAIN_CLASS_MANIFEST_KEY}: {main_class}") - if metadata_path: - lines.append(f"{METADATA_MANIFEST_KEY}: {metadata_path}") - if sdk_version: - lines.append(f"{SDK_VERSION_MANIFEST_KEY}: {sdk_version}") - if dag_code_path: - lines.append(f"{DAG_CODE_MANIFEST_KEY}: {dag_code_path}") - return "\n".join(lines) + "\n" - - -def _make_metadata_yaml(dag_ids: list[str]) -> str: - return yaml.dump({"dags": {dag_id: {} for dag_id in dag_ids}}) - - -def _create_bundle_jar( - jar_path: Path, - *, - dag_ids: list[str] | None = None, - main_class: str | None = TEST_MAIN_CLASS, - include_metadata: bool = True, - include_manifest: bool = True, - dag_code: str | None = None, -) -> Path: - """Create a minimal JAR (zip) file with Airflow Java SDK manifest attributes.""" - with zipfile.ZipFile(jar_path, "w") as zf: - if include_manifest: - dag_code_path = DAG_CODE_PATH if dag_code else None - manifest = _make_manifest( - main_class=main_class, - metadata_path=METADATA_YAML_PATH if include_metadata else None, - dag_code_path=dag_code_path, - ) - zf.writestr(MANIFEST_PATH, manifest) - - if include_metadata and dag_ids is not None: - zf.writestr(METADATA_YAML_PATH, _make_metadata_yaml(dag_ids)) - - if dag_code: - zf.writestr(DAG_CODE_PATH, dag_code) - return jar_path - - -class TestJarFiles: - def test_lists_jar_files_sorted(self, tmp_path: Path): - (tmp_path / "b.jar").touch() - (tmp_path / "a.jar").touch() - (tmp_path / "c.txt").touch() - result = _jar_files(tmp_path) - assert result == [tmp_path / "a.jar", tmp_path / "b.jar"] - - def test_returns_empty_for_nonexistent_directory(self, tmp_path: Path): - assert _jar_files(tmp_path / "nonexistent") == [] - - def test_returns_empty_for_directory_with_no_jars(self, tmp_path: Path): - (tmp_path / "readme.txt").touch() - assert _jar_files(tmp_path) == [] - - def test_ignores_jar_directories(self, tmp_path: Path): - (tmp_path / "fake.jar").mkdir() - assert _jar_files(tmp_path) == [] - - -class TestNormalizeBundleHome: - def test_jar_file_returns_parent(self, tmp_path: Path): - jar = tmp_path / "bundle.jar" - jar.touch() - assert _normalize_bundle_home(jar) == tmp_path.resolve() - - def test_dir_with_lib_containing_jars(self, tmp_path: Path): - lib = tmp_path / "lib" - lib.mkdir() - (lib / "dep.jar").touch() - assert _normalize_bundle_home(tmp_path) == lib.resolve() - - def test_dir_with_empty_lib(self, tmp_path: Path): - lib = tmp_path / "lib" - lib.mkdir() - assert _normalize_bundle_home(tmp_path) == tmp_path.resolve() - - def test_plain_directory(self, tmp_path: Path): - assert _normalize_bundle_home(tmp_path) == tmp_path.resolve() - - -class TestParseDagIdsFromMetadata: - def test_parses_dag_ids(self): - content = yaml.dump({"dags": {"dag_a": {}, "dag_b": {"key": "val"}}}) - assert _parse_dag_ids_from_metadata(content) == {"dag_a", "dag_b"} - - @pytest.mark.parametrize( - "yaml_content", - [ - pytest.param(yaml.dump({"other": 1}), id="missing_dags_key"), - pytest.param("just a string", id="non_dict"), - pytest.param(yaml.dump({"dags": {}}), id="empty_dags"), - ], - ) - def test_returns_empty_set(self, yaml_content): - assert _parse_dag_ids_from_metadata(yaml_content) == set() - - -class TestReadBundleJar: - def test_valid_jar(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "valid.jar", dag_ids=["my_dag"]) - result = _read_bundle_jar(jar) - assert result is not None - main_class, dag_ids = result - assert main_class == TEST_MAIN_CLASS - assert dag_ids == {"my_dag"} - - def test_returns_none_for_missing_manifest(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "no_manifest.jar", include_manifest=False) - assert _read_bundle_jar(jar) is None - - def test_returns_none_for_missing_metadata_key(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "no_meta.jar", include_metadata=False) - assert _read_bundle_jar(jar) is None - - def test_returns_none_for_missing_main_class(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "no_main.jar", dag_ids=["d"], main_class=None) - assert _read_bundle_jar(jar) is None - - def test_returns_none_for_missing_metadata_file(self, tmp_path: Path): - """Manifest references a metadata file that does not exist inside the JAR.""" - jar = tmp_path / "missing_meta_file.jar" - with zipfile.ZipFile(jar, "w") as zf: - manifest = _make_manifest(metadata_path="nonexistent.yaml") - zf.writestr(MANIFEST_PATH, manifest) - assert _read_bundle_jar(jar) is None - - def test_returns_none_for_bad_zip(self, tmp_path: Path): - bad = tmp_path / "bad.jar" - bad.write_text("not a zip file") - assert _read_bundle_jar(bad) is None - - def test_returns_none_for_empty_dag_ids(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "empty_dags.jar", dag_ids=[]) - assert _read_bundle_jar(jar) is None - - def test_multiple_dag_ids(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "multi.jar", dag_ids=["dag_1", "dag_2", "dag_3"]) - result = _read_bundle_jar(jar) - assert result is not None - _, dag_ids = result - assert dag_ids == {"dag_1", "dag_2", "dag_3"} - - -class TestReadDagCode: - def test_reads_embedded_dag_code(self, tmp_path: Path): - code = "public class MyDag {}" - jar = _create_bundle_jar(tmp_path / "with_code.jar", dag_ids=["d"], dag_code=code) - assert read_dag_code(jar) == code - - def test_returns_none_for_missing_dag_code_key(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "no_code.jar", dag_ids=["d"]) - assert read_dag_code(jar) is None - - def test_returns_none_for_missing_manifest(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "no_manifest.jar", include_manifest=False) - assert read_dag_code(jar) is None - - def test_returns_none_for_bad_zip(self, tmp_path: Path): - bad = tmp_path / "bad.jar" - bad.write_text("not a zip") - assert read_dag_code(bad) is None - - def test_returns_none_when_code_file_missing(self, tmp_path: Path): - """Manifest references a dag code file that does not exist inside the JAR.""" - jar = tmp_path / "broken_code.jar" - with zipfile.ZipFile(jar, "w") as zf: - manifest = _make_manifest(dag_code_path="missing_source.py") - zf.writestr(MANIFEST_PATH, manifest) - assert read_dag_code(jar) is None - - -class TestBundleScannerResolveJar: - def test_returns_main_class(self, tmp_path: Path): - jar = _create_bundle_jar(tmp_path / "bundle.jar", dag_ids=["d"]) - assert BundleScanner.resolve_jar(jar) == TEST_MAIN_CLASS - - def test_raises_for_invalid_jar(self, tmp_path: Path): - jar = tmp_path / "not_bundle.jar" - jar.write_text("not a zip") - with pytest.raises(FileNotFoundError, match="Not a valid Airflow Java SDK bundle"): - BundleScanner.resolve_jar(jar) - - -class TestBundleScannerCandidateHomes: - def test_nested_layout(self, tmp_path: Path): - sub_a = tmp_path / "bundle_a" - sub_a.mkdir() - (sub_a / "app.jar").touch() - - sub_b = tmp_path / "bundle_b" - sub_b.mkdir() - (sub_b / "app.jar").touch() - - scanner = BundleScanner(tmp_path) - homes = scanner._candidate_homes() - assert len(homes) == 3 - assert sub_a.resolve() in homes - assert sub_b.resolve() in homes - assert tmp_path.resolve() in homes - - def test_flat_layout(self, tmp_path: Path): - (tmp_path / "app.jar").touch() - scanner = BundleScanner(tmp_path) - homes = scanner._candidate_homes() - assert homes == [tmp_path.resolve()] - - def test_nested_with_lib_subdir(self, tmp_path: Path): - sub = tmp_path / "my_bundle" - sub.mkdir() - lib = sub / "lib" - lib.mkdir() - (lib / "dep.jar").touch() - - scanner = BundleScanner(tmp_path) - homes = scanner._candidate_homes() - assert lib.resolve() in homes - - -class TestBundleScannerResolve: - def test_finds_matching_dag(self, tmp_path: Path): - bundle_dir = tmp_path / "my_bundle" - bundle_dir.mkdir() - _create_bundle_jar(bundle_dir / "app.jar", dag_ids=["target_dag"]) - - scanner = BundleScanner(tmp_path) - result = scanner.resolve("target_dag") - assert isinstance(result, ResolvedJarBundle) - assert result.main_class == TEST_MAIN_CLASS - assert str((bundle_dir / "app.jar").resolve()) in result.classpath - - def test_raises_when_no_match(self, tmp_path: Path): - bundle_dir = tmp_path / "my_bundle" - bundle_dir.mkdir() - _create_bundle_jar(bundle_dir / "app.jar", dag_ids=["other_dag"]) - - scanner = BundleScanner(tmp_path) - with pytest.raises(FileNotFoundError, match="No JAR bundle containing dag_id='missing'"): - scanner.resolve("missing") - - def test_classpath_includes_all_jars(self, tmp_path: Path): - bundle_dir = tmp_path / "my_bundle" - bundle_dir.mkdir() - _create_bundle_jar(bundle_dir / "app.jar", dag_ids=["my_dag"]) - with zipfile.ZipFile(bundle_dir / "dep.jar", "w") as zf: - zf.writestr("placeholder.class", b"") - - scanner = BundleScanner(tmp_path) - result = scanner.resolve("my_dag") - parts = result.classpath.split(os.pathsep) - assert len(parts) == 2 - - def test_flat_layout_resolve(self, tmp_path: Path): - _create_bundle_jar(tmp_path / "app.jar", dag_ids=["flat_dag"]) - - scanner = BundleScanner(tmp_path) - result = scanner.resolve("flat_dag") - assert result.main_class == TEST_MAIN_CLASS - - def test_skips_non_bundle_jars(self, tmp_path: Path): - bundle_dir = tmp_path / "my_bundle" - bundle_dir.mkdir() - with zipfile.ZipFile(bundle_dir / "plain.jar", "w") as zf: - zf.writestr("placeholder.class", b"") - _create_bundle_jar(bundle_dir / "real.jar", dag_ids=["real_dag"]) - - scanner = BundleScanner(tmp_path) - result = scanner.resolve("real_dag") - assert result.main_class == TEST_MAIN_CLASS - - def test_empty_bundles_dir(self, tmp_path: Path): - scanner = BundleScanner(tmp_path) - with pytest.raises(FileNotFoundError): - scanner.resolve("any_dag") diff --git a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py index d1c1761bfd4c8..ca435cfd826f8 100644 --- a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py +++ b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py @@ -123,14 +123,14 @@ class TestCalculateClasspath: def test_single_jar(self, tmp_path): jar = tmp_path.joinpath("app.jar") jar.write_bytes(b"") - result = _calculate_classpath(tmp_path) + result = _calculate_classpath([tmp_path]) assert result == jar.as_posix() def test_multiple_jars_all_included(self, tmp_path): tmp_path.joinpath("a.jar").write_bytes(b"") tmp_path.joinpath("b.jar").write_bytes(b"") tmp_path.joinpath("c.jar").write_bytes(b"") - result = _calculate_classpath(tmp_path) + result = _calculate_classpath([tmp_path]) entries = set(result.split(os.pathsep)) assert entries == { tmp_path.joinpath("a.jar").as_posix(), @@ -143,42 +143,42 @@ def test_non_jar_files_excluded(self, tmp_path): jar.write_bytes(b"") tmp_path.joinpath("readme.txt").write_bytes(b"") tmp_path.joinpath("config.yaml").write_bytes(b"") - result = _calculate_classpath(tmp_path) + result = _calculate_classpath([tmp_path]) assert result == jar.as_posix() def test_empty_directory_returns_empty_string(self, tmp_path): - result = _calculate_classpath(tmp_path) + result = _calculate_classpath([tmp_path]) assert result == "" class TestFindMainClass: def test_returns_main_class_from_jar(self, tmp_path): _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main") - assert _find_main_class(tmp_path) == "com.example.Main" + assert _find_main_class([tmp_path]) == "com.example.Main" def test_no_jars_raises_file_not_found(self, tmp_path): with pytest.raises(FileNotFoundError, match=re.escape(str(tmp_path.resolve()))): - _find_main_class(tmp_path) + _find_main_class([tmp_path]) def test_jar_without_main_class_not_returned(self, tmp_path): _make_jar(tmp_path.joinpath("app.jar"), main_class=None) with pytest.raises(FileNotFoundError): - _find_main_class(tmp_path) + _find_main_class([tmp_path]) def test_non_jar_files_skipped(self, tmp_path): tmp_path.joinpath("readme.txt").write_bytes(b"not a jar") _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main") - assert _find_main_class(tmp_path) == "com.example.Main" + assert _find_main_class([tmp_path]) == "com.example.Main" def test_first_jar_missing_main_class_falls_through_to_second(self, tmp_path): # Alphabetically: a.jar (no Main-Class), b.jar (has Main-Class). _make_jar(tmp_path.joinpath("a.jar"), main_class=None) _make_jar(tmp_path.joinpath("b.jar"), main_class="com.example.Fallback") - assert _find_main_class(tmp_path) == "com.example.Fallback" + assert _find_main_class([tmp_path]) == "com.example.Fallback" def test_fully_qualified_class_name_preserved(self, tmp_path): _make_jar(tmp_path.joinpath("app.jar"), main_class="org.apache.airflow.sdk.java.TaskRunner") - assert _find_main_class(tmp_path) == "org.apache.airflow.sdk.java.TaskRunner" + assert _find_main_class([tmp_path]) == "org.apache.airflow.sdk.java.TaskRunner" class TestAcceptConnections: @@ -272,24 +272,24 @@ def test_returned_sockets_are_connected(self): class TestJavaCoordinatorAttributes: def test_default_kwargs(self): - coordinator = JavaCoordinator(bundles_folder="/airflow/java-bundles") + coordinator = JavaCoordinator(jars_root="/airflow/java-bundles") assert coordinator.java_executable == "java" assert coordinator.jvm_args == [] - assert coordinator.bundles_folder == pathlib.Path("/airflow/java-bundles") + assert coordinator.jars_root == [pathlib.Path("/airflow/java-bundles")] def test_custom_kwargs(self): coordinator = JavaCoordinator( java_executable="/opt/java/bin/java", jvm_args=["-Xmx512m", "-Xms256m"], - bundles_folder="/airflow/java-bundles", + jars_root=["/airflow/java-bundles"], ) assert coordinator.java_executable == "/opt/java/bin/java" assert coordinator.jvm_args == ["-Xmx512m", "-Xms256m"] - assert coordinator.bundles_folder == pathlib.Path("/airflow/java-bundles") + assert coordinator.jars_root == [pathlib.Path("/airflow/java-bundles")] @pytest.fixture -def bundle_dir(tmp_path): +def jars_root(tmp_path): _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.TaskRunner") return tmp_path @@ -304,7 +304,7 @@ def mock_client(make_ti_context): class TestJavaCoordinatorExecuteTask: def _captured_popen_cmd( self, - bundle_dir: pathlib.Path, + jars_root: pathlib.Path, mock_client, *, java_executable: str = "java", @@ -315,7 +315,7 @@ def _captured_popen_cmd( coordinator = JavaCoordinator( java_executable=java_executable, jvm_args=jvm_args or [], - bundles_folder=bundle_dir, + jars_root=jars_root, ) mock_proc = MagicMock(spec=subprocess.Popen) @@ -353,56 +353,56 @@ def capture_popen(cmd, **kwargs): assert popen_calls, "subprocess.Popen was not called" return popen_calls[0] - def test_java_executable_is_first_arg(self, bundle_dir, mock_client): + def test_java_executable_is_first_arg(self, jars_root, mock_client): cmd = self._captured_popen_cmd( - bundle_dir, mock_client, java_executable="/usr/lib/jvm/java-17/bin/java" + jars_root, mock_client, java_executable="/usr/lib/jvm/java-17/bin/java" ) assert cmd[0] == "/usr/lib/jvm/java-17/bin/java" - def test_classpath_flag_and_value_present(self, bundle_dir, mock_client): - cmd = self._captured_popen_cmd(bundle_dir, mock_client) + def test_classpath_flag_and_value_present(self, jars_root, mock_client): + cmd = self._captured_popen_cmd(jars_root, mock_client) assert "-classpath" in cmd cp_idx = cmd.index("-classpath") classpath = cmd[cp_idx + 1] - assert bundle_dir.joinpath("app.jar").as_posix() in classpath + assert jars_root.joinpath("app.jar").as_posix() in classpath - def test_main_class_present(self, bundle_dir, mock_client): - cmd = self._captured_popen_cmd(bundle_dir, mock_client) + def test_main_class_present(self, jars_root, mock_client): + cmd = self._captured_popen_cmd(jars_root, mock_client) assert "com.example.TaskRunner" in cmd - def test_comm_and_logs_args_present(self, bundle_dir, mock_client): - cmd = self._captured_popen_cmd(bundle_dir, mock_client) + def test_comm_and_logs_args_present(self, jars_root, mock_client): + cmd = self._captured_popen_cmd(jars_root, mock_client) comm_args = [a for a in cmd if a.startswith("--comm=")] logs_args = [a for a in cmd if a.startswith("--logs=")] assert len(comm_args) == 1 assert len(logs_args) == 1 - def test_comm_and_logs_contain_port(self, bundle_dir, mock_client): - cmd = self._captured_popen_cmd(bundle_dir, mock_client) + def test_comm_and_logs_contain_port(self, jars_root, mock_client): + cmd = self._captured_popen_cmd(jars_root, mock_client) comm_arg = next(a for a in cmd if a.startswith("--comm=")) logs_arg = next(a for a in cmd if a.startswith("--logs=")) # format is host:port assert ":" in comm_arg.split("=", 1)[1] assert ":" in logs_arg.split("=", 1)[1] - def test_jvm_args_inserted_before_main_class(self, bundle_dir, mock_client): - cmd = self._captured_popen_cmd(bundle_dir, mock_client, jvm_args=["-Xmx512m", "-Dsome.prop=value"]) + def test_jvm_args_inserted_before_main_class(self, jars_root, mock_client): + cmd = self._captured_popen_cmd(jars_root, mock_client, jvm_args=["-Xmx512m", "-Dsome.prop=value"]) main_idx = cmd.index("com.example.TaskRunner") for jvm_arg in ["-Xmx512m", "-Dsome.prop=value"]: assert jvm_arg in cmd assert cmd.index(jvm_arg) < main_idx - def test_comm_and_logs_after_main_class(self, bundle_dir, mock_client): - cmd = self._captured_popen_cmd(bundle_dir, mock_client) + def test_comm_and_logs_after_main_class(self, jars_root, mock_client): + cmd = self._captured_popen_cmd(jars_root, mock_client) main_idx = cmd.index("com.example.TaskRunner") comm_idx = next(i for i, a in enumerate(cmd) if a.startswith("--comm=")) logs_idx = next(i for i, a in enumerate(cmd) if a.startswith("--logs=")) assert comm_idx > main_idx assert logs_idx > main_idx - def test_returns_execution_result(self, bundle_dir, mock_client): + def test_returns_execution_result(self, jars_root, mock_client): ti = _make_ti() - coordinator = JavaCoordinator(bundles_folder=bundle_dir) + coordinator = JavaCoordinator(jars_root=jars_root) mock_proc = MagicMock(spec=subprocess.Popen) mock_proc.pid = 99999 @@ -443,7 +443,7 @@ class TestJavaActivitySubprocessStart: def _start_with_mocks( self, - bundle_dir: pathlib.Path, + jars_root: pathlib.Path, mock_client, *, java_executable: str = "java", @@ -478,26 +478,26 @@ def _start_with_mocks( client=mock_client, java_executable=java_executable, jvm_args=jvm_args or [], - bundles_folder=bundle_dir, + jars_root=[jars_root], subprocess_logs_to_stdout=False, ) return proc, popen_mock - def test_stdout_write_socket_stored_for_cleanup(self, bundle_dir, mock_client): - proc, _ = self._start_with_mocks(bundle_dir, mock_client) + def test_stdout_write_socket_stored_for_cleanup(self, jars_root, mock_client): + proc, _ = self._start_with_mocks(jars_root, mock_client) # _stdout_w must be stored so wait() can close it assert proc._stdout_w is not None - def test_stderr_write_socket_stored_for_cleanup(self, bundle_dir, mock_client): - proc, _ = self._start_with_mocks(bundle_dir, mock_client) + def test_stderr_write_socket_stored_for_cleanup(self, jars_root, mock_client): + proc, _ = self._start_with_mocks(jars_root, mock_client) assert proc._stderr_w is not None - def test_stdout_and_stderr_write_sockets_are_distinct(self, bundle_dir, mock_client): - proc, _ = self._start_with_mocks(bundle_dir, mock_client) + def test_stdout_and_stderr_write_sockets_are_distinct(self, jars_root, mock_client): + proc, _ = self._start_with_mocks(jars_root, mock_client) assert proc._stdout_w is not proc._stderr_w - def test_stdin_is_comm_socket(self, bundle_dir, mock_client): + def test_stdin_is_comm_socket(self, jars_root, mock_client): """stdin (used by send_msg) must be the accepted comm socket.""" ti = _make_ti() comm_sock = MagicMock(spec=socket.socket) @@ -521,17 +521,17 @@ def test_stdin_is_comm_socket(self, bundle_dir, mock_client): client=MagicMock(), java_executable="java", jvm_args=[], - bundles_folder=bundle_dir, + jars_root=[jars_root], subprocess_logs_to_stdout=False, ) assert proc.stdin is comm_sock - def test_pid_taken_from_popen(self, bundle_dir, mock_client): - proc, _ = self._start_with_mocks(bundle_dir, mock_client) + def test_pid_taken_from_popen(self, jars_root, mock_client): + proc, _ = self._start_with_mocks(jars_root, mock_client) assert proc.pid == 12345 - def test_on_child_started_called(self, bundle_dir, mock_client): + def test_on_child_started_called(self, jars_root, mock_client): ti = _make_ti() with ( patch("airflow.sdk.coordinators.java.coordinator.subprocess.Popen") as popen_mock, @@ -551,7 +551,7 @@ def test_on_child_started_called(self, bundle_dir, mock_client): client=mock_client, java_executable="java", jvm_args=[], - bundles_folder=bundle_dir, + jars_root=[jars_root], subprocess_logs_to_stdout=False, ) @@ -560,7 +560,7 @@ def test_on_child_started_called(self, bundle_dir, mock_client): assert kwargs["ti"] is ti assert kwargs["dag_rel_path"] == "dags/test.jar" - def test_register_pipe_readers_called_with_four_sockets(self, bundle_dir, mock_client): + def test_register_pipe_readers_called_with_four_sockets(self, jars_root, mock_client): """Both socketpair read-ends and both TCP sockets must be registered.""" with ( patch("airflow.sdk.coordinators.java.coordinator.subprocess.Popen") as popen_mock, @@ -580,7 +580,7 @@ def test_register_pipe_readers_called_with_four_sockets(self, bundle_dir, mock_c client=mock_client, java_executable="java", jvm_args=[], - bundles_folder=bundle_dir, + jars_root=[jars_root], subprocess_logs_to_stdout=False, ) From 32cedb50ef5d58461aefa835b100aa3690a7dd8c Mon Sep 17 00:00:00 2001 From: "Jason(Zhe-You) Liu" <68415893+jason810496@users.noreply.github.com> Date: Wed, 20 May 2026 14:40:39 +0800 Subject: [PATCH 58/75] Drop selector_loop module (#1587) * Drop selector_loop module; new JavaCoordinator does not need it The rewritten ``JavaCoordinator`` (``airflow.sdk.coordinators.java``) lets the JVM connect directly to two listening sockets (``comm`` and ``logs``) and uses the accepted sockets as the supervisor's ``stdin`` / log pipes straight up. There is no bytes-bridge between two sockets, so ``make_raw_forwarder`` -- the only helper the extracted ``selector_loop`` module added beyond what was already inline in ``supervisor.py`` -- has no caller. ``_JavaActivitySubprocess`` reuses ``_register_pipe_readers`` and ``_close_unused_sockets`` via subclassing ``ActivitySubprocess``; both methods existed before the extraction and remain in ``supervisor.py`` after this revert. The inline selector dispatch loop and ``make_buffered_socket_reader`` come back into ``supervisor.py`` so the existing call sites (including the ``triggerer_job_runner`` re-export) keep working unchanged. This reverts commit 56464a8d45a96c8d15639784c0ca40367d22aae2 ("Add common selector loop utilities for socket I/O handling for subprocesses") and deletes ``test_selector_loop.py`` introduced by 4b80753e16. * Address copilot's comments --- .../src/airflow/dag_processing/processor.py | 5 +- .../airflow/sdk/execution_time/coordinator.py | 4 - .../sdk/execution_time/selector_loop.py | 159 ------ .../airflow/sdk/execution_time/supervisor.py | 79 ++- .../execution_time/test_selector_loop.py | 479 ------------------ 5 files changed, 77 insertions(+), 649 deletions(-) delete mode 100644 task-sdk/src/airflow/sdk/execution_time/selector_loop.py delete mode 100644 task-sdk/tests/task_sdk/execution_time/test_selector_loop.py diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index 4974517ca3b90..303b62d141111 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -93,6 +93,8 @@ from airflow.utils.state import TaskInstanceState if TYPE_CHECKING: + from socket import socket + from structlog.typing import FilteringBoundLogger from airflow.api_fastapi.execution_api.app import InProcessExecutionAPI @@ -101,7 +103,6 @@ from airflow.sdk.definitions.context import Context from airflow.sdk.definitions.dag import DAG from airflow.sdk.definitions.mappedoperator import MappedOperator - from airflow.sdk.execution_time.supervisor import SelectorCallback from airflow.typing_compat import Self @@ -609,7 +610,7 @@ def _get_target_loggers(self) -> tuple[FilteringBoundLogger, ...]: def _create_log_forwarder( self, loggers: tuple[FilteringBoundLogger, ...], name: str, log_level: int = logging.INFO - ) -> SelectorCallback: + ) -> Callable[[socket], bool]: return super()._create_log_forwarder(loggers, name.replace("task.", "dag_processor.", 1), log_level) def _handle_request(self, msg: ToManager, log: FilteringBoundLogger, req_id: int) -> None: diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index cf0dd4e920f0e..f6aa453f06c61 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -34,10 +34,6 @@ 4. Runs a selector-based bridge that transparently forwards bytes between fd 0 (supervisor) and the subprocess comm socket, and re-emits the subprocess's log and stderr output through structlog. - -I/O multiplexing uses the same selector-based loop as -:class:`~airflow.sdk.execution_time.supervisor.WatchedSubprocess`, -driven by :func:`~airflow.sdk.execution_time.selector_loop.service_selector`. """ from __future__ import annotations diff --git a/task-sdk/src/airflow/sdk/execution_time/selector_loop.py b/task-sdk/src/airflow/sdk/execution_time/selector_loop.py deleted file mode 100644 index d67014ad1b418..0000000000000 --- a/task-sdk/src/airflow/sdk/execution_time/selector_loop.py +++ /dev/null @@ -1,159 +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. -""" -Selector-based I/O loop utilities shared across subprocess monitors. - -Both :class:`~airflow.sdk.execution_time.supervisor.WatchedSubprocess` -(supervisor-side) and provider-registered bridges such as the Locale DagFileProcessor (child-side) use these building blocks to multiplex -socket I/O without threads. - -The common contract for every callback registered with the selector: - -* The selector stores a ``(handler, on_close)`` tuple as ``key.data``. -* ``handler(fileobj) -> bool`` — read available data and return - ``True`` to keep listening, ``False`` on EOF / error. -* ``on_close(fileobj)`` — called when the handler returns ``False``; - must unregister the fileobj from the selector. -* :func:`service_selector` drives one iteration of this protocol. -""" - -from __future__ import annotations - -import selectors -from contextlib import suppress -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Callable, Generator - from socket import socket - - # (handler, on_close) — stored as ``selector.register(..., data=cb)`` - SelectorCallback = tuple[Callable[[socket], bool], Callable[[socket], None]] - - -# Sockets, even the `.makefile()` function don't correctly do line buffering on reading. If a chunk is read -# and it doesn't contain a new line character, `.readline()` will just return the chunk as is. -# -# This returns a callback suitable for attaching to a `selector` that reads in to a buffer, and yields lines -# to a (sync) generator -def make_buffered_socket_reader( - gen: Generator[None, bytes | bytearray, None], - on_close: Callable[[socket], None], - buffer_size: int = 4096, -) -> SelectorCallback: - """ - Create a selector callback that line-buffers socket data into a generator. - - Bytes are accumulated until a newline is found; each - complete line is sent to *gen* via ``gen.send(line)``. On EOF the - remainder of the buffer (if any) is flushed. - - Returns a ``(handler, on_close)`` tuple suitable for - ``selector.register(..., data=...)``. - """ - buffer = bytearray() # This will hold our accumulated binary data - read_buffer = bytearray(buffer_size) # Temporary buffer for each read - - # We need to start up the generator to get it to the point it's at waiting on the yield - next(gen) - - def cb(sock: socket): - nonlocal buffer, read_buffer - # Read up to `buffer_size` bytes of data from the socket - n_received = sock.recv_into(read_buffer) - - if not n_received: - # If no data is returned, the connection is closed. Return whatever is left in the buffer - if len(buffer): - with suppress(StopIteration): - gen.send(buffer) - return False - - buffer.extend(read_buffer[:n_received]) - - # We could have read multiple lines in one go, yield them all - while (newline_pos := buffer.find(b"\n")) != -1: - line = buffer[: newline_pos + 1] - try: - gen.send(line) - except StopIteration: - return False - buffer = buffer[newline_pos + 1 :] # Update the buffer with remaining data - - return True - - return cb, on_close - - -def make_raw_forwarder( - dest: socket, - on_close: Callable[[socket], None], -) -> SelectorCallback: - """ - Create a selector callback that forwards raw bytes to *dest*. - - Used for transparent protocol bridges where bytes must be shuttled - between two sockets without interpretation (e.g. length-prefixed - msgpack frames between a supervisor and a Java subprocess). - """ - - def cb(sock: socket) -> bool: - data = sock.recv(65536) - if not data: - return False - try: - dest.sendall(data) - except (BrokenPipeError, ConnectionResetError, OSError): - return False - return True - - return cb, on_close - - -def service_selector(selector: selectors.BaseSelector, timeout: float = 1.0) -> None: - """ - Process one round of selector events. - - For each ready socket whose handler returns ``False`` (EOF / error), - the socket's *on_close* callback is invoked and the socket is closed. - """ - # Ensure minimum timeout to prevent CPU spike with tight loop when timeout is 0 or negative - timeout = max(0.01, timeout) - events = selector.select(timeout=timeout) - for key, _ in events: - # Retrieve the handler responsible for processing this file object (e.g., stdout, stderr) - socket_handler, on_close = key.data - - # Example of handler behavior: - # If the subprocess writes "Hello, World!" to stdout: - # - `socket_handler` reads and processes the message. - # - If EOF is reached, the handler returns False to signal no more reads are expected. - # - BrokenPipeError should be caught and treated as if the handler returned false, similar - # to EOF case - try: - need_more = socket_handler(key.fileobj) - except (BrokenPipeError, ConnectionResetError): - need_more = False - - # If the handler signals that the file object is no longer needed (EOF, closed, etc.) - # unregister it from the selector to stop monitoring; `wait()` blocks until all selectors - # are removed. - if not need_more: - sock: socket = key.fileobj # type: ignore[assignment] - on_close(sock) - sock.close() diff --git a/task-sdk/src/airflow/sdk/execution_time/supervisor.py b/task-sdk/src/airflow/sdk/execution_time/supervisor.py index 2e9ef7ded7d29..c1993e3248ef2 100644 --- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py +++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py @@ -171,7 +171,6 @@ from airflow.executors.workloads import BundleInfo from airflow.sdk.bases.secrets_backend import BaseSecretsBackend from airflow.sdk.definitions.connection import Connection - from airflow.sdk.execution_time.selector_loop import SelectorCallback from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO from airflow.sdk.types import RuntimeTaskInstanceProtocol as RuntimeTI @@ -743,7 +742,7 @@ def _get_target_loggers(self) -> tuple[FilteringBoundLogger, ...]: target_loggers += (log,) return target_loggers - def _create_log_forwarder(self, loggers, name, log_level=logging.INFO) -> SelectorCallback: + def _create_log_forwarder(self, loggers, name, log_level=logging.INFO) -> Callable[[socket], bool]: """Create a socket handler that forwards logs to a logger.""" loggers = tuple( reconfigure_logger( @@ -975,15 +974,41 @@ def _service_subprocess( """ Service subprocess events by processing socket activity and checking for process exit. - Delegates the selector event loop to :func:`service_selector` (shared - with provider-registered bridges), then checks the subprocess status. + This method: + - Waits for activity on the registered file objects (via `self.selector.select`). + - Processes any events triggered on these file objects. + - Checks if the subprocess has exited during the wait. :param max_wait_time: Maximum time to block while waiting for events, in seconds. :param raise_on_timeout: If True, raise an exception if the subprocess does not exit within the timeout. :param expect_signal: Signal not to log if the task exits with this code. :returns: The process exit code, or None if it's still alive """ - service_selector(self.selector, timeout=max_wait_time) + # Ensure minimum timeout to prevent CPU spike with tight loop when timeout is 0 or negative + timeout = max(0.01, max_wait_time) + events = self.selector.select(timeout=timeout) + for key, _ in events: + # Retrieve the handler responsible for processing this file object (e.g., stdout, stderr) + socket_handler, on_close = key.data + + # Example of handler behavior: + # If the subprocess writes "Hello, World!" to stdout: + # - `socket_handler` reads and processes the message. + # - If EOF is reached, the handler returns False to signal no more reads are expected. + # - BrokenPipeError should be caught and treated as if the handler returned false, similar + # to EOF case + try: + need_more = socket_handler(key.fileobj) + except (BrokenPipeError, ConnectionResetError): + need_more = False + + # If the handler signals that the file object is no longer needed (EOF, closed, etc.) + # unregister it from the selector to stop monitoring; `wait()` blocks until all selectors + # are removed. + if not need_more: + sock: socket = key.fileobj # type: ignore[assignment] + on_close(sock) + sock.close() # Check if the subprocess has exited return self._check_subprocess_exit(raise_on_timeout=raise_on_timeout, expect_signal=expect_signal) @@ -2044,6 +2069,50 @@ def run_task_in_process(ti: TaskInstance, task) -> TaskRunResult: return InProcessTestSupervisor.start(what=ti, task=task) +# Sockets, even the `.makefile()` function don't correctly do line buffering on reading. If a chunk is read +# and it doesn't contain a new line character, `.readline()` will just return the chunk as is. +# +# This returns a callback suitable for attaching to a `selector` that reads in to a buffer, and yields lines +# to a (sync) generator +def make_buffered_socket_reader( + gen: Generator[None, bytes | bytearray, None], + on_close: Callable[[socket], None], + buffer_size: int = 4096, +): + buffer = bytearray() # This will hold our accumulated binary data + read_buffer = bytearray(buffer_size) # Temporary buffer for each read + + # We need to start up the generator to get it to the point it's at waiting on the yield + next(gen) + + def cb(sock: socket): + nonlocal buffer, read_buffer + # Read up to `buffer_size` bytes of data from the socket + n_received = sock.recv_into(read_buffer) + + if not n_received: + # If no data is returned, the connection is closed. Return whatever is left in the buffer + if len(buffer): + with suppress(StopIteration): + gen.send(buffer) + return False + + buffer.extend(read_buffer[:n_received]) + + # We could have read multiple lines in one go, yield them all + while (newline_pos := buffer.find(b"\n")) != -1: + line = buffer[: newline_pos + 1] + try: + gen.send(line) + except StopIteration: + return False + buffer = buffer[newline_pos + 1 :] # Update the buffer with remaining data + + return True + + return cb, on_close + + def length_prefixed_frame_reader( gen: Generator[None, _RequestFrame, None], on_close: Callable[[socket], None] ): diff --git a/task-sdk/tests/task_sdk/execution_time/test_selector_loop.py b/task-sdk/tests/task_sdk/execution_time/test_selector_loop.py deleted file mode 100644 index efbfa83adecf8..0000000000000 --- a/task-sdk/tests/task_sdk/execution_time/test_selector_loop.py +++ /dev/null @@ -1,479 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from __future__ import annotations - -import selectors -import socket -from unittest.mock import MagicMock - -import pytest - -from airflow.sdk.execution_time.selector_loop import ( - make_buffered_socket_reader, - make_raw_forwarder, - service_selector, -) - - -def _make_generator(): - """Return a generator that collects sent lines into a list.""" - received: list[bytes | bytearray] = [] - - def gen(): - while True: - line = yield - received.append(bytes(line)) - - g = gen() - return g, received - - -def _make_socket_pair(): - """Create a connected TCP socket pair on localhost.""" - server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server.bind(("127.0.0.1", 0)) - server.listen(1) - addr = server.getsockname() - - client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - client.connect(addr) - conn, _ = server.accept() - server.close() - return client, conn - - -class TestMakeBufferedSocketReader: - def test_single_complete_line(self): - gen, received = _make_generator() - on_close = MagicMock() - handler, returned_on_close = make_buffered_socket_reader(gen, on_close) - - sock = MagicMock(spec=socket.socket) - # recv_into writes data and returns count - data = b"hello world\n" - sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, data) - - result = handler(sock) - - assert result is True - assert received == [b"hello world\n"] - assert returned_on_close is on_close - - def test_multiple_lines_in_single_recv(self): - gen, received = _make_generator() - on_close = MagicMock() - handler, _ = make_buffered_socket_reader(gen, on_close) - - sock = MagicMock(spec=socket.socket) - data = b"line1\nline2\nline3\n" - sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, data) - - result = handler(sock) - - assert result is True - assert received == [b"line1\n", b"line2\n", b"line3\n"] - - def test_partial_line_accumulated_across_calls(self): - gen, received = _make_generator() - on_close = MagicMock() - handler, _ = make_buffered_socket_reader(gen, on_close) - - sock = MagicMock(spec=socket.socket) - - # First call: partial line (no newline) - sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"hell") - result = handler(sock) - assert result is True - assert received == [] - - # Second call: rest of the line - sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"o\n") - result = handler(sock) - assert result is True - assert received == [b"hello\n"] - - def test_eof_flushes_remaining_buffer(self): - gen, received = _make_generator() - on_close = MagicMock() - handler, _ = make_buffered_socket_reader(gen, on_close) - - sock = MagicMock(spec=socket.socket) - - # Send partial data (no newline) - sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"leftover") - handler(sock) - assert received == [] - - # EOF (recv_into returns 0) — clear side_effect so return_value takes effect - sock.recv_into.side_effect = None - sock.recv_into.return_value = 0 - result = handler(sock) - - assert result is False - assert received == [b"leftover"] - - def test_eof_with_empty_buffer(self): - gen, received = _make_generator() - on_close = MagicMock() - handler, _ = make_buffered_socket_reader(gen, on_close) - - sock = MagicMock(spec=socket.socket) - sock.recv_into.return_value = 0 - - result = handler(sock) - - assert result is False - assert received == [] - - def test_generator_stop_iteration_returns_false(self): - """If the generator is exhausted, handler returns False.""" - - def limited_gen(): - yield # startup - yield # receive one line, then stop - - gen = limited_gen() - on_close = MagicMock() - handler, _ = make_buffered_socket_reader(gen, on_close) - - sock = MagicMock(spec=socket.socket) - # First line succeeds - sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"line1\n") - result = handler(sock) - assert result is True - - # Second line triggers StopIteration in the generator - sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"line2\n") - result = handler(sock) - assert result is False - - def test_mixed_complete_and_partial_lines(self): - gen, received = _make_generator() - on_close = MagicMock() - handler, _ = make_buffered_socket_reader(gen, on_close) - - sock = MagicMock(spec=socket.socket) - # Data contains one complete line and a partial line - sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"complete\npart") - handler(sock) - assert received == [b"complete\n"] - - # Finish the partial line - sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, b"ial\n") - handler(sock) - assert received == [b"complete\n", b"partial\n"] - - def test_custom_buffer_size(self): - gen, received = _make_generator() - on_close = MagicMock() - handler, _ = make_buffered_socket_reader(gen, on_close, buffer_size=8) - - sock = MagicMock(spec=socket.socket) - # Data larger than buffer_size — recv_into only reads buffer_size bytes - full_data = b"abcdefghijklmnop\n" - # Simulate chunked reads - sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, full_data[: len(buf)]) - handler(sock) - # Only first 8 bytes read, no newline yet - assert received == [] - - sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, full_data[8:16]) - handler(sock) - assert received == [] - - sock.recv_into.side_effect = lambda buf: _fill_buffer(buf, full_data[16:]) - handler(sock) - assert received == [b"abcdefghijklmnop\n"] - - -def _fill_buffer(buf: bytearray, data: bytes) -> int: - """Helper to simulate socket.recv_into by filling the buffer.""" - n = min(len(data), len(buf)) - buf[:n] = data[:n] - return n - - -class TestMakeRawForwarder: - def test_forwards_data_to_dest(self): - on_close = MagicMock() - dest = MagicMock(spec=socket.socket) - handler, returned_on_close = make_raw_forwarder(dest, on_close) - - src = MagicMock(spec=socket.socket) - src.recv.return_value = b"hello" - - result = handler(src) - - assert result is True - dest.sendall.assert_called_once_with(b"hello") - assert returned_on_close is on_close - - def test_eof_returns_false(self): - on_close = MagicMock() - dest = MagicMock(spec=socket.socket) - handler, _ = make_raw_forwarder(dest, on_close) - - src = MagicMock(spec=socket.socket) - src.recv.return_value = b"" - - result = handler(src) - - assert result is False - dest.sendall.assert_not_called() - - @pytest.mark.parametrize( - "exception", - [BrokenPipeError, ConnectionResetError, OSError], - ids=["broken_pipe", "connection_reset", "os_error"], - ) - def test_sendall_exception_returns_false(self, exception): - on_close = MagicMock() - dest = MagicMock(spec=socket.socket) - dest.sendall.side_effect = exception - handler, _ = make_raw_forwarder(dest, on_close) - - src = MagicMock(spec=socket.socket) - src.recv.return_value = b"data" - - result = handler(src) - - assert result is False - - def test_multiple_forwards(self): - on_close = MagicMock() - dest = MagicMock(spec=socket.socket) - handler, _ = make_raw_forwarder(dest, on_close) - - src = MagicMock(spec=socket.socket) - - for chunk in [b"chunk1", b"chunk2", b"chunk3"]: - src.recv.return_value = chunk - assert handler(src) is True - - assert dest.sendall.call_count == 3 - - -class TestServiceSelector: - def test_calls_handler_for_ready_sockets(self): - sel = MagicMock(spec=selectors.DefaultSelector) - handler = MagicMock(return_value=True) - on_close = MagicMock() - sock = MagicMock(spec=socket.socket) - - key = MagicMock() - key.data = (handler, on_close) - key.fileobj = sock - - sel.select.return_value = [(key, selectors.EVENT_READ)] - - service_selector(sel, timeout=1.0) - - handler.assert_called_once_with(sock) - on_close.assert_not_called() - sock.close.assert_not_called() - - def test_on_close_and_sock_close_when_handler_returns_false(self): - sel = MagicMock(spec=selectors.DefaultSelector) - handler = MagicMock(return_value=False) - on_close = MagicMock() - sock = MagicMock(spec=socket.socket) - - key = MagicMock() - key.data = (handler, on_close) - key.fileobj = sock - - sel.select.return_value = [(key, selectors.EVENT_READ)] - - service_selector(sel, timeout=1.0) - - handler.assert_called_once_with(sock) - on_close.assert_called_once_with(sock) - sock.close.assert_called_once() - - @pytest.mark.parametrize( - "exception", - [BrokenPipeError, ConnectionResetError], - ids=["broken_pipe", "connection_reset"], - ) - def test_pipe_errors_treated_as_eof(self, exception): - sel = MagicMock(spec=selectors.DefaultSelector) - handler = MagicMock(side_effect=exception) - on_close = MagicMock() - sock = MagicMock(spec=socket.socket) - - key = MagicMock() - key.data = (handler, on_close) - key.fileobj = sock - - sel.select.return_value = [(key, selectors.EVENT_READ)] - - service_selector(sel, timeout=1.0) - - on_close.assert_called_once_with(sock) - sock.close.assert_called_once() - - def test_empty_selector_no_events(self): - sel = MagicMock(spec=selectors.DefaultSelector) - sel.select.return_value = [] - - # Should not raise - service_selector(sel, timeout=1.0) - - @pytest.mark.parametrize( - ("input_timeout", "expected_min"), - [ - (0.0, 0.01), - (-1.0, 0.01), - (-100.0, 0.01), - (0.5, 0.5), - (2.0, 2.0), - ], - ids=["zero", "negative", "very_negative", "positive_half", "positive_two"], - ) - def test_timeout_clamped_to_minimum(self, input_timeout, expected_min): - sel = MagicMock(spec=selectors.DefaultSelector) - sel.select.return_value = [] - - service_selector(sel, timeout=input_timeout) - - sel.select.assert_called_once() - actual_timeout = sel.select.call_args[1].get("timeout") or sel.select.call_args[0][0] - assert actual_timeout == pytest.approx(expected_min) - - def test_multiple_ready_sockets(self): - sel = MagicMock(spec=selectors.DefaultSelector) - - handler1 = MagicMock(return_value=True) - on_close1 = MagicMock() - sock1 = MagicMock(spec=socket.socket) - key1 = MagicMock() - key1.data = (handler1, on_close1) - key1.fileobj = sock1 - - handler2 = MagicMock(return_value=False) - on_close2 = MagicMock() - sock2 = MagicMock(spec=socket.socket) - key2 = MagicMock() - key2.data = (handler2, on_close2) - key2.fileobj = sock2 - - sel.select.return_value = [(key1, selectors.EVENT_READ), (key2, selectors.EVENT_READ)] - - service_selector(sel, timeout=1.0) - - # First socket: handler returns True, stays open - handler1.assert_called_once_with(sock1) - on_close1.assert_not_called() - sock1.close.assert_not_called() - - # Second socket: handler returns False, closed - handler2.assert_called_once_with(sock2) - on_close2.assert_called_once_with(sock2) - sock2.close.assert_called_once() - - -class TestSelectorLoopIntegration: - def test_buffered_reader_with_real_sockets(self): - """End-to-end: send lines through real sockets and verify buffered reading.""" - gen, received = _make_generator() - sender, reader = _make_socket_pair() - try: - sel = selectors.DefaultSelector() - - def on_close(sock): - sel.unregister(sock) - - sel.register(reader, selectors.EVENT_READ, make_buffered_socket_reader(gen, on_close)) - - sender.sendall(b"first line\nsecond line\n") - - service_selector(sel, timeout=1.0) - - assert b"first line\n" in received - assert b"second line\n" in received - - # Close sender, then drain - sender.close() - sender = None - - service_selector(sel, timeout=0.5) - - sel.close() - finally: - if sender: - sender.close() - reader.close() - - def test_raw_forwarder_with_real_sockets(self): - """End-to-end: forward raw bytes between real socket pairs.""" - src_send, src_recv = _make_socket_pair() - # Use socketpair for the destination so reads/writes are symmetric - dst_write, dst_read = socket.socketpair() - try: - sel = selectors.DefaultSelector() - - def on_close(sock): - sel.unregister(sock) - - sel.register(src_recv, selectors.EVENT_READ, make_raw_forwarder(dst_write, on_close)) - - src_send.sendall(b"raw data payload") - - service_selector(sel, timeout=1.0) - - dst_read.setblocking(False) - forwarded = dst_read.recv(4096) - - assert forwarded == b"raw data payload" - - sel.close() - finally: - for s in (src_send, src_recv, dst_write, dst_read): - s.close() - - def test_eof_triggers_on_close_with_real_sockets(self): - """When the sender closes, the selector callback chain fires on_close.""" - gen, received = _make_generator() - sender, reader = _make_socket_pair() - closed_sockets: list[socket.socket] = [] - try: - sel = selectors.DefaultSelector() - - def on_close(sock): - sel.unregister(sock) - closed_sockets.append(sock) - - sel.register(reader, selectors.EVENT_READ, make_buffered_socket_reader(gen, on_close)) - - # Send data then close - sender.sendall(b"final\n") - service_selector(sel, timeout=1.0) - assert received == [b"final\n"] - - sender.close() - sender = None - service_selector(sel, timeout=0.5) - - # on_close should have been called, and socket closed by service_selector - assert len(closed_sockets) == 1 - - sel.close() - finally: - if sender: - sender.close() - reader.close() From e4ccc01b17c1efe69b9e5e352f6539e61e15cf30 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 20 May 2026 11:38:06 +0800 Subject: [PATCH 59/75] Add schema migration to supervisor-child comm With foreign language SDKs, it may be possible the two sides of supervisor comm have different versions. This adds a migration layer at the supervisor (server) side, so an SDK (client) using a lower version of the schema may be able to communicate to the server. --- task-sdk/.pre-commit-config.yaml | 5 +-- .../sdk/coordinators/java/coordinator.py | 39 ++++++++++++------- .../coordinators/java/test_coordinator.py | 29 +++++++++----- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/task-sdk/.pre-commit-config.yaml b/task-sdk/.pre-commit-config.yaml index 470a52cb380db..74c9f8d401cef 100644 --- a/task-sdk/.pre-commit-config.yaml +++ b/task-sdk/.pre-commit-config.yaml @@ -43,11 +43,10 @@ repos: ^src/airflow/sdk/definitions/deadline\.py$| ^src/airflow/sdk/definitions/dag\.py$| ^src/airflow/sdk/definitions/_internal/types\.py$| - ^src/airflow/sdk/execution_time/schema/__init__\.py$| - ^src/airflow/sdk/execution_time/coordinator\.py$| + ^src/airflow/sdk/execution_time/callback_supervisor\.py$| ^src/airflow/sdk/execution_time/execute_workload\.py$| ^src/airflow/sdk/execution_time/secrets_masker\.py$| - ^src/airflow/sdk/execution_time/callback_supervisor\.py$| + ^src/airflow/sdk/execution_time/schema/__init__\.py$| ^src/airflow/sdk/execution_time/supervisor\.py$| ^src/airflow/sdk/execution_time/task_runner\.py$| ^src/airflow/sdk/serde/serializers/kubernetes\.py$| diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py index b7a56559e4ef1..f4679e5576718 100644 --- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py +++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py @@ -62,17 +62,25 @@ def _calculate_classpath(jars_root: Sequence[pathlib.Path]) -> str: return os.pathsep.join(jars) -def _find_main_class(jars_root: Sequence[pathlib.Path]) -> str: - for root in jars_root: - for p in root.iterdir(): - if p.suffix != ".jar": - continue - with zipfile.ZipFile(p) as zf: - with zf.open("META-INF/MANIFEST.MF") as f: - if main_class := email.message_from_binary_file(f)["Main-Class"]: - return main_class - resolved_paths = os.pathsep.join(str(p.resolve()) for p in jars_root) - raise FileNotFoundError(f"cannot fine main class in {resolved_paths}") +@attrs.define +class _MainJar: + path: pathlib.Path + main_class: str + schema_version: str | None + + @classmethod + def find(cls, jars_root: Sequence[pathlib.Path]) -> Self: + for root in jars_root: + for p in root.iterdir(): + if p.suffix != ".jar": + continue + with zipfile.ZipFile(p) as zf: + with zf.open("META-INF/MANIFEST.MF") as f: + manifest = email.message_from_binary_file(f) + if main_class := manifest["Main-Class"]: + return cls(p, main_class, manifest.get("Airflow-SDK-Supervisor-Schema-Version")) + resolved_paths = os.pathsep.join(str(p.resolve()) for p in jars_root) + raise FileNotFoundError(f"cannot find main class in {resolved_paths}") def _accept_connections( @@ -107,7 +115,7 @@ class _JavaActivitySubprocess(ActivitySubprocess): _comm_server: socket.socket _logs_server: socket.socket - _child_process: subprocess.Popen + _subprocess: subprocess.Popen # Keep track of channels used to pipe subprocess stdout and stderr so we can # close them on exit. The "read" side is handled by _register_pipe_readers @@ -129,6 +137,8 @@ def start( # type: ignore[override] jars_root: Sequence[pathlib.Path], **kwargs, ) -> Self: + jar = _MainJar.find(jars_root) + comm_server = _start_server() logs_server = _start_server() @@ -144,7 +154,7 @@ def start( # type: ignore[override] "-classpath", _calculate_classpath(jars_root), *jvm_args, - _find_main_class(jars_root), + jar.main_class, # Arguments to MainClass... f"--comm={comm_host}:{comm_port}", f"--logs={logs_host}:{logs_port}", @@ -162,7 +172,8 @@ def start( # type: ignore[override] process_log=logger or structlog.get_logger(logger_name="task").bind(), start_time=time.monotonic(), stdin=socks["comm"], - child_process=proc, + subprocess=proc, + subprocess_schema_version=jar.schema_version, comm_server=comm_server, logs_server=logs_server, stdout_w=stdout_w, diff --git a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py index ca435cfd826f8..382c907a5f451 100644 --- a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py +++ b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py @@ -36,8 +36,8 @@ JavaCoordinator, _accept_connections, _calculate_classpath, - _find_main_class, _JavaActivitySubprocess, + _MainJar, _start_server, ) from airflow.sdk.execution_time.coordinator import BaseCoordinator @@ -69,11 +69,18 @@ def _make_ti(dag_id: str = "test_dag", queue: str = "java") -> TaskInstanceDTO: ) -def _make_jar(path: pathlib.Path, *, main_class: str | None = "com.example.Main") -> pathlib.Path: +def _make_jar( + path: pathlib.Path, + *, + main_class: str | None = "com.example.Main", + schema_version: str | None = None, +) -> pathlib.Path: """Write a minimal JAR with (optionally) a Main-Class manifest entry.""" lines = ["Manifest-Version: 1.0"] if main_class: lines.append(f"Main-Class: {main_class}") + if schema_version: + lines.append(f"Airflow-SDK-Supervisor-Schema-Version: {schema_version}") manifest = "\n".join(lines) + "\n\n" with zipfile.ZipFile(path, "w") as zf: zf.writestr("META-INF/MANIFEST.MF", manifest) @@ -151,34 +158,38 @@ def test_empty_directory_returns_empty_string(self, tmp_path): assert result == "" -class TestFindMainClass: +class TestMainJar: def test_returns_main_class_from_jar(self, tmp_path): _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main") - assert _find_main_class([tmp_path]) == "com.example.Main" + assert _MainJar.find([tmp_path]) == _MainJar(tmp_path.joinpath("app.jar"), "com.example.Main", None) def test_no_jars_raises_file_not_found(self, tmp_path): with pytest.raises(FileNotFoundError, match=re.escape(str(tmp_path.resolve()))): - _find_main_class([tmp_path]) + _MainJar.find([tmp_path]) def test_jar_without_main_class_not_returned(self, tmp_path): _make_jar(tmp_path.joinpath("app.jar"), main_class=None) with pytest.raises(FileNotFoundError): - _find_main_class([tmp_path]) + _MainJar.find([tmp_path]) def test_non_jar_files_skipped(self, tmp_path): tmp_path.joinpath("readme.txt").write_bytes(b"not a jar") _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main") - assert _find_main_class([tmp_path]) == "com.example.Main" + assert _MainJar.find([tmp_path]) == _MainJar(tmp_path.joinpath("app.jar"), "com.example.Main", None) def test_first_jar_missing_main_class_falls_through_to_second(self, tmp_path): # Alphabetically: a.jar (no Main-Class), b.jar (has Main-Class). _make_jar(tmp_path.joinpath("a.jar"), main_class=None) _make_jar(tmp_path.joinpath("b.jar"), main_class="com.example.Fallback") - assert _find_main_class([tmp_path]) == "com.example.Fallback" + assert _MainJar.find([tmp_path]) == _MainJar(tmp_path.joinpath("b.jar"), "com.example.Fallback", None) def test_fully_qualified_class_name_preserved(self, tmp_path): _make_jar(tmp_path.joinpath("app.jar"), main_class="org.apache.airflow.sdk.java.TaskRunner") - assert _find_main_class([tmp_path]) == "org.apache.airflow.sdk.java.TaskRunner" + assert _MainJar.find([tmp_path]) == _MainJar( + path=tmp_path.joinpath("app.jar"), + main_class="org.apache.airflow.sdk.java.TaskRunner", + schema_version=None, + ) class TestAcceptConnections: From 7b1593f8294943e4c1e16bc92e0f334f533f2886 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 20 May 2026 14:42:53 +0800 Subject: [PATCH 60/75] Redo 16332b2e3be427e614a1ddc7c7bb610f4d284b22 --- .dockerignore | 1 - airflow-core/docs/extra-packages-ref.rst | 11 ----------- .../airflow/serialization/definitions/baseoperator.py | 1 - .../src/airflow_breeze/utils/docker_command_utils.py | 1 - devel-common/src/docs/provider_conf.py | 2 +- .../src/sphinx_exts/includes/sections-and-options.rst | 4 ++-- scripts/ci/docker-compose/local.yml | 3 --- 7 files changed, 3 insertions(+), 20 deletions(-) diff --git a/.dockerignore b/.dockerignore index 368d437dcfd1a..df08c066ce3b4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -39,7 +39,6 @@ !task-sdk/ !airflow-ctl/ !go-sdk/ -!sdk/ # Add all "test" distributions !tests diff --git a/airflow-core/docs/extra-packages-ref.rst b/airflow-core/docs/extra-packages-ref.rst index 9fb579c9b08ec..2646b0a7c3079 100644 --- a/airflow-core/docs/extra-packages-ref.rst +++ b/airflow-core/docs/extra-packages-ref.rst @@ -178,17 +178,6 @@ all the ``airflow`` packages together - similarly to what happened in Airflow 2. ``airflow-task-sdk`` separately, if you want to install providers, you need to install them separately as ``apache-airflow-providers-*`` distribution packages. -Multi-Language extras -===================== - -These are extras that add dependencies needed for integration with other languages runtimes. Currently we have only Java SDK related extra, but in the future we might add more extras related to other languages runtimes. - -+----------+------------------------------------------+------------------------------------------------------------------+ -| extra | install command | enables | -+==========+==========================================+==================================================================+ -| sdk.java | ``pip install apache-airflow[sdk.java]`` | JavaCoordinator for both dag processing and workload execution. | -+----------+------------------------------------------+------------------------------------------------------------------+ - Apache Software extras ====================== diff --git a/airflow-core/src/airflow/serialization/definitions/baseoperator.py b/airflow-core/src/airflow/serialization/definitions/baseoperator.py index 9eaf9cc3ed906..6bafc5891235a 100644 --- a/airflow-core/src/airflow/serialization/definitions/baseoperator.py +++ b/airflow-core/src/airflow/serialization/definitions/baseoperator.py @@ -195,7 +195,6 @@ def get_serialized_fields(cls): "ignore_first_depends_on_past", "inlets", "is_setup", - "sdk", "is_teardown", "map_index_template", "max_active_tis_per_dag", diff --git a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py index b50ca5f7d1b69..91fcd6cd9afc1 100644 --- a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py +++ b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py @@ -110,7 +110,6 @@ ("scripts", "/opt/airflow/scripts"), ("uv.lock", "/opt/airflow/uv.lock"), ("scripts/docker/entrypoint_ci.sh", "/entrypoint"), - ("sdk", "/opt/airflow/sdk"), ("shared", "/opt/airflow/shared"), ("task-sdk", "/opt/airflow/task-sdk"), ] diff --git a/devel-common/src/docs/provider_conf.py b/devel-common/src/docs/provider_conf.py index b730e8f20a417..6bc9da15f5f61 100644 --- a/devel-common/src/docs/provider_conf.py +++ b/devel-common/src/docs/provider_conf.py @@ -151,7 +151,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -empty_subpackages = ["apache", "atlassian", "common", "cncf", "dbt", "microsoft", "sdk"] +empty_subpackages = ["apache", "atlassian", "common", "cncf", "dbt", "microsoft"] exclude_patterns = [ "operators/_partials", "_api/airflow/index.rst", diff --git a/devel-common/src/sphinx_exts/includes/sections-and-options.rst b/devel-common/src/sphinx_exts/includes/sections-and-options.rst index b0d84a1bd8a5a..e04383c8c5582 100644 --- a/devel-common/src/sphinx_exts/includes/sections-and-options.rst +++ b/devel-common/src/sphinx_exts/includes/sections-and-options.rst @@ -65,7 +65,7 @@ {% if default and "\n" in default %} .. code-block:: - {{ default | indent(width=8) }} + {{ default }} {% else %} ``{{ "''" if default == "" else default }}`` {% endif %} @@ -85,7 +85,7 @@ {% if "\n" in example %} .. code-block:: - {{ example | indent(width=8) }} + {{ example }} {% else %} ``{{ example }}`` {% endif %} diff --git a/scripts/ci/docker-compose/local.yml b/scripts/ci/docker-compose/local.yml index 45dab4d56ffaf..f91198189f6db 100644 --- a/scripts/ci/docker-compose/local.yml +++ b/scripts/ci/docker-compose/local.yml @@ -123,9 +123,6 @@ services: - type: bind source: ../../../scripts/docker/entrypoint_ci.sh target: /entrypoint - - type: bind - source: ../../../sdk - target: /opt/airflow/sdk - type: bind source: ../../../shared target: /opt/airflow/shared From 08e1ad83c6904c4b1dd8a74735ba352c6f77c488 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 22 May 2026 10:20:32 +0800 Subject: [PATCH 61/75] CI: Fix uv lock --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index 3a097187f7d56..bca09add09083 100644 --- a/uv.lock +++ b/uv.lock @@ -21092,8 +21092,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version < '3.15' and sys_platform == 'emscripten') or (python_full_version < '3.15' and sys_platform == 'win32') or (platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, - { name = "jeepney", marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version < '3.15' and sys_platform == 'emscripten') or (python_full_version < '3.15' and sys_platform == 'win32') or (platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cryptography", marker = "python_full_version >= '3.14' or platform_machine != 'arm64' or sys_platform != 'darwin'" }, + { name = "jeepney", marker = "python_full_version >= '3.14' or platform_machine != 'arm64' or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ From 522985fc0622eaa435f54d77f2e10f21b6f2684d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Mon, 25 May 2026 14:57:19 +0800 Subject: [PATCH 62/75] Add log to identify queue-coordinator lookup --- task-sdk/src/airflow/sdk/execution_time/coordinator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index f6aa453f06c61..08011e09e647b 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -44,6 +44,7 @@ import attrs import pydantic +import structlog from airflow.sdk._shared.module_loading import import_string from airflow.sdk.configuration import conf @@ -65,6 +66,8 @@ "reset_coordinator_manager", ] +log = structlog.get_logger(__name__) + class BaseCoordinator: """ @@ -213,9 +216,12 @@ def for_queue(self, queue: str) -> BaseCoordinator: If an entry is not registered, a Python coordinator is returned. """ try: - return self._for_queue_internal(queue) + coordinator = self._for_queue_internal(queue) except KeyError: + log.debug("Queue not configured to a coordinator; defaulting to Python", queue=queue) return _build_python_coordinator() + log.debug("Coordinator found for queue", coordinator=coordinator, queue=queue) + return coordinator @functools.cache From 7d7fa95dab00274925b25f0289ee806edb102c24 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Mon, 25 May 2026 15:26:38 +0800 Subject: [PATCH 63/75] Track Popen object instead of psutil.Process A wrapper type is introduced to abstract away the differences between the two types, and allow we to only keep one reference to the child process in a WatchedSubprocess instance. --- .../sdk/coordinators/java/coordinator.py | 32 +++++++-- .../airflow/sdk/execution_time/supervisor.py | 70 +++++++++++++++++-- .../execution_time/test_supervisor.py | 10 ++- 3 files changed, 99 insertions(+), 13 deletions(-) diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py index f4679e5576718..30bd077affb9e 100644 --- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py +++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py @@ -23,6 +23,7 @@ import os import pathlib import selectors +import signal import socket import subprocess import time @@ -30,11 +31,10 @@ from typing import TYPE_CHECKING, cast import attrs -import psutil import structlog from airflow.sdk.execution_time.coordinator import BaseCoordinator -from airflow.sdk.execution_time.supervisor import ActivitySubprocess +from airflow.sdk.execution_time.supervisor import ActivitySubprocess, NeverRaised, ProcessTracker if TYPE_CHECKING: from collections.abc import Sequence @@ -109,13 +109,36 @@ def _accept_connections( return accepted +class PopenTracker(ProcessTracker): + """ + Process tracker backed by :class:`subprocess.Popen`. + + :meta private: + """ + + ProcessNotFound = NeverRaised + TimeoutExpired = subprocess.TimeoutExpired + + def __init__(self, impl: subprocess.Popen) -> None: + self._impl = impl + + @property + def pid(self) -> int: + return self._impl.pid + + def send_signal(self, s: signal.Signals) -> None: + self._impl.send_signal(s) + + def wait(self, timeout: float | None) -> int: + return self._impl.wait(timeout) + + @attrs.define(kw_only=True) class _JavaActivitySubprocess(ActivitySubprocess): """Java task runner process.""" _comm_server: socket.socket _logs_server: socket.socket - _subprocess: subprocess.Popen # Keep track of channels used to pipe subprocess stdout and stderr so we can # close them on exit. The "read" side is handled by _register_pipe_readers @@ -168,11 +191,10 @@ def start( # type: ignore[override] self = cls( id=what.id, pid=proc.pid, - process=psutil.Process(proc.pid), + process=PopenTracker(proc), process_log=logger or structlog.get_logger(logger_name="task").bind(), start_time=time.monotonic(), stdin=socks["comm"], - subprocess=proc, subprocess_schema_version=jar.schema_version, comm_server=comm_server, logs_server=logs_server, diff --git a/task-sdk/src/airflow/sdk/execution_time/supervisor.py b/task-sdk/src/airflow/sdk/execution_time/supervisor.py index c1993e3248ef2..327feec5b826d 100644 --- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py +++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py @@ -535,6 +535,66 @@ def _child_exec_main(): _fork_main(child_requests, child_stdout, child_stderr, 0, _subprocess_main) +class NeverRaised(Exception): + """ + An exception type that's never raised. + + This is used in :class:`_ProcessTracker` as a stand-in when an exception is + not supposed to be raised by an implementation. + + :meta private: + """ + + +class ProcessTracker: + """ + Common interface to track a process. + + This protocol works for both :class:`psutil.Process` (used by the forking + model) and :class:`subprocess.Popen` (used by the subprocess model). A + custom class may also be implemented if needed in the future. + + :meta private: + """ + + ProcessNotFound: type[Exception] + TimeoutExpired: type[Exception] + + @property + def pid(self) -> int: + raise NotImplementedError + + def send_signal(self, s: signal.Signals) -> None: + raise NotImplementedError + + def wait(self, timeout: float | None) -> int: + raise NotImplementedError + + +class PsutilTracker(ProcessTracker): + """ + Process tracker backed by :class:`psutil.Process`. + + :meta private: + """ + + ProcessNotFound = psutil.NoSuchProcess + TimeoutExpired = psutil.TimeoutExpired + + def __init__(self, impl: psutil.Process) -> None: + self._impl = impl + + @property + def pid(self) -> int: + return self._impl.pid + + def send_signal(self, s: signal.Signals) -> None: + self._impl.send_signal(s) + + def wait(self, timeout: float | None) -> int: + return self._impl.wait(timeout) + + def _validate_schema_version(instance, _, value) -> str | None: if value is None: return None @@ -561,8 +621,8 @@ class WatchedSubprocess: decoder: ClassVar[TypeAdapter] """The decoder to use for incoming messages from the child process.""" - _process: psutil.Process = attrs.field(repr=False) - """File descriptor for request handling.""" + _process: ProcessTracker = attrs.field(repr=False) + """Handler to track the process.""" _subprocess_schema_version: str | None = attrs.field(default=None, validator=_validate_schema_version) """ @@ -680,7 +740,7 @@ def start( proc = cls( pid=pid, stdin=read_requests, - process=psutil.Process(pid), + process=PsutilTracker(psutil.Process(pid)), process_log=logger, start_time=time.monotonic(), **constructor_kwargs, @@ -944,7 +1004,7 @@ def kill( if sig != escalation_path[-1]: msg += "; escalating" log.warning(msg, pid=self.pid, signal=sig.name) - except psutil.NoSuchProcess: + except self._process.ProcessNotFound: log.debug("Process already terminated", pid=self.pid) self._exit_code = -1 return @@ -1022,7 +1082,7 @@ def _check_subprocess_exit( try: self._exit_code = self._process.wait(timeout=0) - except psutil.TimeoutExpired: + except self._process.TimeoutExpired: if raise_on_timeout: raise else: diff --git a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py index 344bebf5cd3e4..684d9460529d0 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py +++ b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py @@ -160,6 +160,7 @@ ActivitySubprocess, InProcessSupervisorComms, InProcessTestSupervisor, + ProcessTracker, _make_process_nondumpable, _remote_logging_conn, in_process_api_server, @@ -1269,9 +1270,12 @@ def test_cleanup_sockets_after_delay(self, monkeypatch, mocker): class TestWatchedSubprocessKill: @pytest.fixture def mock_process(self, mocker): - process = mocker.Mock(spec=psutil.Process) - process.pid = 12345 - return process + return mocker.Mock( + spec=ProcessTracker, + pid=12345, + ProcessNotFound=psutil.NoSuchProcess, + TimeoutExpired=psutil.TimeoutExpired, + ) @pytest.fixture def watched_subprocess(self, mocker, mock_process): From 142ad46b3c47fe76bf8ad2c5529f200468f07248 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Mon, 25 May 2026 15:30:44 +0800 Subject: [PATCH 64/75] Handle tilde (user home) in coordinator kwargs --- task-sdk/src/airflow/sdk/coordinators/java/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py index 30bd077affb9e..38e6bcc7f7daa 100644 --- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py +++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py @@ -223,8 +223,8 @@ def _convert_jars_root( if value is None: return [] if isinstance(value, (str, os.PathLike, pathlib.Path)): - return [pathlib.Path(value)] - return [pathlib.Path(v) for v in value] + return [pathlib.Path(value).expanduser()] + return [pathlib.Path(v).expanduser() for v in value] @attrs.define(kw_only=True) From fb79d63ab203b5d7493e08655597ad1d4c42c55b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Mon, 25 May 2026 16:01:09 +0800 Subject: [PATCH 65/75] Improve reporting on invalid coordinator configs --- .../airflow/sdk/execution_time/coordinator.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py b/task-sdk/src/airflow/sdk/execution_time/coordinator.py index 08011e09e647b..072e9c0b6c739 100644 --- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py +++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py @@ -108,7 +108,7 @@ def execute_task( class _CoordinatorSpec(pydantic.BaseModel): classpath: str - kwargs: dict[str, Any] + kwargs: dict[str, Any] = pydantic.Field(default_factory=dict) class _PythonCoordinator(BaseCoordinator): @@ -155,6 +155,10 @@ def _build_python_coordinator() -> _PythonCoordinator: return _PythonCoordinator() +class InvalidCoordinatorError(ValueError): + """Raised for an invalid coordinator configuration.""" + + @attrs.define(kw_only=True) class CoordinatorManager: """ @@ -201,8 +205,7 @@ def from_config(cls) -> Self: raise ValueError(f"[sdk] queue_to_coordinator references invalid coordinator key: {key!r}") return cls(coordinator_specs=coordinator_specs, queue_to_coordinator=queue_to_coordinator) - def _for_queue_internal(self, queue: str) -> BaseCoordinator: - key = self._queue_to_coordinator[queue] + def _find_queue(self, key: str) -> BaseCoordinator: with contextlib.suppress(KeyError): return self._created_coordinators[key] spec = self._coordinator_specs[key] @@ -216,10 +219,18 @@ def for_queue(self, queue: str) -> BaseCoordinator: If an entry is not registered, a Python coordinator is returned. """ try: - coordinator = self._for_queue_internal(queue) + key = self._queue_to_coordinator[queue] except KeyError: log.debug("Queue not configured to a coordinator; defaulting to Python", queue=queue) return _build_python_coordinator() + try: + coordinator = self._find_queue(key) + except KeyError: + raise InvalidCoordinatorError(f"Queue {queue!r} configured to nonexistent coordinator") + except ImportError: + raise InvalidCoordinatorError(f"Cannot import coordinator {key!r}") + except TypeError: + raise InvalidCoordinatorError(f"Cannot instantiate coordinator {key!r}") log.debug("Coordinator found for queue", coordinator=coordinator, queue=queue) return coordinator From ef5a836bed93521f4bd0d4258338c4739f6ba1b8 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Mon, 25 May 2026 17:13:31 +0800 Subject: [PATCH 66/75] Handle JAR inspection errors --- .../sdk/coordinators/java/coordinator.py | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py index 38e6bcc7f7daa..d77904071bb3f 100644 --- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py +++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py @@ -34,6 +34,7 @@ import structlog from airflow.sdk.execution_time.coordinator import BaseCoordinator +from airflow.sdk.execution_time.schema import get_schema_version_migrator from airflow.sdk.execution_time.supervisor import ActivitySubprocess, NeverRaised, ProcessTracker if TYPE_CHECKING: @@ -62,24 +63,45 @@ def _calculate_classpath(jars_root: Sequence[pathlib.Path]) -> str: return os.pathsep.join(jars) +def _read_jar_metadata(path: pathlib.Path) -> tuple[str, str]: + try: + with zipfile.ZipFile(path) as zf: + try: + manifest_info = zf.getinfo("META-INF/MANIFEST.MF") + except KeyError: + log.debug("JAR does not contain META-INF/MANIFEST.MF; ignored", path=path) + return "", "" + with zf.open(manifest_info) as f: + manifest = email.message_from_binary_file(f) + return manifest["Main-Class"], manifest["Airflow-SDK-Supervisor-Schema-Version"] + except zipfile.BadZipFile: + log.exception("Cannot read JAR; ignored", path=path) + return "", "" + + +def _validate_schema_version(instance, _, value) -> str: + return get_schema_version_migrator().resolve_version(str(value)) + + @attrs.define class _MainJar: path: pathlib.Path main_class: str - schema_version: str | None + schema_version: str = attrs.field(validator=_validate_schema_version) @classmethod def find(cls, jars_root: Sequence[pathlib.Path]) -> Self: for root in jars_root: + log.debug("Finding Main-Class JAR in directory", dir=root) for p in root.iterdir(): if p.suffix != ".jar": continue - with zipfile.ZipFile(p) as zf: - with zf.open("META-INF/MANIFEST.MF") as f: - manifest = email.message_from_binary_file(f) - if main_class := manifest["Main-Class"]: - return cls(p, main_class, manifest.get("Airflow-SDK-Supervisor-Schema-Version")) - resolved_paths = os.pathsep.join(str(p.resolve()) for p in jars_root) + main_class, schema_version = _read_jar_metadata(p) + if not main_class: + continue + log.debug("JAR located with Main-Class metadata set", path=p, main_class=main_class) + return cls(p, main_class, schema_version) + resolved_paths = os.pathsep.join(os.fspath(p.resolve()) for p in jars_root) raise FileNotFoundError(f"cannot find main class in {resolved_paths}") From d2f899318effcedb57888a4e6cfbbeeedf458fe3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Mon, 25 May 2026 17:33:40 +0800 Subject: [PATCH 67/75] Improve JAR metadata inspection Errors are now emitted with more context. Debug logs also added. JavaCoordinator now may explicitly provide a Main-Class to look for. --- .../sdk/coordinators/java/coordinator.py | 81 +++++++++++++------ .../coordinators/java/test_coordinator.py | 59 +++++++++++--- 2 files changed, 101 insertions(+), 39 deletions(-) diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py index d77904071bb3f..3be9bcd5b0510 100644 --- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py +++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py @@ -63,20 +63,26 @@ def _calculate_classpath(jars_root: Sequence[pathlib.Path]) -> str: return os.pathsep.join(jars) -def _read_jar_metadata(path: pathlib.Path) -> tuple[str, str]: - try: - with zipfile.ZipFile(path) as zf: - try: - manifest_info = zf.getinfo("META-INF/MANIFEST.MF") - except KeyError: - log.debug("JAR does not contain META-INF/MANIFEST.MF; ignored", path=path) - return "", "" - with zf.open(manifest_info) as f: - manifest = email.message_from_binary_file(f) - return manifest["Main-Class"], manifest["Airflow-SDK-Supervisor-Schema-Version"] - except zipfile.BadZipFile: - log.exception("Cannot read JAR; ignored", path=path) - return "", "" +@attrs.define +class _JarMetadata: + main_class: str + schema_version: str + + @classmethod + def from_jar(cls, path: pathlib.Path) -> Self | None: + try: + with zipfile.ZipFile(path) as zf: + try: + manifest_info = zf.getinfo("META-INF/MANIFEST.MF") + except KeyError: + log.debug("JAR does not contain META-INF/MANIFEST.MF; ignored", path=path) + return None + with zf.open(manifest_info) as f: + manifest = email.message_from_binary_file(f) + return cls(manifest["Main-Class"], manifest["Airflow-Supervisor-Schema-Version"]) + except zipfile.BadZipFile: + log.exception("Cannot read JAR; ignored", path=path) + return None def _validate_schema_version(instance, _, value) -> str: @@ -90,19 +96,27 @@ class _MainJar: schema_version: str = attrs.field(validator=_validate_schema_version) @classmethod - def find(cls, jars_root: Sequence[pathlib.Path]) -> Self: - for root in jars_root: - log.debug("Finding Main-Class JAR in directory", dir=root) + def find(cls, roots: Sequence[pathlib.Path], main_class: str) -> Self: + for root in roots: + log.debug("Finding Main-Class in directory", dir=root) for p in root.iterdir(): if p.suffix != ".jar": continue - main_class, schema_version = _read_jar_metadata(p) - if not main_class: + if (metadata := _JarMetadata.from_jar(p)) is None: + continue + if not metadata.main_class: + continue + if main_class and metadata.main_class != main_class: continue - log.debug("JAR located with Main-Class metadata set", path=p, main_class=main_class) - return cls(p, main_class, schema_version) - resolved_paths = os.pathsep.join(os.fspath(p.resolve()) for p in jars_root) - raise FileNotFoundError(f"cannot find main class in {resolved_paths}") + log.debug("JAR located with Main-Class metadata", path=p, main_class=metadata.main_class) + if not metadata.schema_version: + raise ValueError("JAR with Main-Class found without Airflow-Supervisor-Schema-Version") + return cls(p, metadata.main_class, metadata.schema_version) + if main_class: + tp = "cannot find a JAR with Main-Class matching {0} in {1}" + else: + tp = "cannot find a JAR with Main-Class metadata in {1}" + raise FileNotFoundError(tp.format(main_class, os.pathsep.join(os.fspath(p.resolve()) for p in roots))) def _accept_connections( @@ -180,9 +194,10 @@ def start( # type: ignore[override] java_executable: str, jvm_args: list[str], jars_root: Sequence[pathlib.Path], + main_class: str, **kwargs, ) -> Self: - jar = _MainJar.find(jars_root) + jar = _MainJar.find(jars_root, main_class) comm_server = _start_server() logs_server = _start_server() @@ -267,15 +282,28 @@ class JavaCoordinator(BaseCoordinator): }, } - :param java_executable: Path to the ``java`` binary (defaults to ``"java"``, - which relies on ``$PATH``). + :param java_executable: Path to the ``java`` command (defaults to + ``"java"``, which relies on ``$PATH``). :param jvm_args: Extra arguments passed to the JVM (e.g. ``["-Xmx512m"]``). :param jars_root: A list of directories scanned for JAR bundles. + :param main_class: Explicit entry point to execute with *java_executable*. + + If *main_class* is not explicitly set, JavaCoordinator scans *jars_root* to + find an executable JAR (one with Main-Class set in its metadata). If more + than one executable JAR is found, it may be nondeterministic which one ends + up being executed. + + A JAR containing metadata *Airflow-Supervisor-Schema-Version* should also be + available to specify the wire schema version. The JAR containing the Java + SDK automatically sets this, so you don't generally need to do anything if + dependency JARs are deployed as-is. If you repackage the dependencies, + however, you must also reproduce the metadata entry in one of the JARs. """ java_executable: str = "java" jvm_args: list[str] = attrs.field(factory=list) jars_root: list[pathlib.Path] = attrs.field(converter=_convert_jars_root, factory=list) + main_class: str = "" def execute_task( self, @@ -300,6 +328,7 @@ def execute_task( java_executable=self.java_executable, jvm_args=self.jvm_args, jars_root=self.jars_root, + main_class=self.main_class, ) exit_code = process.wait() return self.ExecutionResult(exit_code, process.final_state) diff --git a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py index 382c907a5f451..fa6080f530cbd 100644 --- a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py +++ b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py @@ -80,7 +80,7 @@ def _make_jar( if main_class: lines.append(f"Main-Class: {main_class}") if schema_version: - lines.append(f"Airflow-SDK-Supervisor-Schema-Version: {schema_version}") + lines.append(f"Airflow-Supervisor-Schema-Version: {schema_version}") manifest = "\n".join(lines) + "\n\n" with zipfile.ZipFile(path, "w") as zf: zf.writestr("META-INF/MANIFEST.MF", manifest) @@ -160,37 +160,66 @@ def test_empty_directory_returns_empty_string(self, tmp_path): class TestMainJar: def test_returns_main_class_from_jar(self, tmp_path): - _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main") - assert _MainJar.find([tmp_path]) == _MainJar(tmp_path.joinpath("app.jar"), "com.example.Main", None) + _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main", schema_version="2026-06-16") + assert _MainJar.find([tmp_path], "") == _MainJar( + tmp_path.joinpath("app.jar"), "com.example.Main", "2026-06-16" + ) def test_no_jars_raises_file_not_found(self, tmp_path): with pytest.raises(FileNotFoundError, match=re.escape(str(tmp_path.resolve()))): - _MainJar.find([tmp_path]) + _MainJar.find([tmp_path], "") def test_jar_without_main_class_not_returned(self, tmp_path): _make_jar(tmp_path.joinpath("app.jar"), main_class=None) with pytest.raises(FileNotFoundError): - _MainJar.find([tmp_path]) + _MainJar.find([tmp_path], "") + + def test_jar_with_main_class_but_no_schema_version_raises(self, tmp_path): + """A JAR with Main-Class but no Airflow-Supervisor-Schema-Version must raise ValueError.""" + _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main") + with pytest.raises(ValueError, match="Airflow-Supervisor-Schema-Version"): + _MainJar.find([tmp_path], "") def test_non_jar_files_skipped(self, tmp_path): tmp_path.joinpath("readme.txt").write_bytes(b"not a jar") - _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main") - assert _MainJar.find([tmp_path]) == _MainJar(tmp_path.joinpath("app.jar"), "com.example.Main", None) + _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main", schema_version="2026-06-16") + assert _MainJar.find([tmp_path], "") == _MainJar( + tmp_path.joinpath("app.jar"), "com.example.Main", "2026-06-16" + ) def test_first_jar_missing_main_class_falls_through_to_second(self, tmp_path): # Alphabetically: a.jar (no Main-Class), b.jar (has Main-Class). _make_jar(tmp_path.joinpath("a.jar"), main_class=None) - _make_jar(tmp_path.joinpath("b.jar"), main_class="com.example.Fallback") - assert _MainJar.find([tmp_path]) == _MainJar(tmp_path.joinpath("b.jar"), "com.example.Fallback", None) + _make_jar(tmp_path.joinpath("b.jar"), main_class="com.example.Fallback", schema_version="2026-06-16") + assert _MainJar.find([tmp_path], "") == _MainJar( + tmp_path.joinpath("b.jar"), "com.example.Fallback", "2026-06-16" + ) def test_fully_qualified_class_name_preserved(self, tmp_path): - _make_jar(tmp_path.joinpath("app.jar"), main_class="org.apache.airflow.sdk.java.TaskRunner") - assert _MainJar.find([tmp_path]) == _MainJar( + _make_jar( + tmp_path.joinpath("app.jar"), + main_class="org.apache.airflow.sdk.java.TaskRunner", + schema_version="2026-06-16", + ) + assert _MainJar.find([tmp_path], "") == _MainJar( path=tmp_path.joinpath("app.jar"), main_class="org.apache.airflow.sdk.java.TaskRunner", - schema_version=None, + schema_version="2026-06-16", ) + def test_find_by_explicit_main_class(self, tmp_path): + """When a main_class filter is given, only the matching JAR is returned.""" + _make_jar(tmp_path.joinpath("a.jar"), main_class="com.example.Alpha", schema_version="2026-06-16") + _make_jar(tmp_path.joinpath("b.jar"), main_class="com.example.Beta", schema_version="2026-06-16") + result = _MainJar.find([tmp_path], "com.example.Beta") + assert result.main_class == "com.example.Beta" + + def test_find_by_explicit_main_class_not_present_raises(self, tmp_path): + """When no JAR matches the main_class filter, FileNotFoundError is raised.""" + _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main", schema_version="2026-06-16") + with pytest.raises(FileNotFoundError, match="com.example.Missing"): + _MainJar.find([tmp_path], "com.example.Missing") + class TestAcceptConnections: def _connect_after_delay(self, addr: tuple[str, int], delay: float = 0.0) -> None: @@ -301,7 +330,7 @@ def test_custom_kwargs(self): @pytest.fixture def jars_root(tmp_path): - _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.TaskRunner") + _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.TaskRunner", schema_version="2026-06-16") return tmp_path @@ -490,6 +519,7 @@ def _start_with_mocks( java_executable=java_executable, jvm_args=jvm_args or [], jars_root=[jars_root], + main_class="", subprocess_logs_to_stdout=False, ) @@ -533,6 +563,7 @@ def test_stdin_is_comm_socket(self, jars_root, mock_client): java_executable="java", jvm_args=[], jars_root=[jars_root], + main_class="", subprocess_logs_to_stdout=False, ) @@ -563,6 +594,7 @@ def test_on_child_started_called(self, jars_root, mock_client): java_executable="java", jvm_args=[], jars_root=[jars_root], + main_class="", subprocess_logs_to_stdout=False, ) @@ -592,6 +624,7 @@ def test_register_pipe_readers_called_with_four_sockets(self, jars_root, mock_cl java_executable="java", jvm_args=[], jars_root=[jars_root], + main_class="", subprocess_logs_to_stdout=False, ) From 54335bf3ab42c521ffa5b2349d78b24128ad42f4 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Mon, 25 May 2026 19:11:17 +0800 Subject: [PATCH 68/75] Cleanup for errors during Java subprocess init --- .../sdk/coordinators/java/coordinator.py | 135 +++++++++++------- 1 file changed, 87 insertions(+), 48 deletions(-) diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py index 3be9bcd5b0510..fddb07dada657 100644 --- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py +++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py @@ -28,7 +28,7 @@ import subprocess import time import zipfile -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, TypeVar, cast import attrs import structlog @@ -47,6 +47,8 @@ from airflow.sdk.api.datamodels._generated import BundleInfo from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO + Tracked = TypeVar("Tracked", socket.socket, subprocess.Popen) + log: FilteringBoundLogger = structlog.get_logger(logger_name="coordinators.java") @@ -134,8 +136,12 @@ def _accept_connections( while len(accepted) < len(servers): remaining = deadline - time.monotonic() if remaining <= 0: + for s in accepted.values(): + s.close() raise TimeoutError("process did not connect within timeout") if proc.poll() is not None: + for s in accepted.values(): + s.close() raise RuntimeError(f"process exited with {proc.returncode} before connecting") for event, _ in sel.select(timeout=min(remaining, 1.0)): log.debug("Accepting child process connection", key=(key := event.data)) @@ -169,6 +175,36 @@ def wait(self, timeout: float | None) -> int: return self._impl.wait(timeout) +@attrs.define(kw_only=True) +class _ResourceTracker: + timeout: float + tracked: dict[int, socket.socket | subprocess.Popen] = attrs.field(init=False, factory=dict) + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + for o in self.tracked.values(): + match o: + case socket.socket(): + o.close() + case subprocess.Popen(): + o.terminate() + try: + o.wait(self.timeout) + except subprocess.TimeoutExpired: + o.kill() + + def track(self, *objects: Tracked) -> tuple[Tracked, ...]: + self.tracked.update((id(o), o) for o in objects) + return objects + + def untrack(self, *objects: Tracked) -> tuple[Tracked, ...]: + for o in objects: + self.tracked.pop(id(o), None) + return objects + + @attrs.define(kw_only=True) class _JavaActivitySubprocess(ActivitySubprocess): """Java task runner process.""" @@ -198,54 +234,57 @@ def start( # type: ignore[override] **kwargs, ) -> Self: jar = _MainJar.find(jars_root, main_class) + with _ResourceTracker(timeout=10.0) as tracker: + comm_server, logs_server = tracker.track(_start_server(), _start_server()) + stdout_r, stdout_w = tracker.track(*socket.socketpair()) + stderr_r, stderr_w = tracker.track(*socket.socketpair()) + + proc = subprocess.Popen( + [ + java_executable, + "-classpath", + _calculate_classpath(jars_root), + *jvm_args, + jar.main_class, + # Arguments to MainClass... + "--comm={0[0]}:{0[1]}".format(comm_server.getsockname()), + "--logs={0[0]}:{0[1]}".format(logs_server.getsockname()), + ], + stdout=stdout_w.makefile("wb", buffering=0).fileno(), + stderr=stderr_w.makefile("wb", buffering=0).fileno(), + ) + tracker.track(proc) + log.info("Starting subprocess", pid=proc.pid) + + socks = _accept_connections({"comm": comm_server, "logs": logs_server}, proc) + tracker.track(*socks.values()) + + self = cls( + id=what.id, + pid=proc.pid, + process=PopenTracker(proc), + process_log=logger or structlog.get_logger(logger_name="task").bind(), + start_time=time.monotonic(), + stdin=socks["comm"], + subprocess_schema_version=jar.schema_version, + comm_server=comm_server, + logs_server=logs_server, + stdout_w=stdout_w, + stderr_w=stderr_w, + **kwargs, + ) + self._register_pipe_readers(*tracker.untrack(stdout_r, stderr_r, socks["comm"], socks["logs"])) + self._on_child_started( + ti=what, + dag_rel_path=dag_rel_path, + bundle_info=bundle_info, + sentry_integration=sentry_integration, + ) + + # Untrack everything left. 'self' keeps track of these and close the + # servers when the subprocess exits in 'wait'. + tracker.untrack(comm_server, logs_server, proc) - comm_server = _start_server() - logs_server = _start_server() - - stdout_r, stdout_w = socket.socketpair() - stderr_r, stderr_w = socket.socketpair() - - comm_host, comm_port = comm_server.getsockname() - logs_host, logs_port = logs_server.getsockname() - - proc = subprocess.Popen( - [ - java_executable, - "-classpath", - _calculate_classpath(jars_root), - *jvm_args, - jar.main_class, - # Arguments to MainClass... - f"--comm={comm_host}:{comm_port}", - f"--logs={logs_host}:{logs_port}", - ], - stdout=stdout_w.makefile("wb", buffering=0).fileno(), - stderr=stderr_w.makefile("wb", buffering=0).fileno(), - ) - log.info("Starting subprocess", pid=proc.pid) - socks = _accept_connections({"comm": comm_server, "logs": logs_server}, proc) - - self = cls( - id=what.id, - pid=proc.pid, - process=PopenTracker(proc), - process_log=logger or structlog.get_logger(logger_name="task").bind(), - start_time=time.monotonic(), - stdin=socks["comm"], - subprocess_schema_version=jar.schema_version, - comm_server=comm_server, - logs_server=logs_server, - stdout_w=stdout_w, - stderr_w=stderr_w, - **kwargs, - ) - self._register_pipe_readers(stdout_r, stderr_r, socks["comm"], socks["logs"]) - self._on_child_started( - ti=what, - dag_rel_path=dag_rel_path, - bundle_info=bundle_info, - sentry_integration=sentry_integration, - ) return self def wait(self) -> int: From 78b0d7ab2e120d9e332cbb1627150ac8bd59688b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Mon, 25 May 2026 23:57:44 +0800 Subject: [PATCH 69/75] Drain child stdout/err while accepting conns This avoids the streams being filled up before the client connection. This should be unlikely (the child is supposed to connect first thing) but it's not difficult to handle so why not. --- .../src/airflow/dag_processing/processor.py | 14 +- .../src/airflow/jobs/triggerer_job_runner.py | 12 +- .../unit/dag_processing/test_processor.py | 4 +- .../sdk/coordinators/java/coordinator.py | 42 +++- .../airflow/sdk/execution_time/supervisor.py | 72 +++++-- .../coordinators/java/test_coordinator.py | 191 +++++++++++++++--- .../execution_time/test_supervisor.py | 99 +++++++++ 7 files changed, 375 insertions(+), 59 deletions(-) diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index 303b62d141111..f7b3affd00830 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -609,9 +609,19 @@ def _get_target_loggers(self) -> tuple[FilteringBoundLogger, ...]: ) def _create_log_forwarder( - self, loggers: tuple[FilteringBoundLogger, ...], name: str, log_level: int = logging.INFO + self, + loggers: tuple[FilteringBoundLogger, ...], + name: str, + *, + data: bytes, + log_level: int = logging.INFO, ) -> Callable[[socket], bool]: - return super()._create_log_forwarder(loggers, name.replace("task.", "dag_processor.", 1), log_level) + return super()._create_log_forwarder( + loggers, + name.replace("task.", "dag_processor.", 1), + data=data, + log_level=log_level, + ) def _handle_request(self, msg: ToManager, log: FilteringBoundLogger, req_id: int) -> None: from airflow.sdk.api.datamodels._generated import ( diff --git a/airflow-core/src/airflow/jobs/triggerer_job_runner.py b/airflow-core/src/airflow/jobs/triggerer_job_runner.py index 36102aaa0d3ad..cb8e45b343d5f 100644 --- a/airflow-core/src/airflow/jobs/triggerer_job_runner.py +++ b/airflow-core/src/airflow/jobs/triggerer_job_runner.py @@ -873,8 +873,16 @@ def update_triggers(self, requested_trigger_ids: set[int]): # Enqueue orphaned triggers for cancellation self.cancelling_triggers.update(cancel_trigger_ids) - def _register_pipe_readers(self, stdout: socket, stderr: socket, requests: socket, logs: socket): - super()._register_pipe_readers(stdout, stderr, requests, logs) + def _register_pipe_readers( + self, + stdout: socket, + stderr: socket, + requests: socket, + logs: socket, + *, + data: dict[socket, bytes], + ): + super()._register_pipe_readers(stdout, stderr, requests, logs, data=data) # We want to handle logging differently here, so un-register the one our parent class created self.selector.unregister(logs) diff --git a/airflow-core/tests/unit/dag_processing/test_processor.py b/airflow-core/tests/unit/dag_processing/test_processor.py index 7fe7204e68e2c..1ee02d0cbb813 100644 --- a/airflow-core/tests/unit/dag_processing/test_processor.py +++ b/airflow-core/tests/unit/dag_processing/test_processor.py @@ -2075,8 +2075,8 @@ def test_create_log_forwarder_rewrites_task_prefix_to_dag_processor(self, proc): from airflow.sdk.execution_time.supervisor import WatchedSubprocess with patch.object(WatchedSubprocess, "_create_log_forwarder") as mock_base: - proc._create_log_forwarder((), "task.stdout") - mock_base.assert_called_once_with((), "dag_processor.stdout", logging.INFO) + proc._create_log_forwarder((), "task.stdout", data=b"") + mock_base.assert_called_once_with((), "dag_processor.stdout", data=b"", log_level=logging.INFO) def test_handle_request_get_connection_masks_password_and_extra(self, proc): proc.client.connections.get.return_value = ConnectionResponse( diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py index fddb07dada657..5d7228c59a725 100644 --- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py +++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py @@ -20,6 +20,7 @@ from __future__ import annotations import email +import itertools import os import pathlib import selectors @@ -123,14 +124,17 @@ def find(cls, roots: Sequence[pathlib.Path], main_class: str) -> Self: def _accept_connections( servers: dict[str, socket.socket], + drains: dict[str, socket.socket], proc: subprocess.Popen, *, max_wait: float = 10.0, -) -> dict[str, socket.socket]: + drain_size: int = 4096, +) -> tuple[dict[socket.socket, socket.socket], dict[socket.socket, bytes]]: """Block until the Java process connects to servers.""" - accepted: dict[str, socket.socket] = {} + accepted: dict[socket.socket, socket.socket] = {} + drained: dict[socket.socket, bytes] = {s: b"" for s in drains.values()} with selectors.DefaultSelector() as sel: - for key, soc in servers.items(): + for key, soc in itertools.chain(servers.items(), drains.items()): sel.register(soc, selectors.EVENT_READ, data=key) deadline = time.monotonic() + max_wait while len(accepted) < len(servers): @@ -144,11 +148,20 @@ def _accept_connections( s.close() raise RuntimeError(f"process exited with {proc.returncode} before connecting") for event, _ in sel.select(timeout=min(remaining, 1.0)): - log.debug("Accepting child process connection", key=(key := event.data)) - conn, _ = cast("socket.socket", event.fileobj).accept() - sel.unregister(servers[key]) - accepted[key] = conn - return accepted + soc = cast("socket.socket", event.fileobj) + if soc in drained: + if incoming := soc.recv(drain_size): + log.debug("Draining child process stream", key=event.data) + drained[soc] += incoming + else: + log.warning("Child stream closed before ready!", key=event.data) + sel.unregister(soc) + else: + log.debug("Accepting child process connection", key=event.data) + conn, _ = soc.accept() + sel.unregister(soc) + accepted[soc] = conn + return accepted, drained class PopenTracker(ProcessTracker): @@ -256,7 +269,11 @@ def start( # type: ignore[override] tracker.track(proc) log.info("Starting subprocess", pid=proc.pid) - socks = _accept_connections({"comm": comm_server, "logs": logs_server}, proc) + socks, drained = _accept_connections( + {"comm": comm_server, "logs": logs_server}, + {"stdout": stdout_r, "stderr": stderr_r}, + proc, + ) tracker.track(*socks.values()) self = cls( @@ -265,7 +282,7 @@ def start( # type: ignore[override] process=PopenTracker(proc), process_log=logger or structlog.get_logger(logger_name="task").bind(), start_time=time.monotonic(), - stdin=socks["comm"], + stdin=socks[comm_server], subprocess_schema_version=jar.schema_version, comm_server=comm_server, logs_server=logs_server, @@ -273,7 +290,10 @@ def start( # type: ignore[override] stderr_w=stderr_w, **kwargs, ) - self._register_pipe_readers(*tracker.untrack(stdout_r, stderr_r, socks["comm"], socks["logs"])) + self._register_pipe_readers( + *tracker.untrack(stdout_r, stderr_r, socks[comm_server], socks[logs_server]), + data=drained, + ) self._on_child_started( ti=what, dag_rel_path=dag_rel_path, diff --git a/task-sdk/src/airflow/sdk/execution_time/supervisor.py b/task-sdk/src/airflow/sdk/execution_time/supervisor.py index 327feec5b826d..781e98ae2f67f 100644 --- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py +++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py @@ -747,15 +747,24 @@ def start( ) proc._register_pipe_readers( - stdout=read_stdout, - stderr=read_stderr, - requests=read_requests, - logs=read_logs, + read_stdout, + read_stderr, + read_requests, + read_logs, + data={}, ) return proc - def _register_pipe_readers(self, stdout: socket, stderr: socket, requests: socket, logs: socket): + def _register_pipe_readers( + self, + stdout: socket, + stderr: socket, + requests: socket, + logs: socket, + *, + data: dict[socket, bytes], + ): """Register handlers for subprocess communication channels.""" # self.selector is a way of registering a handler/callback to be called when the given IO channel has # activity to read on (https://www.man7.org/linux/man-pages/man2/select.2.html etc, but better @@ -775,12 +784,19 @@ def _register_pipe_readers(self, stdout: socket, stderr: socket, requests: socke target_loggers = self._get_target_loggers() self.selector.register( - stdout, selectors.EVENT_READ, self._create_log_forwarder(target_loggers, "task.stdout") + stdout, + selectors.EVENT_READ, + self._create_log_forwarder(target_loggers, "task.stdout", data=data.get(stdout, b"")), ) self.selector.register( stderr, selectors.EVENT_READ, - self._create_log_forwarder(target_loggers, "task.stderr", log_level=logging.ERROR), + self._create_log_forwarder( + target_loggers, + "task.stderr", + data=data.get(stderr, b""), + log_level=logging.ERROR, + ), ) self.selector.register( logs, @@ -802,7 +818,14 @@ def _get_target_loggers(self) -> tuple[FilteringBoundLogger, ...]: target_loggers += (log,) return target_loggers - def _create_log_forwarder(self, loggers, name, log_level=logging.INFO) -> Callable[[socket], bool]: + def _create_log_forwarder( + self, + loggers: tuple[FilteringBoundLogger, ...], + name: str, + *, + data: bytes, + log_level: int = logging.INFO, + ) -> Callable[[socket], bool]: """Create a socket handler that forwards logs to a logger.""" loggers = tuple( reconfigure_logger( @@ -812,7 +835,9 @@ def _create_log_forwarder(self, loggers, name, log_level=logging.INFO) -> Callab for log in loggers ) return make_buffered_socket_reader( - forward_to_log(loggers, logger=name, level=log_level), on_close=self._on_socket_closed + forward_to_log(loggers, logger=name, level=log_level), + data=data, + on_close=self._on_socket_closed, ) def _on_socket_closed(self, sock: socket): @@ -2136,15 +2161,29 @@ def run_task_in_process(ti: TaskInstance, task) -> TaskRunResult: # to a (sync) generator def make_buffered_socket_reader( gen: Generator[None, bytes | bytearray, None], + *, + data: bytes = b"", on_close: Callable[[socket], None], buffer_size: int = 4096, ): - buffer = bytearray() # This will hold our accumulated binary data + buffer = bytearray(data) # This will hold our accumulated binary data read_buffer = bytearray(buffer_size) # Temporary buffer for each read # We need to start up the generator to get it to the point it's at waiting on the yield next(gen) + def process(buffer: bytearray) -> bytearray: + # We could have read multiple lines in one go, yield them all + while (newline_pos := buffer.find(b"\n")) != -1: + line = buffer[: newline_pos + 1] + gen.send(line) + buffer = buffer[newline_pos + 1 :] # Update the buffer with remaining data + return buffer + + # Flush any complete lines that arrived before the selector was running. + with contextlib.suppress(StopIteration): + buffer = process(buffer) + def cb(sock: socket): nonlocal buffer, read_buffer # Read up to `buffer_size` bytes of data from the socket @@ -2158,15 +2197,10 @@ def cb(sock: socket): return False buffer.extend(read_buffer[:n_received]) - - # We could have read multiple lines in one go, yield them all - while (newline_pos := buffer.find(b"\n")) != -1: - line = buffer[: newline_pos + 1] - try: - gen.send(line) - except StopIteration: - return False - buffer = buffer[newline_pos + 1 :] # Update the buffer with remaining data + try: + buffer = process(buffer) + except StopIteration: + return False return True diff --git a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py index fa6080f530cbd..cbe03e2227beb 100644 --- a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py +++ b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py @@ -27,7 +27,7 @@ import threading import time import zipfile -from unittest.mock import MagicMock, patch +from unittest.mock import ANY, MagicMock, call, patch import pytest from uuid6 import uuid7 @@ -38,6 +38,7 @@ _calculate_classpath, _JavaActivitySubprocess, _MainJar, + _ResourceTracker, _start_server, ) from airflow.sdk.execution_time.coordinator import BaseCoordinator @@ -240,9 +241,9 @@ def test_accepts_single_server(self): mock_proc.poll.return_value = None try: - accepted = _accept_connections({"comm": server}, mock_proc) - assert "comm" in accepted - accepted["comm"].close() + accepted, _ = _accept_connections({"comm": server}, {}, mock_proc) + assert server in accepted + accepted[server].close() finally: server.close() @@ -259,8 +260,8 @@ def test_accepts_multiple_servers(self): mock_proc.poll.return_value = None try: - accepted = _accept_connections({"comm": comm_server, "logs": logs_server}, mock_proc) - assert set(accepted) == {"comm", "logs"} + accepted, _ = _accept_connections({"comm": comm_server, "logs": logs_server}, {}, mock_proc) + assert set(accepted) == {comm_server, logs_server} for sock in accepted.values(): sock.close() finally: @@ -273,7 +274,7 @@ def test_raises_timeout_when_no_connection(self): mock_proc.poll.return_value = None try: with pytest.raises(TimeoutError, match="did not connect within timeout"): - _accept_connections({"comm": server}, mock_proc, max_wait=0.05) + _accept_connections({"comm": server}, {}, mock_proc, max_wait=0.05) finally: server.close() @@ -285,7 +286,7 @@ def test_raises_runtime_error_if_process_exits_before_connecting(self): mock_proc.returncode = 1 try: with pytest.raises(RuntimeError, match="process exited with 1"): - _accept_connections({"comm": server}, mock_proc) + _accept_connections({"comm": server}, {}, mock_proc) finally: server.close() @@ -301,14 +302,144 @@ def test_returned_sockets_are_connected(self): mock_proc.poll.return_value = None try: - accepted = _accept_connections({"comm": server}, mock_proc) - accepted["comm"].sendall(b"hello") + accepted, _ = _accept_connections({"comm": server}, {}, mock_proc) + accepted[server].sendall(b"hello") assert client.recv(5) == b"hello" - accepted["comm"].close() + accepted[server].close() client.close() finally: server.close() + def test_empty_drains_returns_empty_drained_dict(self): + """When drains={} the returned drained mapping must also be empty.""" + server = _start_server() + _, port = server.getsockname() + self._connect_after_delay(("127.0.0.1", port)) + mock_proc = MagicMock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + try: + _, drained = _accept_connections({"comm": server}, {}, mock_proc) + assert drained == {} + finally: + server.close() + + def test_drain_socket_present_in_drained_dict(self): + """The drained dict must be keyed by the drain socket objects.""" + server = _start_server() + drain_r, drain_w = socket.socketpair() + _, port = server.getsockname() + self._connect_after_delay(("127.0.0.1", port)) + mock_proc = MagicMock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + try: + _, drained = _accept_connections({"comm": server}, {"stdout": drain_r}, mock_proc) + assert drain_r in drained + finally: + server.close() + drain_r.close() + drain_w.close() + + def test_bytes_written_to_drain_socket_are_returned(self): + """Bytes written to a drain socket before the connection is accepted + must be captured and returned in the drained dict.""" + server = _start_server() + drain_r, drain_w = socket.socketpair() + _, port = server.getsockname() + + drain_w.sendall(b"early output\n") + self._connect_after_delay(("127.0.0.1", port), delay=0.05) + + mock_proc = MagicMock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + try: + _, drained = _accept_connections({"comm": server}, {"stdout": drain_r}, mock_proc) + assert drained[drain_r] == b"early output\n" + finally: + server.close() + drain_r.close() + drain_w.close() + + def test_accepted_dict_keyed_by_server_socket_object(self): + """The returned accepted mapping must use server socket objects as keys, + not the string names passed in the servers dict.""" + server = _start_server() + _, port = server.getsockname() + self._connect_after_delay(("127.0.0.1", port)) + mock_proc = MagicMock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + try: + accepted, _ = _accept_connections({"comm": server}, {}, mock_proc) + # Key must be the socket object itself, not the string "comm" + assert server in accepted + assert "comm" not in accepted + accepted[server].close() + finally: + server.close() + + +class TestResourceTracker: + """Unit tests for the _ResourceTracker context manager introduced in this PR. + + _ResourceTracker tracks sockets and Popen objects and ensures they are + closed/terminated on context-manager exit, unless explicitly untracked + beforehand. + """ + + def test_track_returns_passed_objects_as_tuple(self): + tracker = _ResourceTracker(timeout=0.1) + sock = MagicMock(spec=socket.socket) + result = tracker.track(sock) + assert result == (sock,) + + def test_track_multiple_objects_returns_all(self): + tracker = _ResourceTracker(timeout=0.1) + sock1 = MagicMock(spec=socket.socket) + sock2 = MagicMock(spec=socket.socket) + result = tracker.track(sock1, sock2) + assert set(result) == {sock1, sock2} + + def test_untrack_returns_objects(self): + tracker = _ResourceTracker(timeout=0.1) + sock = MagicMock(spec=socket.socket) + tracker.track(sock) + result = tracker.untrack(sock) + assert result == (sock,) + + def test_context_manager_closes_tracked_socket_on_exit(self): + sock = MagicMock(spec=socket.socket) + with _ResourceTracker(timeout=0.1) as tracker: + tracker.track(sock) + sock.close.assert_called_once() + + def test_context_manager_terminates_tracked_popen_on_exit(self): + proc = MagicMock(spec=subprocess.Popen) + with _ResourceTracker(timeout=0.1) as tracker: + tracker.track(proc) + proc.terminate.assert_called_once() + + def test_untracked_socket_not_closed_on_exit(self): + sock = MagicMock(spec=socket.socket) + with _ResourceTracker(timeout=0.1) as tracker: + tracker.track(sock) + tracker.untrack(sock) + sock.close.assert_not_called() + + def test_only_remaining_tracked_objects_cleaned_up(self): + """After untracking one socket the other must still be closed.""" + sock_keep = MagicMock(spec=socket.socket) + sock_release = MagicMock(spec=socket.socket) + with _ResourceTracker(timeout=0.1) as tracker: + tracker.track(sock_keep, sock_release) + tracker.untrack(sock_release) + sock_keep.close.assert_called_once() + sock_release.close.assert_not_called() + + def test_untrack_unknown_object_does_not_raise(self): + sock = MagicMock(spec=socket.socket) + tracker = _ResourceTracker(timeout=0.1) + # Untracking something never tracked must be a no-op, not an error + tracker.untrack(sock) + class TestJavaCoordinatorAttributes: def test_default_kwargs(self): @@ -375,7 +506,10 @@ def capture_popen(cmd, **kwargs): ), patch( "airflow.sdk.coordinators.java.coordinator._accept_connections", - return_value={"comm": comm_sock, "logs": logs_sock}, + side_effect=lambda servers, drains, proc, **kw: ( + {servers["comm"]: comm_sock, servers["logs"]: logs_sock}, + {soc: b"" for soc in drains.values()}, + ), ), patch.object(ActivitySubprocess, "_register_pipe_readers"), patch.object(ActivitySubprocess, "_on_child_started"), @@ -453,7 +587,10 @@ def test_returns_execution_result(self, jars_root, mock_client): patch("subprocess.Popen", return_value=mock_proc), patch( "airflow.sdk.coordinators.java.coordinator._accept_connections", - return_value={"comm": comm_sock, "logs": logs_sock}, + side_effect=lambda servers, drains, proc, **kw: ( + {servers["comm"]: comm_sock, servers["logs"]: logs_sock}, + {soc: b"" for soc in drains.values()}, + ), ), patch.object(ActivitySubprocess, "_register_pipe_readers"), patch.object(ActivitySubprocess, "_on_child_started"), @@ -505,7 +642,10 @@ def _start_with_mocks( ) as popen_mock, patch( "airflow.sdk.coordinators.java.coordinator._accept_connections", - return_value={"comm": comm_sock, "logs": logs_sock}, + side_effect=lambda servers, drains, proc, **kw: ( + {servers["comm"]: comm_sock, servers["logs"]: logs_sock}, + {soc: b"" for soc in drains.values()}, + ), ), patch.object(ActivitySubprocess, "_register_pipe_readers"), patch.object(ActivitySubprocess, "_on_child_started"), @@ -548,7 +688,10 @@ def test_stdin_is_comm_socket(self, jars_root, mock_client): patch("airflow.sdk.coordinators.java.coordinator.subprocess.Popen") as popen_mock, patch( "airflow.sdk.coordinators.java.coordinator._accept_connections", - return_value={"comm": comm_sock, "logs": logs_sock}, + side_effect=lambda servers, drains, proc, **kw: ( + {servers["comm"]: comm_sock, servers["logs"]: logs_sock}, + {soc: b"" for soc in drains.values()}, + ), ), patch.object(ActivitySubprocess, "_register_pipe_readers"), patch.object(ActivitySubprocess, "_on_child_started"), @@ -579,7 +722,10 @@ def test_on_child_started_called(self, jars_root, mock_client): patch("airflow.sdk.coordinators.java.coordinator.subprocess.Popen") as popen_mock, patch( "airflow.sdk.coordinators.java.coordinator._accept_connections", - return_value={"comm": MagicMock(), "logs": MagicMock()}, + side_effect=lambda servers, drains, proc, **kw: ( + {soc: MagicMock(spec=socket.socket) for soc in servers.values()}, + {soc: b"" for soc in drains.values()}, + ), ), patch.object(ActivitySubprocess, "_register_pipe_readers"), patch.object(ActivitySubprocess, "_on_child_started") as mock_on_started, @@ -604,12 +750,15 @@ def test_on_child_started_called(self, jars_root, mock_client): assert kwargs["dag_rel_path"] == "dags/test.jar" def test_register_pipe_readers_called_with_four_sockets(self, jars_root, mock_client): - """Both socketpair read-ends and both TCP sockets must be registered.""" + """Both socketpair read-ends and both TCP sockets must be registered, with a data kwarg.""" with ( patch("airflow.sdk.coordinators.java.coordinator.subprocess.Popen") as popen_mock, patch( "airflow.sdk.coordinators.java.coordinator._accept_connections", - return_value={"comm": MagicMock(), "logs": MagicMock()}, + side_effect=lambda servers, drains, proc, **kw: ( + {soc: MagicMock(spec=socket.socket) for soc in servers.values()}, + {soc: b"" for soc in drains.values()}, + ), ), patch.object(ActivitySubprocess, "_register_pipe_readers") as mock_register, patch.object(ActivitySubprocess, "_on_child_started"), @@ -627,8 +776,4 @@ def test_register_pipe_readers_called_with_four_sockets(self, jars_root, mock_cl main_class="", subprocess_logs_to_stdout=False, ) - - mock_register.assert_called_once() - args = mock_register.call_args.args - # positional: stdout, stderr, comm, logs — all four must be sockets - assert len(args) == 4 + assert mock_register.mock_calls == [call(ANY, ANY, ANY, ANY, data=ANY)] diff --git a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py index 684d9460529d0..798500d381c45 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py +++ b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py @@ -164,6 +164,7 @@ _make_process_nondumpable, _remote_logging_conn, in_process_api_server, + make_buffered_socket_reader, process_log_messages_from_subprocess, set_supervisor_comms, supervise_task, @@ -4081,3 +4082,101 @@ def capture_on_get(key): assert captured == [expected_span_id] # Context is detached after dispatch — no leak. assert get_current_span().get_span_context().span_id != expected_span_id + + +class TestMakeBufferedSocketReader: + """Tests for the data= pre-seeding parameter added to make_buffered_socket_reader. + + The ``data`` kwarg lets callers inject bytes that arrived on the socket + before the selector loop started (e.g. bytes drained from a subprocess + stdout/stderr pipe while waiting for a TCP handshake). Complete lines + in ``data`` must be dispatched to the generator immediately on + construction; partial lines must be held in the buffer until the socket + delivers the remainder. + """ + + def _collecting_gen(self, received: list[bytes]): + """Generator that appends every line sent to it into *received*.""" + while True: + line = yield + received.append(bytes(line)) + + def test_empty_data_dispatches_nothing_before_socket_reads(self): + received: list[bytes] = [] + on_close = MagicMock() + make_buffered_socket_reader(self._collecting_gen(received), data=b"", on_close=on_close) + assert received == [] + + def test_complete_line_in_data_dispatched_immediately(self): + received: list[bytes] = [] + on_close = MagicMock() + make_buffered_socket_reader(self._collecting_gen(received), data=b"hello\n", on_close=on_close) + assert received == [b"hello\n"] + + def test_multiple_complete_lines_in_data_all_dispatched(self): + received: list[bytes] = [] + on_close = MagicMock() + make_buffered_socket_reader( + self._collecting_gen(received), data=b"line1\nline2\nline3\n", on_close=on_close + ) + assert received == [b"line1\n", b"line2\n", b"line3\n"] + + def test_partial_line_in_data_held_until_socket_completes_it(self): + """A line without a trailing newline in data must not be dispatched + until the socket delivers the rest of the line.""" + received: list[bytes] = [] + on_close = MagicMock() + r, w = socket.socketpair() + try: + cb, _ = make_buffered_socket_reader( + self._collecting_gen(received), data=b"partial", on_close=on_close + ) + assert received == [] # incomplete line — must not fire yet + w.sendall(b" rest\n") + cb(r) + assert received == [b"partial rest\n"] + finally: + r.close() + w.close() + + def test_complete_and_partial_lines_in_data_only_complete_dispatched(self): + """Only the complete line is flushed on construction; the trailing + fragment is retained and completed by subsequent socket reads.""" + received: list[bytes] = [] + on_close = MagicMock() + r, w = socket.socketpair() + try: + cb, _ = make_buffered_socket_reader( + self._collecting_gen(received), data=b"full\nincomplete", on_close=on_close + ) + assert received == [b"full\n"] + w.sendall(b" suffix\n") + cb(r) + assert received == [b"full\n", b"incomplete suffix\n"] + finally: + r.close() + w.close() + + def test_data_pre_seeding_does_not_trigger_on_close(self): + """Flushing pre-seeded lines must never invoke the on_close callback.""" + received: list[bytes] = [] + on_close = MagicMock() + make_buffered_socket_reader(self._collecting_gen(received), data=b"line\n", on_close=on_close) + on_close.assert_not_called() + + def test_socket_data_appended_after_pre_seeded_data(self): + """Bytes arriving on the socket are appended after any pre-seeded + partial content, preserving message order.""" + received: list[bytes] = [] + on_close = MagicMock() + r, w = socket.socketpair() + try: + cb, _ = make_buffered_socket_reader( + self._collecting_gen(received), data=b"seeded\n", on_close=on_close + ) + w.sendall(b"live\n") + cb(r) + assert received == [b"seeded\n", b"live\n"] + finally: + r.close() + w.close() From b11ee9faa065d71471747c47bc76d00523bd4af5 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 26 May 2026 00:14:19 +0800 Subject: [PATCH 70/75] Close stdout/err write sockets in supervisor The write ends are only used by the child to send data. They are not used by the supervisor past this point. --- .../airflow/sdk/coordinators/java/coordinator.py | 16 +++++----------- .../coordinators/java/test_coordinator.py | 13 ------------- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py index 5d7228c59a725..2c3bf2c65afd8 100644 --- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py +++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py @@ -225,12 +225,6 @@ class _JavaActivitySubprocess(ActivitySubprocess): _comm_server: socket.socket _logs_server: socket.socket - # Keep track of channels used to pipe subprocess stdout and stderr so we can - # close them on exit. The "read" side is handled by _register_pipe_readers - # callbacks so we don't need to worry about them. - _stdout_w: socket.socket - _stderr_w: socket.socket - @classmethod def start( # type: ignore[override] cls, @@ -263,10 +257,12 @@ def start( # type: ignore[override] "--comm={0[0]}:{0[1]}".format(comm_server.getsockname()), "--logs={0[0]}:{0[1]}".format(logs_server.getsockname()), ], - stdout=stdout_w.makefile("wb", buffering=0).fileno(), - stderr=stderr_w.makefile("wb", buffering=0).fileno(), + stdout=stdout_w.fileno(), + stderr=stderr_w.fileno(), ) tracker.track(proc) + for soc in tracker.untrack(stdout_w, stderr_w): + soc.close() log.info("Starting subprocess", pid=proc.pid) socks, drained = _accept_connections( @@ -286,8 +282,6 @@ def start( # type: ignore[override] subprocess_schema_version=jar.schema_version, comm_server=comm_server, logs_server=logs_server, - stdout_w=stdout_w, - stderr_w=stderr_w, **kwargs, ) self._register_pipe_readers( @@ -309,7 +303,7 @@ def start( # type: ignore[override] def wait(self) -> int: code = super().wait() - self._close_unused_sockets(self._comm_server, self._logs_server, self._stdout_w, self._stderr_w) + self._close_unused_sockets(self._comm_server, self._logs_server) return code diff --git a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py index cbe03e2227beb..91e491055a2d5 100644 --- a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py +++ b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py @@ -665,19 +665,6 @@ def _start_with_mocks( return proc, popen_mock - def test_stdout_write_socket_stored_for_cleanup(self, jars_root, mock_client): - proc, _ = self._start_with_mocks(jars_root, mock_client) - # _stdout_w must be stored so wait() can close it - assert proc._stdout_w is not None - - def test_stderr_write_socket_stored_for_cleanup(self, jars_root, mock_client): - proc, _ = self._start_with_mocks(jars_root, mock_client) - assert proc._stderr_w is not None - - def test_stdout_and_stderr_write_sockets_are_distinct(self, jars_root, mock_client): - proc, _ = self._start_with_mocks(jars_root, mock_client) - assert proc._stdout_w is not proc._stderr_w - def test_stdin_is_comm_socket(self, jars_root, mock_client): """stdin (used by send_msg) must be the accepted comm socket.""" ti = _make_ti() From ae8c4925c825800a47ee72e220a69dc8fc3b5556 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 26 May 2026 19:19:15 +0800 Subject: [PATCH 71/75] Find JAR metadata separately This allows Main-Class and Airflow-Supervisor-Schema-Version to be set in different JARs. In practice, Main-Class should be on the dag bundle JAR, and the schema version can simply be set on the SDK JAR without affecting the user's JAR. --- .../sdk/coordinators/java/coordinator.py | 46 +++++++++++++------ .../coordinators/java/test_coordinator.py | 29 +++++------- 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py index 2c3bf2c65afd8..988bc42bf1a02 100644 --- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py +++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py @@ -93,30 +93,46 @@ def _validate_schema_version(instance, _, value) -> str: @attrs.define -class _MainJar: - path: pathlib.Path +class _JarInfo: main_class: str schema_version: str = attrs.field(validator=_validate_schema_version) + @attrs.define + class _Progress: + main_class: str | None = attrs.field(init=False, default=None) + schema_version: str | None = attrs.field(init=False, default=None) + + def collect(self) -> _JarInfo | None: + if self.main_class is None or self.schema_version is None: + return None + return _JarInfo(self.main_class, self.schema_version) + @classmethod - def find(cls, roots: Sequence[pathlib.Path], main_class: str) -> Self: + def find(cls, roots: Sequence[pathlib.Path], main_class: str) -> _JarInfo: + progress = cls._Progress() for root in roots: - log.debug("Finding Main-Class in directory", dir=root) + log.debug("Finding required JAR metadata in directory", dir=root) for p in root.iterdir(): if p.suffix != ".jar": continue if (metadata := _JarMetadata.from_jar(p)) is None: continue - if not metadata.main_class: - continue - if main_class and metadata.main_class != main_class: - continue - log.debug("JAR located with Main-Class metadata", path=p, main_class=metadata.main_class) - if not metadata.schema_version: - raise ValueError("JAR with Main-Class found without Airflow-Supervisor-Schema-Version") - return cls(p, metadata.main_class, metadata.schema_version) - if main_class: - tp = "cannot find a JAR with Main-Class matching {0} in {1}" + if metadata.main_class and ((main_class == metadata.main_class) or not main_class): + log.debug("JAR located with Main-Class metadata", path=p, main_class=metadata.main_class) + progress.main_class = metadata.main_class + if metadata.schema_version: + log.debug( + "JAR located with Airflow-Supervisor-Schema-Version metadata", + path=p, + schema_version=metadata.schema_version, + ) + progress.schema_version = metadata.schema_version + if (result := progress.collect()) is not None: + return result + if progress.main_class is not None: + tp = "cannot find a JAR with Airflow-Supervisor-Schema-Version metadata in {1}" + elif main_class: + tp = "cannot find a JAR with Main-Class matching {0!r} in {1}" else: tp = "cannot find a JAR with Main-Class metadata in {1}" raise FileNotFoundError(tp.format(main_class, os.pathsep.join(os.fspath(p.resolve()) for p in roots))) @@ -240,7 +256,7 @@ def start( # type: ignore[override] main_class: str, **kwargs, ) -> Self: - jar = _MainJar.find(jars_root, main_class) + jar = _JarInfo.find(jars_root, main_class) with _ResourceTracker(timeout=10.0) as tracker: comm_server, logs_server = tracker.track(_start_server(), _start_server()) stdout_r, stdout_w = tracker.track(*socket.socketpair()) diff --git a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py index 91e491055a2d5..58014d76e32a2 100644 --- a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py +++ b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py @@ -36,8 +36,8 @@ JavaCoordinator, _accept_connections, _calculate_classpath, + _JarInfo, _JavaActivitySubprocess, - _MainJar, _ResourceTracker, _start_server, ) @@ -162,39 +162,33 @@ def test_empty_directory_returns_empty_string(self, tmp_path): class TestMainJar: def test_returns_main_class_from_jar(self, tmp_path): _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main", schema_version="2026-06-16") - assert _MainJar.find([tmp_path], "") == _MainJar( - tmp_path.joinpath("app.jar"), "com.example.Main", "2026-06-16" - ) + assert _JarInfo.find([tmp_path], "") == _JarInfo("com.example.Main", "2026-06-16") def test_no_jars_raises_file_not_found(self, tmp_path): with pytest.raises(FileNotFoundError, match=re.escape(str(tmp_path.resolve()))): - _MainJar.find([tmp_path], "") + _JarInfo.find([tmp_path], "") def test_jar_without_main_class_not_returned(self, tmp_path): _make_jar(tmp_path.joinpath("app.jar"), main_class=None) with pytest.raises(FileNotFoundError): - _MainJar.find([tmp_path], "") + _JarInfo.find([tmp_path], "") def test_jar_with_main_class_but_no_schema_version_raises(self, tmp_path): """A JAR with Main-Class but no Airflow-Supervisor-Schema-Version must raise ValueError.""" _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main") - with pytest.raises(ValueError, match="Airflow-Supervisor-Schema-Version"): - _MainJar.find([tmp_path], "") + with pytest.raises(FileNotFoundError, match="Airflow-Supervisor-Schema-Version"): + _JarInfo.find([tmp_path], "") def test_non_jar_files_skipped(self, tmp_path): tmp_path.joinpath("readme.txt").write_bytes(b"not a jar") _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main", schema_version="2026-06-16") - assert _MainJar.find([tmp_path], "") == _MainJar( - tmp_path.joinpath("app.jar"), "com.example.Main", "2026-06-16" - ) + assert _JarInfo.find([tmp_path], "") == _JarInfo("com.example.Main", "2026-06-16") def test_first_jar_missing_main_class_falls_through_to_second(self, tmp_path): # Alphabetically: a.jar (no Main-Class), b.jar (has Main-Class). _make_jar(tmp_path.joinpath("a.jar"), main_class=None) _make_jar(tmp_path.joinpath("b.jar"), main_class="com.example.Fallback", schema_version="2026-06-16") - assert _MainJar.find([tmp_path], "") == _MainJar( - tmp_path.joinpath("b.jar"), "com.example.Fallback", "2026-06-16" - ) + assert _JarInfo.find([tmp_path], "") == _JarInfo("com.example.Fallback", "2026-06-16") def test_fully_qualified_class_name_preserved(self, tmp_path): _make_jar( @@ -202,8 +196,7 @@ def test_fully_qualified_class_name_preserved(self, tmp_path): main_class="org.apache.airflow.sdk.java.TaskRunner", schema_version="2026-06-16", ) - assert _MainJar.find([tmp_path], "") == _MainJar( - path=tmp_path.joinpath("app.jar"), + assert _JarInfo.find([tmp_path], "") == _JarInfo( main_class="org.apache.airflow.sdk.java.TaskRunner", schema_version="2026-06-16", ) @@ -212,14 +205,14 @@ def test_find_by_explicit_main_class(self, tmp_path): """When a main_class filter is given, only the matching JAR is returned.""" _make_jar(tmp_path.joinpath("a.jar"), main_class="com.example.Alpha", schema_version="2026-06-16") _make_jar(tmp_path.joinpath("b.jar"), main_class="com.example.Beta", schema_version="2026-06-16") - result = _MainJar.find([tmp_path], "com.example.Beta") + result = _JarInfo.find([tmp_path], "com.example.Beta") assert result.main_class == "com.example.Beta" def test_find_by_explicit_main_class_not_present_raises(self, tmp_path): """When no JAR matches the main_class filter, FileNotFoundError is raised.""" _make_jar(tmp_path.joinpath("app.jar"), main_class="com.example.Main", schema_version="2026-06-16") with pytest.raises(FileNotFoundError, match="com.example.Missing"): - _MainJar.find([tmp_path], "com.example.Missing") + _JarInfo.find([tmp_path], "com.example.Missing") class TestAcceptConnections: From 44c8a337b39e64d694e0583b8c76adf5a8957749 Mon Sep 17 00:00:00 2001 From: TP Date: Tue, 26 May 2026 20:14:25 +0800 Subject: [PATCH 72/75] Correctly indent config example block --- devel-common/src/sphinx_exts/includes/sections-and-options.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel-common/src/sphinx_exts/includes/sections-and-options.rst b/devel-common/src/sphinx_exts/includes/sections-and-options.rst index e04383c8c5582..edb674adb843f 100644 --- a/devel-common/src/sphinx_exts/includes/sections-and-options.rst +++ b/devel-common/src/sphinx_exts/includes/sections-and-options.rst @@ -85,7 +85,7 @@ {% if "\n" in example %} .. code-block:: - {{ example }} + {{ example | indent(width=8) }} {% else %} ``{{ example }}`` {% endif %} From 7d57673fd3979f00436edc18afb5ebd6650232db Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 27 May 2026 15:03:08 +0800 Subject: [PATCH 73/75] fixup! Move ExecutorCallback execution into a supervised process (#62645) --- airflow-core/src/airflow/executors/base_executor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/airflow-core/src/airflow/executors/base_executor.py b/airflow-core/src/airflow/executors/base_executor.py index 5080e75750a48..eff6ff0771474 100644 --- a/airflow-core/src/airflow/executors/base_executor.py +++ b/airflow-core/src/airflow/executors/base_executor.py @@ -652,10 +652,11 @@ def run_workload( if isinstance(workload, ExecuteTask): from airflow.sdk.execution_time.supervisor import supervise_task - from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO as SDKTaskInstanceDTO + # workload.ti is a TaskInstanceDTO which duck-types as TaskInstance. + # TODO: Create a protocol for this. return supervise_task( - ti=SDKTaskInstanceDTO.model_validate(workload.ti, from_attributes=True), + ti=workload.ti, # type: ignore[arg-type] bundle_info=workload.bundle_info, dag_rel_path=workload.dag_rel_path, token=workload.token, From 6ab32d0b79b3756ad7041d6eb6f69171fdfffcf3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 27 May 2026 15:52:00 +0800 Subject: [PATCH 74/75] Expose startup timeout in coordinator kwargs --- .../sdk/coordinators/java/coordinator.py | 69 ++++++++++++------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py index 988bc42bf1a02..138c91cecf1cd 100644 --- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py +++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py @@ -39,7 +39,7 @@ from airflow.sdk.execution_time.supervisor import ActivitySubprocess, NeverRaised, ProcessTracker if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Iterable, Iterator, Sequence from structlog.typing import FilteringBoundLogger from typing_extensions import Self @@ -61,9 +61,17 @@ def _start_server() -> socket.socket: return server +def _find_jars(items: Iterable[pathlib.Path]) -> Iterator[pathlib.Path]: + for item in items: + if item.is_dir(): + yield from _find_jars(item.iterdir()) + elif item.is_file() and item.suffix == ".jar": + yield item + + def _calculate_classpath(jars_root: Sequence[pathlib.Path]) -> str: - jars = (p.as_posix() for root in jars_root for p in root.iterdir() if p.suffix == ".jar") - return os.pathsep.join(jars) + jars = (p.as_posix() for p in _find_jars(jars_root)) + return os.pathsep.join(sorted(jars)) # Keep output deterministic. @attrs.define @@ -109,26 +117,23 @@ def collect(self) -> _JarInfo | None: @classmethod def find(cls, roots: Sequence[pathlib.Path], main_class: str) -> _JarInfo: + log.debug("Finding JARs recursively", roots=roots) progress = cls._Progress() - for root in roots: - log.debug("Finding required JAR metadata in directory", dir=root) - for p in root.iterdir(): - if p.suffix != ".jar": - continue - if (metadata := _JarMetadata.from_jar(p)) is None: - continue - if metadata.main_class and ((main_class == metadata.main_class) or not main_class): - log.debug("JAR located with Main-Class metadata", path=p, main_class=metadata.main_class) - progress.main_class = metadata.main_class - if metadata.schema_version: - log.debug( - "JAR located with Airflow-Supervisor-Schema-Version metadata", - path=p, - schema_version=metadata.schema_version, - ) - progress.schema_version = metadata.schema_version - if (result := progress.collect()) is not None: - return result + for p in _find_jars(roots): + if (metadata := _JarMetadata.from_jar(p)) is None: + continue + if metadata.main_class and ((main_class == metadata.main_class) or not main_class): + log.debug("JAR located with Main-Class metadata", path=p, main_class=metadata.main_class) + progress.main_class = metadata.main_class + if metadata.schema_version: + log.debug( + "JAR located with Airflow-Supervisor-Schema-Version metadata", + path=p, + schema_version=metadata.schema_version, + ) + progress.schema_version = metadata.schema_version + if (result := progress.collect()) is not None: + return result if progress.main_class is not None: tp = "cannot find a JAR with Airflow-Supervisor-Schema-Version metadata in {1}" elif main_class: @@ -254,10 +259,11 @@ def start( # type: ignore[override] jvm_args: list[str], jars_root: Sequence[pathlib.Path], main_class: str, + startup_timeout: float = 10.0, **kwargs, ) -> Self: jar = _JarInfo.find(jars_root, main_class) - with _ResourceTracker(timeout=10.0) as tracker: + with _ResourceTracker(timeout=startup_timeout) as tracker: comm_server, logs_server = tracker.track(_start_server(), _start_server()) stdout_r, stdout_w = tracker.track(*socket.socketpair()) stderr_r, stderr_w = tracker.track(*socket.socketpair()) @@ -285,6 +291,7 @@ def start( # type: ignore[override] {"comm": comm_server, "logs": logs_server}, {"stdout": stdout_r, "stderr": stderr_r}, proc, + max_wait=startup_timeout, ) tracker.track(*socks.values()) @@ -356,6 +363,8 @@ class JavaCoordinator(BaseCoordinator): :param jvm_args: Extra arguments passed to the JVM (e.g. ``["-Xmx512m"]``). :param jars_root: A list of directories scanned for JAR bundles. :param main_class: Explicit entry point to execute with *java_executable*. + :param task_startup_timeout: Maximum time the coordinator waits for a task + process to start, in seconds. The default is 10 seconds. If *main_class* is not explicitly set, JavaCoordinator scans *jars_root* to find an executable JAR (one with Main-Class set in its metadata). If more @@ -367,12 +376,23 @@ class JavaCoordinator(BaseCoordinator): SDK automatically sets this, so you don't generally need to do anything if dependency JARs are deployed as-is. If you repackage the dependencies, however, you must also reproduce the metadata entry in one of the JARs. + + The default *task_startup_timeout* should plenty long enough since a task- + containing JAR is not supposed to consume significant time to perform setup + (it should happen in individual tasks instead). However, if the launch time + has to be so slow, you can increase the timeout to give the JAR more time. + Note that decreasing the value is generally not meaningful since the + coordinator does not need to wait for the full period. """ java_executable: str = "java" jvm_args: list[str] = attrs.field(factory=list) - jars_root: list[pathlib.Path] = attrs.field(converter=_convert_jars_root, factory=list) + jars_root: list[pathlib.Path] = attrs.field( + converter=_convert_jars_root, + validator=attrs.validators.min_len(1), + ) main_class: str = "" + task_startup_timeout: float = 10.0 def execute_task( self, @@ -398,6 +418,7 @@ def execute_task( jvm_args=self.jvm_args, jars_root=self.jars_root, main_class=self.main_class, + startup_timeout=self.task_startup_timeout, ) exit_code = process.wait() return self.ExecutionResult(exit_code, process.final_state) From 110c34cb55cf799d9c516aaff91cda609cfd0ac1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 27 May 2026 23:09:12 +0800 Subject: [PATCH 75/75] Fix Supervisor Schema output --- .../sdk/execution_time/schema/schema.json | 231 ++++++++++++------ 1 file changed, 159 insertions(+), 72 deletions(-) diff --git a/task-sdk/src/airflow/sdk/execution_time/schema/schema.json b/task-sdk/src/airflow/sdk/execution_time/schema/schema.json index ce6cbb6b73a3a..6f07e7e3cb18c 100644 --- a/task-sdk/src/airflow/sdk/execution_time/schema/schema.json +++ b/task-sdk/src/airflow/sdk/execution_time/schema/schema.json @@ -3582,7 +3582,7 @@ "StartupDetails": { "properties": { "ti": { - "$ref": "#/$defs/TaskInstance" + "$ref": "#/$defs/TaskInstanceDTO" }, "dag_rel_path": { "title": "Dag Rel Path", @@ -3817,6 +3817,164 @@ "title": "TaskCallbackRequest", "type": "object" }, + "TaskInstance": { + "description": "Schema for TaskInstance model with minimal required fields needed for Runtime.", + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "task_id": { + "title": "Task Id", + "type": "string" + }, + "dag_id": { + "title": "Dag Id", + "type": "string" + }, + "run_id": { + "title": "Run Id", + "type": "string" + }, + "try_number": { + "title": "Try Number", + "type": "integer" + }, + "dag_version_id": { + "format": "uuid", + "title": "Dag Version Id", + "type": "string" + }, + "map_index": { + "default": -1, + "title": "Map Index", + "type": "integer" + }, + "hostname": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hostname" + }, + "context_carrier": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Context Carrier" + } + }, + "required": [ + "id", + "task_id", + "dag_id", + "run_id", + "try_number", + "dag_version_id" + ], + "title": "TaskInstance", + "type": "object" + }, + "TaskInstanceDTO": { + "description": "Task SDK TaskInstanceDTO.", + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "dag_version_id": { + "format": "uuid", + "title": "Dag Version Id", + "type": "string" + }, + "task_id": { + "title": "Task Id", + "type": "string" + }, + "dag_id": { + "title": "Dag Id", + "type": "string" + }, + "run_id": { + "title": "Run Id", + "type": "string" + }, + "try_number": { + "title": "Try Number", + "type": "integer" + }, + "map_index": { + "default": -1, + "title": "Map Index", + "type": "integer" + }, + "pool_slots": { + "title": "Pool Slots", + "type": "integer" + }, + "queue": { + "title": "Queue", + "type": "string" + }, + "priority_weight": { + "title": "Priority Weight", + "type": "integer" + }, + "parent_context_carrier": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Parent Context Carrier" + }, + "context_carrier": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Context Carrier" + } + }, + "required": [ + "id", + "dag_version_id", + "task_id", + "dag_id", + "run_id", + "try_number", + "pool_slots", + "queue", + "priority_weight" + ], + "title": "TaskInstanceDTO", + "type": "object" + }, "TaskInstanceState": { "description": "All possible states that a Task Instance can be in.\n\nNote that None is also allowed, so always use this in a type hint with Optional.", "enum": [ @@ -4697,77 +4855,6 @@ "title": "TIRunContext", "type": "object" }, - "TaskInstance": { - "description": "Schema for TaskInstance model with minimal required fields needed for Runtime.", - "properties": { - "id": { - "format": "uuid", - "title": "Id", - "type": "string" - }, - "task_id": { - "title": "Task Id", - "type": "string" - }, - "dag_id": { - "title": "Dag Id", - "type": "string" - }, - "run_id": { - "title": "Run Id", - "type": "string" - }, - "try_number": { - "title": "Try Number", - "type": "integer" - }, - "dag_version_id": { - "format": "uuid", - "title": "Dag Version Id", - "type": "string" - }, - "map_index": { - "default": -1, - "title": "Map Index", - "type": "integer" - }, - "hostname": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Hostname" - }, - "context_carrier": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Context Carrier" - } - }, - "required": [ - "id", - "task_id", - "dag_id", - "run_id", - "try_number", - "dag_version_id" - ], - "title": "TaskInstance", - "type": "object" - }, "VariableResponse": { "additionalProperties": false, "description": "Variable schema for responses with fields that are needed for Runtime.",