From dfa07b02cc3bc1c101bda61417318d5d7529bf3c Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 8 Apr 2026 14:17:15 +0800 Subject: [PATCH 01/80] 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 dd2e24d59b9903377d6596fed79d32d0f671d57e Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 8 Apr 2026 15:40:05 +0800 Subject: [PATCH 02/80] 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 4a988db2d3ea7..119a45515af20 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", @@ -1170,6 +1174,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", @@ -1437,6 +1443,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 @@ -1588,6 +1595,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 @@ -1749,6 +1757,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 } @@ -1886,6 +1895,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 e0084ec1cb511..5f2a6b2a6ce03 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", @@ -1001,6 +1003,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" }, @@ -1251,6 +1254,9 @@ kerberos = [ keycloak = [ { name = "apache-airflow-providers-keycloak" }, ] +languages-java = [ + { name = "apache-airflow-providers-languages-java" }, +] ldap = [ { name = "python-ldap" }, ] @@ -1587,6 +1593,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" }, @@ -5813,6 +5821,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.1.2" From 9a771f52f294680387f53b5561c3ad6ec88e1005 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 10 Apr 2026 14:33:41 +0800 Subject: [PATCH 03/80] 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 | 80 +-------- 2 files changed, 165 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 cd25d9279571b..6630979db4cdd 100644 --- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py +++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py @@ -126,6 +126,7 @@ handle_get_variable, handle_mask_secret, ) +from airflow.sdk.execution_time.selector_loop import make_buffered_socket_reader, service_selector try: from socket import send_fds @@ -144,6 +145,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"] @@ -695,7 +697,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( @@ -888,41 +890,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) @@ -1932,50 +1908,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 a8386fcad3e7f91acbdf958f2246e15185251a73 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 9 Apr 2026 16:12:37 +0800 Subject: [PATCH 04/80] 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 aa9f07411f87d..73605d5817b8c 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 @@ -161,6 +162,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. @@ -553,7 +599,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, @@ -566,6 +619,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 4c2ee0e70de535a27894f766fb1383b47618cd19 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 9 Apr 2026 17:55:06 +0800 Subject: [PATCH 05/80] 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 933171c3a84f6a74d7f6f681da9734f986c4a39b Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 9 Apr 2026 22:32:20 +0800 Subject: [PATCH 06/80] 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 73605d5817b8c..717da78f1defa 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -190,7 +190,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 @@ -650,7 +650,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 5c51d0d98f05c6f578d1ad5048f6a3b69c36d28c Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 9 Apr 2026 23:51:19 +0800 Subject: [PATCH 07/80] 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 18a70d7e510d999ddcb3f9729233ea899695a0dd Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 10 Apr 2026 14:38:33 +0800 Subject: [PATCH 08/80] 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 78eb3ec638fc1ff417cb38f57f6b53498eaf4b43 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 10 Apr 2026 17:08:00 +0800 Subject: [PATCH 09/80] 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 289519523612f0c0e6e5a9656e3bf4dc3c17f66c Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 10 Apr 2026 17:09:39 +0800 Subject: [PATCH 10/80] 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 d4f5ce350a30d1ab78fdd04ed898a812ac30c315 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 13 Apr 2026 09:58:29 +0800 Subject: [PATCH 11/80] 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 | 52 +++++++++++++++++ 4 files changed, 155 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 56ba8343c648b..2f47bcf5741f5 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -1954,6 +1954,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") @@ -1980,6 +2025,13 @@ 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 locale coordinator should + # handle this task (e.g. Java, Go) instead of the standard + # Python execution path. + locale_entrypoint = _resolve_locale_entrypoint(startup_details, log) + if locale_entrypoint is not None: + locale_entrypoint() + return span = _make_task_span(msg=startup_details) stack.enter_context(span) ti, context, log = startup(msg=startup_details) From e31ec6038933e66f6abcbd2ff3e07f5f0cb9a5de Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 13 Apr 2026 11:30:24 +0800 Subject: [PATCH 12/80] 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 717da78f1defa..020171667763a 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -162,51 +162,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. @@ -599,12 +554,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) @@ -627,7 +582,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 @@ -636,44 +591,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 2f47bcf5741f5..87ca426f33d08 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -1956,7 +1956,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 @@ -1972,14 +1972,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( @@ -1988,7 +1988,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 7c8f3529fc803c21a331303c797cd500990c498d Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 14 Apr 2026 15:26:56 +0800 Subject: [PATCH 13/80] 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 87ca426f33d08..f8e9db3f8585a 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -47,6 +47,7 @@ from airflow.sdk.api.client import get_hostname, getuser from airflow.sdk.api.datamodels._generated import ( AssetProfile, + BundleInfo, DagRun, PreviousTIResponse, TaskInstance, @@ -778,12 +779,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)) @@ -909,6 +905,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. @@ -1990,8 +2002,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 5df4fe08224cb7fce3ff5d9d39f3aa2e7b0b3c02 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 15 Apr 2026 14:12:17 +0800 Subject: [PATCH 14/80] Make @task.stub(language=java) works --- .../execution_api/datamodels/taskinstance.py | 1 + .../execution_api/versions/__init__.py | 3 +- .../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 +++++++--- 12 files changed, 119 insertions(+), 9 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 dfa27f53ebd91..d39beaa79cdc8 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 @@ -42,10 +42,11 @@ ) from airflow.api_fastapi.execution_api.versions.v2026_04_17 import AddStateEndpoints, AddTeamNameField from airflow.api_fastapi.execution_api.versions.v2026_06_16 import AddRetryPolicyFields +from airflow.api_fastapi.execution_api.versions.v2026_07_01 import AddLanguageField bundle = VersionBundle( HeadVersion(), - Version("2026-06-16", AddRetryPolicyFields), + Version("2026-06-16", AddRetryPolicyFields, AddLanguageField), Version( "2026-04-17", AddTeamNameField, 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 d05affe433096..38842473c4d31 100644 --- a/airflow-core/src/airflow/executors/workloads/task.py +++ b/airflow-core/src/airflow/executors/workloads/task.py @@ -50,6 +50,8 @@ class TaskInstanceDTO(BaseModel): executor_config: dict | None = Field(default=None, exclude=True) external_executor_id: str | 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 e5b19f2768da9..191d7406f8f05 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -563,6 +563,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) @@ -750,6 +751,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, @@ -920,6 +922,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 b5b100154c389..5bf4fa95225e5 100644 --- a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py +++ b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py @@ -513,6 +513,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 f8e9db3f8585a..0b5bb22e35756 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -1970,20 +1970,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) @@ -1994,9 +1994,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( @@ -2007,6 +2011,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 From 340ef89f2603070a0ada11af6cd40589384acf38 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 16 Apr 2026 10:14:19 +0800 Subject: [PATCH 15/80] 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 0b5bb22e35756..a7f0aa5ee2362 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -1975,6 +1975,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 @@ -1986,7 +1989,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 e59df360c95885c58b4badd1429f542d124ea099 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 16 Apr 2026 11:21:41 +0800 Subject: [PATCH 16/80] Add java_sdk_setup script for Breeze --- scripts/in_container/java_sdk_setup.sh | 73 ++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 scripts/in_container/java_sdk_setup.sh diff --git a/scripts/in_container/java_sdk_setup.sh b/scripts/in_container/java_sdk_setup.sh new file mode 100644 index 0000000000000..b3437b7fc4200 --- /dev/null +++ b/scripts/in_container/java_sdk_setup.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# 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. + + + +# 1. Check Java +check_java() { + local java_bin="/files/openjdk/bin/java" + local version_output + + # First check if the locally installed OpenJDK exists and works. + if [ -x "$java_bin" ] && version_output=$("$java_bin" -version 2>&1); then + echo "Found existing OpenJDK at $java_bin. OK." + return + fi + + # On macOS, /usr/bin/java exists as a shim even without a JDK installed, + # so we must test with `java -version` directly. + if ! version_output=$(java -version 2>&1); then + echo "Java is not installed." + install_java + return + fi + + local java_version + java_version=$(echo "$version_output" | head -n1 | sed -E 's/.*"([0-9]+)(\.[0-9]+)*.*/\1/') + + if ! [[ "$java_version" =~ ^[0-9]+$ ]]; then + echo "Could not determine Java version." + install_java + return + fi + + if [ "$java_version" -ge 11 ]; then + echo "Java $java_version detected. OK." + else + echo "Java version $java_version found, but >= 11 is required." + install_java + fi +} + + +install_java() { + echo "Installing OpenJDK 11 in Breeze..." + + curl -L -o /files/openjdk-11-aarch64.tar.gz \ + https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.30+7/OpenJDK11U-jdk_aarch64_linux_hotspot_11.0.30_7.tar.gz + + rm -rf /files/openjdk && mkdir -p /files/openjdk && \ + tar -xzf /files/openjdk-11-aarch64.tar.gz --strip-components=1 -C /files/openjdk + + /files/openjdk/bin/java -version + echo "" +} + +check_java +# Install Java Provider +pip install -e /opt/airflow/providers/languages/java/ From 03631ed70340a092a209a931fee6d2218cb3370b Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 16 Apr 2026 14:57:37 +0800 Subject: [PATCH 17/80] 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 020171667763a..a1dc12705ad22 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -591,6 +591,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( @@ -598,7 +601,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 fc17fae3bf16f2cf6b24de397f6bbc7f0f1dbb1c Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 17 Apr 2026 13:10:53 +0800 Subject: [PATCH 18/80] 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 8d497ca7508e3..4e1ab653d1603 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -267,6 +267,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""" @@ -815,6 +818,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. @@ -822,7 +846,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.""" @@ -837,7 +865,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 1978b9279fb10df285b415845c0f7ce9fede1a81 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 17 Apr 2026 15:31:09 +0800 Subject: [PATCH 19/80] 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 | 36 +++--- .../airflow/sdk/providers_manager_runtime.py | 38 ++++--- .../test_providers_manager_runtime.py | 20 ++-- 17 files changed, 189 insertions(+), 218 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 4e1ab653d1603..38f0dde3ee32f 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -267,8 +267,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""" @@ -818,26 +818,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]: """ @@ -847,10 +839,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.""" @@ -865,7 +857,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 a1dc12705ad22..e7057af6a8b5f 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -554,12 +554,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) @@ -582,30 +582,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( @@ -615,15 +610,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 a7f0aa5ee2362..2a174ea21a300 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -1966,43 +1966,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, ) @@ -2015,7 +2005,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, ) @@ -2048,12 +2038,12 @@ 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 locale coordinator should + # Check if a provider-registered runtime coordinator should # handle this task (e.g. Java, Go) instead of the standard # Python execution path. - locale_entrypoint = _resolve_locale_entrypoint(startup_details, log) - if locale_entrypoint is not None: - locale_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) 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 ba4ba6c953a180da044c320a221b75f809316496 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 10:44:18 +0800 Subject: [PATCH 20/80] Rename stub operator language field as sdk --- .../execution_api/datamodels/taskinstance.py | 2 +- .../api_fastapi/execution_api/versions/__init__.py | 4 ++-- .../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, 36 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 d39beaa79cdc8..e2fb9d5252cc4 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 @@ -42,11 +42,11 @@ ) from airflow.api_fastapi.execution_api.versions.v2026_04_17 import AddStateEndpoints, AddTeamNameField from airflow.api_fastapi.execution_api.versions.v2026_06_16 import AddRetryPolicyFields -from airflow.api_fastapi.execution_api.versions.v2026_07_01 import AddLanguageField +from airflow.api_fastapi.execution_api.versions.v2026_07_01 import AddSdkField bundle = VersionBundle( HeadVersion(), - Version("2026-06-16", AddRetryPolicyFields, AddLanguageField), + Version("2026-06-16", AddRetryPolicyFields, AddSdkField), Version( "2026-04-17", AddTeamNameField, 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 38842473c4d31..419c884185c50 100644 --- a/airflow-core/src/airflow/executors/workloads/task.py +++ b/airflow-core/src/airflow/executors/workloads/task.py @@ -50,7 +50,7 @@ class TaskInstanceDTO(BaseModel): executor_config: dict | None = Field(default=None, exclude=True) external_executor_id: str | 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 191d7406f8f05..131ed0cf4bd73 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -563,7 +563,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) @@ -751,7 +751,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, @@ -922,7 +922,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 5bf4fa95225e5..ce3eb8a1c152b 100644 --- a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py +++ b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py @@ -513,7 +513,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 2a174ea21a300..17d960e4a4b00 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -1970,13 +1970,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 @@ -1987,13 +1987,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( @@ -2005,8 +2005,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 6eb47847a2710c1296e46377e4fa63be103f3845 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 11:34:35 +0800 Subject: [PATCH 21/80] 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 | 87 +++++++++++-------- 33 files changed, 148 insertions(+), 113 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 3f8df8a6efab7..0968ae9e0e91b 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 119a45515af20..9e064892c6831 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", @@ -1174,8 +1174,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", @@ -1224,6 +1222,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", @@ -1443,7 +1443,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 @@ -1468,6 +1467,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 @@ -1595,7 +1595,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 @@ -1620,6 +1619,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 @@ -1757,7 +1757,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 } @@ -1782,6 +1781,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 } @@ -1895,7 +1895,6 @@ members = [ "providers/jdbc", "providers/jenkins", "providers/keycloak", - "providers/languages/java", "providers/microsoft/azure", "providers/microsoft/mssql", "providers/microsoft/psrp", @@ -1920,6 +1919,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 5f2a6b2a6ce03..b760605ebed7e 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 @@ -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 @@ -153,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] @@ -226,7 +226,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", @@ -251,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", @@ -1003,7 +1003,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" }, @@ -1028,6 +1027,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" }, @@ -1254,9 +1254,6 @@ kerberos = [ keycloak = [ { name = "apache-airflow-providers-keycloak" }, ] -languages-java = [ - { name = "apache-airflow-providers-languages-java" }, -] ldap = [ { name = "python-ldap" }, ] @@ -1350,6 +1347,9 @@ salesforce = [ samba = [ { name = "apache-airflow-providers-samba" }, ] +sdk-java = [ + { name = "apache-airflow-providers-sdk-java" }, +] segment = [ { name = "apache-airflow-providers-segment" }, ] @@ -1593,8 +1593,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" }, @@ -1643,6 +1641,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" }, @@ -5821,35 +5821,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.1.2" @@ -7064,6 +7035,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 c898258aa17a8e36186525c0c36030eafddabab3 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 19:20:42 +0800 Subject: [PATCH 22/80] 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 59496a3d0ab16e4b1763811f6cc6f2c7d9ac5533 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 21 Apr 2026 18:37:27 +0800 Subject: [PATCH 23/80] 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 | 13 +- 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, 2309 insertions(+), 22 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 a875d9e9c77e7..0d4d45d7e40db 100644 --- a/airflow-core/pyproject.toml +++ b/airflow-core/pyproject.toml @@ -249,6 +249,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" @@ -337,6 +338,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 9c9487c5377bc..546aba77c00e0 100644 --- a/airflow-core/src/airflow/executors/base_executor.py +++ b/airflow-core/src/airflow/executors/base_executor.py @@ -69,6 +69,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 @@ -651,10 +652,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 419c884185c50..9ecde0c52c8a8 100644 --- a/airflow-core/src/airflow/executors/workloads/task.py +++ b/airflow-core/src/airflow/executors/workloads/task.py @@ -18,12 +18,12 @@ from __future__ import annotations -import uuid from pathlib import Path from typing import TYPE_CHECKING, Literal -from pydantic import BaseModel, Field +from pydantic import Field +from airflow._shared.workloads import TaskInstanceDTO as _BaseTaskInstanceDTO from airflow.executors.workloads.base import BaseDagBundleWorkload, BundleInfo from airflow.utils.state import TaskInstanceState @@ -33,8 +33,13 @@ from airflow.models.taskinstancekey import TaskInstanceKey -class TaskInstanceDTO(BaseModel): - """Schema for TaskInstance with minimal required fields needed for Executors and Task SDK.""" +class TaskInstanceDTO(_BaseTaskInstanceDTO): + """ + TaskInstanceDTO with executor-specific ``key`` property. + + Extends the shared :class:`~airflow._shared.workloads.TaskInstanceDTO` + to add the :attr:`key` property used by executors for workload tracking. + """ id: uuid.UUID dag_version_id: uuid.UUID diff --git a/pyproject.toml b/pyproject.toml index 9e064892c6831..56826f2104ede 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1335,6 +1335,7 @@ dev = [ "apache-airflow-shared-state", "apache-airflow-shared-template-rendering", "apache-airflow-shared-timezones", + "apache-airflow-shared-workloads", ] # To build docs: @@ -1503,6 +1504,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 @@ -1655,6 +1657,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 @@ -1700,6 +1703,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 } @@ -1838,6 +1842,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 6e4a0b1017d01..ced299cc41bd0 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" @@ -317,6 +318,7 @@ shared_distributions = [ "apache-airflow-shared-secrets-masker", "apache-airflow-shared-serialization", "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 1e11e9636e56f..93e7385b56348 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, @@ -79,7 +80,6 @@ PreviousTIResponse, PrevSuccessfulDagRunResponse, TaskBreadcrumbsResponse, - TaskInstance, TaskInstanceState, TaskStatesResponse, TIDeferredStatePayload, @@ -332,7 +332,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 6630979db4cdd..fabb2db246911 100644 --- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py +++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py @@ -142,6 +142,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 @@ -1108,7 +1109,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, @@ -1137,7 +1138,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, @@ -2082,7 +2083,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 17d960e4a4b00..94ae5127beb7c 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -29,7 +29,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 @@ -134,6 +134,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 @@ -1998,7 +1999,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 f0d8e1a0b65d6..1c0b9602fc715 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 BaseOperator, timezone from airflow.sdk.api import client as sdk_client @@ -622,8 +623,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, @@ -659,8 +668,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 b760605ebed7e..af1d2fbefb052 100644 --- a/uv.lock +++ b/uv.lock @@ -97,6 +97,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", @@ -1446,6 +1448,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" }, ] @@ -1721,6 +1724,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" }, ] @@ -8474,6 +8478,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 fc642fb31fa67f2af600f0b6d417b8e3185bdde4 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 22 Apr 2026 14:16:33 +0800 Subject: [PATCH 24/80] Add [workers/queue_to_runtime_mapping] --- .../src/airflow/config_templates/config.yml | 14 ++ .../tests/airflow_e2e_tests/conftest.py | 136 ++++++++++++++++++ .../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 | 37 +++-- .../execution_time/test_task_runner.py | 26 +++- 7 files changed, 244 insertions(+), 18 deletions(-) diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml index 8a060007978ee..8c82175fc18e6 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -1967,6 +1967,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 4443f7d8a2898..eb5253969de6f 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py @@ -142,8 +142,144 @@ 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("airflow-e2e-tests") +======= +def _download_jdk(dest_dir: Path) -> Path: + """Download a Linux JDK matching the host/container architecture into dest_dir/openjdk.""" + machine = platform.machine() + if machine in ("x86_64", "amd64"): + arch = "x64" + elif machine in ("aarch64", "arm64"): + arch = "aarch64" + else: + raise RuntimeError(f"Unsupported architecture: {machine}") + + jdk_url = ( + "https://github.com/adoptium/temurin11-binaries/releases/download/" + f"jdk-11.0.30%2B7/OpenJDK11U-jdk_{arch}_linux_hotspot_11.0.30_7.tar.gz" + ) + + tarball_path = dest_dir / "openjdk-11.tar.gz" + openjdk_dir = dest_dir / "openjdk" + openjdk_dir.mkdir(exist_ok=True) + + console.print(f"[yellow]Downloading OpenJDK 11 ({arch}) for containers...") + result = subprocess.run( + ["curl", "-fL", "-o", str(tarball_path), jdk_url], + capture_output=True, + text=True, + timeout=300, + check=False, + ) + if result.returncode != 0: + raise RuntimeError(f"Failed to download OpenJDK: {result.stderr}") + + console.print("[yellow]Extracting OpenJDK...") + result = subprocess.run( + ["tar", "-xzf", str(tarball_path), "--strip-components=1", "-C", str(openjdk_dir)], + capture_output=True, + text=True, + timeout=120, + check=False, + ) + if result.returncode != 0: + raise RuntimeError(f"Failed to extract OpenJDK: {result.stderr}") + + tarball_path.unlink() + console.print("[green]OpenJDK 11 downloaded and extracted successfully") + return openjdk_dir + + +def _build_java_sdk_bundles(tmp_dir: Path) -> Path: + """Build both Java SDK bundles (stub + pure Java) using the shared build script. + + Returns the bundles output directory containing both bundle subdirectories. + The caller's system Java is used for the Gradle build (not the downloaded Linux JDK). + """ + build_script = AIRFLOW_ROOT_PATH / "scripts" / "in_container" / "java_sdk_build.sh" + bundles_dir = tmp_dir / "java-sdk-bundles" + + build_env = { + **os.environ, + "JAVA_SDK_SRC_DIR": str(JAVA_SDK_PATH), + "BUNDLES_OUTPUT_DIR": str(bundles_dir), + "JAVA_STUB_BUNDLE_NAME": JAVA_STUB_BUNDLE_NAME, + "JAVA_PURE_BUNDLE_NAME": JAVA_PURE_BUNDLE_NAME, + "JAVA_STUB_DAG_ID": JAVA_STUB_DAG_ID, + "JAVA_PURE_DAG_ID": JAVA_PURE_DAG_ID, + "JAVA_SDK_DAGS_DIR": str(JAVA_SDK_DAGS_FOLDER), + } + + console.print("[yellow]Building Java SDK bundles via shared build script...") + result = subprocess.run( + ["bash", str(build_script)], + env=build_env, + capture_output=True, + text=True, + timeout=600, + check=False, + ) + if result.returncode != 0: + console.print(f"[red]Java SDK build failed:\n{result.stdout}\n{result.stderr}") + raise RuntimeError("Failed to build Java SDK bundles") + console.print(result.stdout) + + return bundles_dir + + +def _setup_java_sdk_integration(dot_env_file, tmp_dir): + """Set up Java SDK integration: download JDK, build bundles, write env and compose override.""" + # Download the Linux JDK on the host so containers get it via bind mount + openjdk_dir = _download_jdk(tmp_dir) + + # Build both bundles (stub + pure Java) using the shared script + bundles_dir = _build_java_sdk_bundles(tmp_dir) + stub_bundle_root = bundles_dir / JAVA_STUB_BUNDLE_NAME + pure_bundle_dir = bundles_dir / JAVA_PURE_BUNDLE_NAME + + # Copy the docker-compose override + copyfile(JAVA_SDK_COMPOSE_PATH, tmp_dir / "java-sdk.yml") + + # Set host environment variables consumed by docker-compose variable substitution + os.environ["JAVA_OPENJDK_PATH"] = str(openjdk_dir) + os.environ["JAVA_STUB_BUNDLE_PATH"] = str(stub_bundle_root) + os.environ["JAVA_BUNDLES_PATH"] = str(pure_bundle_dir) + + # Write .env file with bundle config for both DAG bundles + bundle_config = json.dumps( + [ + { + "name": JAVA_STUB_BUNDLE_NAME, + "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle", + "kwargs": {"path": JAVA_CONTAINER_STUB_DAG_BUNDLE_PATH, "refresh_interval": 20}, + }, + { + "name": JAVA_PURE_BUNDLE_NAME, + "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle", + "kwargs": {"path": JAVA_CONTAINER_PURE_BUNDLE_PATH, "refresh_interval": 20}, + }, + ] + ) + queue_to_runtime_mapping = json.dumps({"java-queue": "java"}) + + dot_env_file.write_text( + f"AIRFLOW_UID={os.getuid()}\n" + "AIRFLOW__CORE__LOAD_EXAMPLES=false\n" + "AIRFLOW__LOGGING__LOGGING_LEVEL=DEBUG\n" + f"AIRFLOW__WORKERS__QUEUE_TO_RUNTIME_MAPPING={queue_to_runtime_mapping}\n" + f"AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST={bundle_config}\n" + f"AIRFLOW__JAVA__BUNDLES_FOLDER={JAVA_CONTAINER_STUB_JAVA_BUNDLES_FOLDER_PATH}\n" + ) + os.environ["ENV_FILE_PATH"] = str(dot_env_file) + + +def spin_up_airflow_environment(): + # We indent to use explicitly created temp directory instead of pytest's tmp_path fixture because we want the directory to persist after the test run for debugging purposes and pytest's tmp_path is automatically deleted after the test run. + tmp_dir = Path(tempfile.mkdtemp(prefix="airflow-e2e-tests-")) + console.print(f"[yellow]Temp directory (persists after test run): {tmp_dir}") +>>>>>>> 1978a719f72 (Add [workers/queue_to_runtime_mapping]) console.print(f"[yellow]Using docker compose file: {DOCKER_COMPOSE_PATH}") copyfile(DOCKER_COMPOSE_PATH, tmp_dir / "docker-compose.yaml") diff --git a/devel-common/src/tests_common/pytest_plugin.py b/devel-common/src/tests_common/pytest_plugin.py index 98a871ebb12d2..60b27c7cf6628 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -2510,7 +2510,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 @@ -2538,6 +2537,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 @@ -2615,14 +2615,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 94ae5127beb7c..49fa6711d131d 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -29,7 +29,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 @@ -134,7 +134,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 @@ -1971,14 +1970,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 @@ -1999,7 +2019,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, @@ -2042,6 +2062,7 @@ def main(): # Check if a provider-registered 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/tests/task_sdk/execution_time/test_task_runner.py b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py index 630aff9094ed1..3331a868d2d76 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 @@ -55,6 +55,7 @@ timezone, ) from airflow.sdk._shared.observability.metrics.base_stats_logger import StatsLogger +from airflow.sdk._shared.workloads import TaskInstanceDTO from airflow.sdk.api.datamodels._generated import ( AssetProfile, AssetResponse, @@ -177,13 +178,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), @@ -224,13 +228,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), @@ -284,13 +291,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), @@ -330,13 +340,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), @@ -593,13 +606,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 26764dde56b041c587d01b9e54315ca4ed75957f Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 20:29:29 +0800 Subject: [PATCH 25/80] Remove the sdk field from stub operator and respect [workers/queue_to_runtime_mapping] config --- .../execution_api/datamodels/taskinstance.py | 1 - .../execution_api/versions/__init__.py | 3 +- .../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 | 136 ------------------ .../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 +++++++---- 13 files changed, 57 insertions(+), 268 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/__init__.py b/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py index e2fb9d5252cc4..dfa27f53ebd91 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 @@ -42,11 +42,10 @@ ) from airflow.api_fastapi.execution_api.versions.v2026_04_17 import AddStateEndpoints, AddTeamNameField from airflow.api_fastapi.execution_api.versions.v2026_06_16 import AddRetryPolicyFields -from airflow.api_fastapi.execution_api.versions.v2026_07_01 import AddSdkField bundle = VersionBundle( HeadVersion(), - Version("2026-06-16", AddRetryPolicyFields, AddSdkField), + Version("2026-06-16", AddRetryPolicyFields), Version( "2026-04-17", AddTeamNameField, 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 9ecde0c52c8a8..89a171d9244df 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 @@ -55,8 +56,6 @@ class TaskInstanceDTO(_BaseTaskInstanceDTO): executor_config: dict | None = Field(default=None, exclude=True) external_executor_id: str | 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 131ed0cf4bd73..e5b19f2768da9 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -563,7 +563,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) @@ -751,7 +750,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, @@ -922,7 +920,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 eb5253969de6f..4443f7d8a2898 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py @@ -142,144 +142,8 @@ 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("airflow-e2e-tests") -======= -def _download_jdk(dest_dir: Path) -> Path: - """Download a Linux JDK matching the host/container architecture into dest_dir/openjdk.""" - machine = platform.machine() - if machine in ("x86_64", "amd64"): - arch = "x64" - elif machine in ("aarch64", "arm64"): - arch = "aarch64" - else: - raise RuntimeError(f"Unsupported architecture: {machine}") - - jdk_url = ( - "https://github.com/adoptium/temurin11-binaries/releases/download/" - f"jdk-11.0.30%2B7/OpenJDK11U-jdk_{arch}_linux_hotspot_11.0.30_7.tar.gz" - ) - - tarball_path = dest_dir / "openjdk-11.tar.gz" - openjdk_dir = dest_dir / "openjdk" - openjdk_dir.mkdir(exist_ok=True) - - console.print(f"[yellow]Downloading OpenJDK 11 ({arch}) for containers...") - result = subprocess.run( - ["curl", "-fL", "-o", str(tarball_path), jdk_url], - capture_output=True, - text=True, - timeout=300, - check=False, - ) - if result.returncode != 0: - raise RuntimeError(f"Failed to download OpenJDK: {result.stderr}") - - console.print("[yellow]Extracting OpenJDK...") - result = subprocess.run( - ["tar", "-xzf", str(tarball_path), "--strip-components=1", "-C", str(openjdk_dir)], - capture_output=True, - text=True, - timeout=120, - check=False, - ) - if result.returncode != 0: - raise RuntimeError(f"Failed to extract OpenJDK: {result.stderr}") - - tarball_path.unlink() - console.print("[green]OpenJDK 11 downloaded and extracted successfully") - return openjdk_dir - - -def _build_java_sdk_bundles(tmp_dir: Path) -> Path: - """Build both Java SDK bundles (stub + pure Java) using the shared build script. - - Returns the bundles output directory containing both bundle subdirectories. - The caller's system Java is used for the Gradle build (not the downloaded Linux JDK). - """ - build_script = AIRFLOW_ROOT_PATH / "scripts" / "in_container" / "java_sdk_build.sh" - bundles_dir = tmp_dir / "java-sdk-bundles" - - build_env = { - **os.environ, - "JAVA_SDK_SRC_DIR": str(JAVA_SDK_PATH), - "BUNDLES_OUTPUT_DIR": str(bundles_dir), - "JAVA_STUB_BUNDLE_NAME": JAVA_STUB_BUNDLE_NAME, - "JAVA_PURE_BUNDLE_NAME": JAVA_PURE_BUNDLE_NAME, - "JAVA_STUB_DAG_ID": JAVA_STUB_DAG_ID, - "JAVA_PURE_DAG_ID": JAVA_PURE_DAG_ID, - "JAVA_SDK_DAGS_DIR": str(JAVA_SDK_DAGS_FOLDER), - } - - console.print("[yellow]Building Java SDK bundles via shared build script...") - result = subprocess.run( - ["bash", str(build_script)], - env=build_env, - capture_output=True, - text=True, - timeout=600, - check=False, - ) - if result.returncode != 0: - console.print(f"[red]Java SDK build failed:\n{result.stdout}\n{result.stderr}") - raise RuntimeError("Failed to build Java SDK bundles") - console.print(result.stdout) - - return bundles_dir - - -def _setup_java_sdk_integration(dot_env_file, tmp_dir): - """Set up Java SDK integration: download JDK, build bundles, write env and compose override.""" - # Download the Linux JDK on the host so containers get it via bind mount - openjdk_dir = _download_jdk(tmp_dir) - - # Build both bundles (stub + pure Java) using the shared script - bundles_dir = _build_java_sdk_bundles(tmp_dir) - stub_bundle_root = bundles_dir / JAVA_STUB_BUNDLE_NAME - pure_bundle_dir = bundles_dir / JAVA_PURE_BUNDLE_NAME - - # Copy the docker-compose override - copyfile(JAVA_SDK_COMPOSE_PATH, tmp_dir / "java-sdk.yml") - - # Set host environment variables consumed by docker-compose variable substitution - os.environ["JAVA_OPENJDK_PATH"] = str(openjdk_dir) - os.environ["JAVA_STUB_BUNDLE_PATH"] = str(stub_bundle_root) - os.environ["JAVA_BUNDLES_PATH"] = str(pure_bundle_dir) - - # Write .env file with bundle config for both DAG bundles - bundle_config = json.dumps( - [ - { - "name": JAVA_STUB_BUNDLE_NAME, - "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle", - "kwargs": {"path": JAVA_CONTAINER_STUB_DAG_BUNDLE_PATH, "refresh_interval": 20}, - }, - { - "name": JAVA_PURE_BUNDLE_NAME, - "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle", - "kwargs": {"path": JAVA_CONTAINER_PURE_BUNDLE_PATH, "refresh_interval": 20}, - }, - ] - ) - queue_to_runtime_mapping = json.dumps({"java-queue": "java"}) - - dot_env_file.write_text( - f"AIRFLOW_UID={os.getuid()}\n" - "AIRFLOW__CORE__LOAD_EXAMPLES=false\n" - "AIRFLOW__LOGGING__LOGGING_LEVEL=DEBUG\n" - f"AIRFLOW__WORKERS__QUEUE_TO_RUNTIME_MAPPING={queue_to_runtime_mapping}\n" - f"AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST={bundle_config}\n" - f"AIRFLOW__JAVA__BUNDLES_FOLDER={JAVA_CONTAINER_STUB_JAVA_BUNDLES_FOLDER_PATH}\n" - ) - os.environ["ENV_FILE_PATH"] = str(dot_env_file) - - -def spin_up_airflow_environment(): - # We indent to use explicitly created temp directory instead of pytest's tmp_path fixture because we want the directory to persist after the test run for debugging purposes and pytest's tmp_path is automatically deleted after the test run. - tmp_dir = Path(tempfile.mkdtemp(prefix="airflow-e2e-tests-")) - console.print(f"[yellow]Temp directory (persists after test run): {tmp_dir}") ->>>>>>> 1978a719f72 (Add [workers/queue_to_runtime_mapping]) console.print(f"[yellow]Using docker compose file: {DOCKER_COMPOSE_PATH}") copyfile(DOCKER_COMPOSE_PATH, tmp_dir / "docker-compose.yaml") 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 ce3eb8a1c152b..b5b100154c389 100644 --- a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py +++ b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py @@ -513,7 +513,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 49fa6711d131d..9212c9ee63b99 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -1972,49 +1972,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( @@ -2025,9 +2048,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 76253455e4a2b74747fb67d63513d4a927e6d356 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 20:52:27 +0800 Subject: [PATCH 26/80] 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 8c82175fc18e6..afaca7cc604bd 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -1967,9 +1967,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 9212c9ee63b99..b93b5c8568b69 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -1972,7 +1972,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 5ba39670fb25428cff64b5d105e1f9603cbfef81 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Fri, 24 Apr 2026 19:32:49 +0800 Subject: [PATCH 27/80] 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 38f0dde3ee32f..67837dca498b5 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -826,7 +826,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 e7057af6a8b5f..4733bf26db21e 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -77,8 +77,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 @@ -87,6 +85,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 @@ -590,7 +589,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", @@ -646,7 +645,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 100a6e6490849..c1d4498a623fc 100644 --- a/task-sdk/.pre-commit-config.yaml +++ b/task-sdk/.pre-commit-config.yaml @@ -43,6 +43,7 @@ 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/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 b93b5c8568b69..68a5240386af0 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -1985,25 +1985,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, ) @@ -2016,8 +2015,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, ) @@ -2036,7 +2035,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 ea4eef9d0317f963ab86646ebebb1de6350ee6e8 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 15:09:55 +0800 Subject: [PATCH 28/80] 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 | 2 +- 5 files changed, 127 insertions(+), 1 deletion(-) 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 af1d2fbefb052..35dfc5acb7f4c 100644 --- a/uv.lock +++ b/uv.lock @@ -1696,7 +1696,7 @@ requires-dist = [ { name = "sentry-sdk", marker = "extra == 'sentry'", specifier = ">=2.30.0" }, { name = "uv", marker = "extra == 'uv'", specifier = ">=0.11.8" }, ] -provides-extras = ["all-core", "async", "graphviz", "gunicorn", "kerberos", "memray", "otel", "statsd", "all-task-sdk", "airbyte", "akeyless", "alibaba", "amazon", "apache-cassandra", "apache-drill", "apache-druid", "apache-flink", "apache-hdfs", "apache-hive", "apache-iceberg", "apache-impala", "apache-kafka", "apache-kylin", "apache-livy", "apache-pig", "apache-pinot", "apache-spark", "apache-tinkerpop", "apprise", "arangodb", "asana", "atlassian-jira", "celery", "cloudant", "cncf-kubernetes", "cohere", "common-ai", "common-compat", "common-io", "common-messaging", "common-sql", "databricks", "datadog", "dbt-cloud", "dingding", "discord", "docker", "edge3", "elasticsearch", "exasol", "fab", "facebook", "ftp", "git", "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", "all", "aiobotocore", "apache-atlas", "apache-webhdfs", "amazon-aws-auth", "cloudpickle", "github-enterprise", "google-auth", "ldap", "pandas", "polars", "rabbitmq", "sentry", "s3fs", "uv"] +provides-extras = ["all-core", "async", "graphviz", "gunicorn", "kerberos", "memray", "otel", "statsd", "all-task-sdk", "airbyte", "akeyless", "alibaba", "amazon", "apache-cassandra", "apache-drill", "apache-druid", "apache-flink", "apache-hdfs", "apache-hive", "apache-iceberg", "apache-impala", "apache-kafka", "apache-kylin", "apache-livy", "apache-pig", "apache-pinot", "apache-spark", "apache-tinkerpop", "apprise", "arangodb", "asana", "atlassian-jira", "celery", "cloudant", "cncf-kubernetes", "cohere", "common-ai", "common-compat", "common-io", "common-messaging", "common-sql", "databricks", "datadog", "dbt-cloud", "dingding", "discord", "docker", "edge3", "elasticsearch", "exasol", "fab", "facebook", "ftp", "git", "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", "all", "aiobotocore", "apache-atlas", "apache-webhdfs", "amazon-aws-auth", "cloudpickle", "github-enterprise", "google-auth", "ldap", "pandas", "polars", "rabbitmq", "sentry", "s3fs", "uv"] [package.metadata.requires-dev] dev = [ From 5c803ddf4292d5c6a531926b720a1fa5ea165b2e Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 15:15:28 +0800 Subject: [PATCH 29/80] 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 1c97cf397428ff3c62efa5e565fe151dc83f4440 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 15:35:59 +0800 Subject: [PATCH 30/80] 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 +++++++++ .../edge3/worker_api/v2-edge-generated.yaml | 8 +++++-- providers/sdk/java/docs/index.rst | 2 +- pyproject.toml | 4 ++-- .../execution_time/test_coordinator.py | 9 +++---- 6 files changed, 23 insertions(+), 35 deletions(-) diff --git a/Dockerfile.ci b/Dockerfile.ci index 0968ae9e0e91b..3f8df8a6efab7 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/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml b/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml index 01c8149d1dad8..b041c1daa44e9 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml @@ -1257,8 +1257,12 @@ components: - queue - priority_weight title: TaskInstanceDTO - description: Schema for TaskInstance with minimal required fields needed for - Executors and Task SDK. + description: 'TaskInstanceDTO with executor-specific ``key`` property. + + + Extends the shared :class:`~airflow._shared.workloads.TaskInstanceDTO` + + to add the :attr:`key` property used by executors for workload tracking.' TaskInstanceState: type: string enum: 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 56826f2104ede..888f46a4df590 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 9c7b41ccf1356526ccfafaf063d13edd04f94ec5 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 16:27:30 +0800 Subject: [PATCH 31/80] 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 c8223f3eeff10..84847ea3e3041 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 81d720b7b402d6e1761029ca9e6bd66824d6b9ac Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 16:31:02 +0800 Subject: [PATCH 32/80] 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 60b27c7cf6628..46bede9633951 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 @@ -2537,7 +2539,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 f8388c97c442da92ac5bade4472f2805f74d43f5 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 16:59:04 +0800 Subject: [PATCH 33/80] 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 46bede9633951..2ea9347188267 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 @@ -2539,6 +2537,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 6467fb104d66b8805be606d30616b2f8e35c8551 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 18:58:42 +0800 Subject: [PATCH 34/80] 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 546aba77c00e0..b2a284ad1cc36 100644 --- a/airflow-core/src/airflow/executors/base_executor.py +++ b/airflow-core/src/airflow/executors/base_executor.py @@ -69,7 +69,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 @@ -653,7 +652,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 2ea9347188267..d8891a09d7cad 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -2542,7 +2542,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 @@ -2627,8 +2627,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 375eaa96909ecff0ce6e94bb42703c9867728d6a Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 18:59:55 +0800 Subject: [PATCH 35/80] 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 d8891a09d7cad..35bf28195bad1 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -2627,7 +2627,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 e615c97e58b9a2006fe313cb290494c9d1902d3b Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:14:44 +0800 Subject: [PATCH 36/80] 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 36d0bdffe7986ceacccd41cbe04d950b5b4691c1 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:24:57 +0800 Subject: [PATCH 37/80] 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 3331a868d2d76..7a9fd76811eec 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 @@ -404,13 +404,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="", @@ -497,13 +500,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="", @@ -565,13 +571,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="", @@ -1056,13 +1065,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="", @@ -1172,13 +1184,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, @@ -1220,13 +1235,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, @@ -1268,13 +1286,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, @@ -1308,13 +1329,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, @@ -1481,8 +1505,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), @@ -3899,13 +3931,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 4c58701dc19e298150128e80cc72ed575f17ecf6 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:28:27 +0800 Subject: [PATCH 38/80] 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 c938a381d5020c4fd8e8fa7aa3657a81898c9ec1 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:31:07 +0800 Subject: [PATCH 39/80] 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 afaca7cc604bd..e7812fcfa3b00 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -1978,8 +1978,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 abfade1faa6478f12e3c02c1bd13c054ef244f18 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:38:56 +0800 Subject: [PATCH 40/80] 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 ++++---- dev/registry/extract_metadata.py | 2 +- dev/registry/extract_versions.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev/breeze/src/airflow_breeze/global_constants.py b/dev/breeze/src/airflow_breeze/global_constants.py index 4deaa3bf598b9..ce35be9d26a3a 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", }, ] diff --git a/dev/registry/extract_metadata.py b/dev/registry/extract_metadata.py index 463a9c408082b..5d9f635f74ea3 100644 --- a/dev/registry/extract_metadata.py +++ b/dev/registry/extract_metadata.py @@ -46,7 +46,7 @@ try: import tomllib # Python 3.11+ stdlib except ModuleNotFoundError: # pragma: no cover -- Python 3.10 fallback - import tomli as tomllib + import tomli as tomllib # type: ignore[no-redef] import yaml from registry_contract_models import validate_providers_catalog diff --git a/dev/registry/extract_versions.py b/dev/registry/extract_versions.py index d9dc4e166dcf1..2908b22b32e6a 100644 --- a/dev/registry/extract_versions.py +++ b/dev/registry/extract_versions.py @@ -49,7 +49,7 @@ try: import tomllib # Python 3.11+ stdlib except ModuleNotFoundError: # pragma: no cover -- Python 3.10 fallback - import tomli as tomllib + import tomli as tomllib # type: ignore[no-redef] from registry_contract_models import validate_provider_version_metadata try: From 52ce0ff989e8ab32e7af623121eef90e553bd712 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:47:00 +0800 Subject: [PATCH 41/80] 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 e91547363655f403316f8424cff52be13fbce768 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 21:55:36 +0800 Subject: [PATCH 42/80] 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 316c29a16d48fd41be24cde6528287d867a3e921 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 29 Apr 2026 16:44:31 +0800 Subject: [PATCH 43/80] 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 e7812fcfa3b00..9b98798f2bedd 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -1981,7 +1981,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 35bf28195bad1..0e9dced826894 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -2542,7 +2542,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 82817393d597b04c5b9cf8d2e6ab0d39ffbb41e5 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 29 Apr 2026 21:13:09 +0800 Subject: [PATCH 44/80] 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 0e9dced826894..0008d49c9a636 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -2629,7 +2629,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 58a6001d4f51fafd0fff8947773bf2830f4fb5fc Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 30 Apr 2026 10:29:21 +0800 Subject: [PATCH 45/80] CI: Refactor map_index handling and update time travel decorators for timezone awareness in tests --- .../java/tests/unit/sdk/java/test_coordinator.py | 6 +++--- .../task_sdk/execution_time/test_task_runner.py | 15 +++++++++------ 2 files changed, 12 insertions(+), 9 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 7a9fd76811eec..b9a05291aac3a 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 @@ -2109,8 +2109,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) @@ -4542,7 +4544,8 @@ class CustomOperator(BaseOperator): class TestTriggerDagRunOperator: """Tests to verify various aspects of TriggerDagRunOperator""" - @time_machine.travel("2025-01-01 00:00:00", tick=False) + # make timetravel timezone-aware + @time_machine.travel(datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc), tick=False) def test_handle_trigger_dag_run(self, create_runtime_ti, mock_supervisor_comms): """Test that TriggerDagRunOperator (with default args) sends the correct message to the Supervisor""" from airflow.providers.standard.operators.trigger_dagrun import TriggerDagRunOperator @@ -4590,7 +4593,7 @@ def test_handle_trigger_dag_run(self, create_runtime_ti, mock_supervisor_comms): (False, TaskInstanceState.FAILED), ], ) - @time_machine.travel("2025-01-01 00:00:00", tick=False) + @time_machine.travel(datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc), tick=False) def test_handle_trigger_dag_run_conflict( self, skip_when_already_exists, expected_state, create_runtime_ti, mock_supervisor_comms ): @@ -4634,7 +4637,7 @@ def test_handle_trigger_dag_run_conflict( ([DagRunState.SUCCESS], None, DagRunState.FAILED, DagRunState.FAILED), ], ) - @time_machine.travel("2025-01-01 00:00:00", tick=False) + @time_machine.travel(datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc), tick=False) def test_handle_trigger_dag_run_wait_for_completion( self, allowed_states, @@ -4755,7 +4758,7 @@ def test_handle_trigger_dag_run_deferred( assert state == intermediate_state - @time_machine.travel("2025-01-01 00:00:00", tick=False) + @time_machine.travel(datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc), tick=False) def test_handle_trigger_dag_run_deferred_with_reset_uses_run_id_only( self, create_runtime_ti, mock_supervisor_comms ): From d9c2b4adc0930027ce692c98cc17505fd699bc1b Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 30 Apr 2026 13:25:28 +0800 Subject: [PATCH 46/80] CI: Replace TaskInstance with TaskInstanceDTO and add additional parameters in test_supervisor --- .../execution_time/test_supervisor.py | 99 +++++++++++++++---- 1 file changed, 80 insertions(+), 19 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 1c0b9602fc715..61313cd83cd74 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 @@ -212,13 +211,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) @@ -305,13 +307,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, @@ -380,13 +385,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, @@ -477,13 +485,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, @@ -506,13 +517,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, @@ -541,8 +555,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, @@ -580,13 +602,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, @@ -692,13 +717,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) @@ -733,13 +761,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 @@ -860,8 +891,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, @@ -938,8 +977,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, @@ -1143,13 +1190,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, @@ -1306,8 +1356,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, @@ -3321,13 +3379,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 66eece79cf399009dc4f04c633179a0b921e1178 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 30 Apr 2026 14:37:46 +0800 Subject: [PATCH 47/80] 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 837d96487a27f590dc1b5afe8a610ce534284135 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 30 Apr 2026 18:58:24 +0800 Subject: [PATCH 48/80] 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. --- .pre-commit-config.yaml | 6 + 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 | 19 +- .../src/tests_common/pytest_plugin.py | 2 +- .../edge3/worker_api/v2-edge-generated.yaml | 8 +- .../airflow/providers/sdk/java/coordinator.py | 2 +- .../tests/unit/sdk/java/test_coordinator.py | 2 +- pyproject.toml | 5 - .../ci/prek/check_task_instance_dto_sync.py | 125 + shared/workloads/.gitignore | 1 - shared/workloads/.pre-commit-config.yaml | 31 - shared/workloads/pyproject.toml | 55 - 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 | 4 +- .../airflow/sdk/execution_time/coordinator.py | 2 +- .../airflow/sdk/execution_time/supervisor.py | 2 +- .../sdk/execution_time/workloads/__init__.py | 7 +- .../sdk/execution_time/workloads/task.py | 15 +- .../execution_time/test_supervisor.py | 2 +- .../execution_time/test_task_runner.py | 2 +- uv.lock | 23 - 27 files changed, 172 insertions(+), 2348 deletions(-) delete mode 120000 airflow-core/src/airflow/_shared/workloads create mode 100755 scripts/ci/prek/check_task_instance_dto_sync.py 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/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 rename shared/workloads/tests/conftest.py => task-sdk/src/airflow/sdk/execution_time/workloads/__init__.py (83%) rename shared/workloads/src/airflow_shared/workloads/__init__.py => task-sdk/src/airflow/sdk/execution_time/workloads/task.py (71%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6c0a97b801c9a..0a594e3e848a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -492,6 +492,12 @@ repos: language: python pass_filenames: false files: ^dev/registry/registry_tools/types\.py$|^registry/src/_data/types\.json$ + - id: check-task-instance-dto-sync + name: Check BaseTaskInstanceDTO duplicate is in sync between core and task-sdk + entry: ./scripts/ci/prek/check_task_instance_dto_sync.py + language: python + pass_filenames: false + files: ^airflow-core/src/airflow/executors/workloads/task\.py$|^task-sdk/src/airflow/sdk/execution_time/workloads/task\.py$ - id: ruff name: Run 'ruff' for extremely fast Python linting description: "Run 'ruff' for extremely fast Python linting" diff --git a/airflow-core/pyproject.toml b/airflow-core/pyproject.toml index 0d4d45d7e40db..a875d9e9c77e7 100644 --- a/airflow-core/pyproject.toml +++ b/airflow-core/pyproject.toml @@ -249,7 +249,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" @@ -338,7 +337,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 b2a284ad1cc36..bced160a5f4c7 100644 --- a/airflow-core/src/airflow/executors/base_executor.py +++ b/airflow-core/src/airflow/executors/base_executor.py @@ -650,9 +650,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 89a171d9244df..9af3f33c10efd 100644 --- a/airflow-core/src/airflow/executors/workloads/task.py +++ b/airflow-core/src/airflow/executors/workloads/task.py @@ -22,9 +22,8 @@ from pathlib import Path from typing import TYPE_CHECKING, Literal -from pydantic import Field +from pydantic import BaseModel, Field -from airflow._shared.workloads import TaskInstanceDTO as _BaseTaskInstanceDTO from airflow.executors.workloads.base import BaseDagBundleWorkload, BundleInfo from airflow.utils.state import TaskInstanceState @@ -34,12 +33,13 @@ from airflow.models.taskinstancekey import TaskInstanceKey -class TaskInstanceDTO(_BaseTaskInstanceDTO): +class BaseTaskInstanceDTO(BaseModel): """ - TaskInstanceDTO with executor-specific ``key`` property. + Base schema for TaskInstance with the minimal fields shared by Executors and the Task SDK. - Extends the shared :class:`~airflow._shared.workloads.TaskInstanceDTO` - to add the :attr:`key` property used by executors for workload tracking. + This definition is duplicated in :mod:`airflow.sdk.execution_time.workloads.task` + and the two are kept in sync by the ``check-task-instance-dto-sync`` prek + hook. Update both files together. """ id: uuid.UUID @@ -54,11 +54,16 @@ class TaskInstanceDTO(_BaseTaskInstanceDTO): queue: str priority_weight: int executor_config: dict | None = Field(default=None, exclude=True) - external_executor_id: str | None = Field(default=None, exclude=True) parent_context_carrier: dict | None = None context_carrier: dict | None = None + +class TaskInstanceDTO(BaseTaskInstanceDTO): + """TaskInstanceDTO with executor-specific ``external_executor_id`` field and ``key`` property.""" + + external_executor_id: str | None = Field(default=None, exclude=True) + # TODO: Task-SDK: Can we replace TaskInstanceKey with just the uuid across the codebase? @property def key(self) -> TaskInstanceKey: diff --git a/devel-common/src/tests_common/pytest_plugin.py b/devel-common/src/tests_common/pytest_plugin.py index 0008d49c9a636..ed3547ccbfa84 100644 --- a/devel-common/src/tests_common/pytest_plugin.py +++ b/devel-common/src/tests_common/pytest_plugin.py @@ -2540,7 +2540,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/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml b/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml index b041c1daa44e9..c777d1f1bec97 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml @@ -1257,12 +1257,8 @@ components: - queue - priority_weight title: TaskInstanceDTO - description: 'TaskInstanceDTO with executor-specific ``key`` property. - - - Extends the shared :class:`~airflow._shared.workloads.TaskInstanceDTO` - - to add the :attr:`key` property used by executors for workload tracking.' + description: TaskInstanceDTO with executor-specific ``external_executor_id`` + field and ``key`` property. TaskInstanceState: type: string enum: 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 888f46a4df590..2c733873cd796 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1335,7 +1335,6 @@ dev = [ "apache-airflow-shared-state", "apache-airflow-shared-template-rendering", "apache-airflow-shared-timezones", - "apache-airflow-shared-workloads", ] # To build docs: @@ -1504,7 +1503,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 @@ -1657,7 +1655,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 @@ -1703,7 +1700,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 } @@ -1842,7 +1838,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/scripts/ci/prek/check_task_instance_dto_sync.py b/scripts/ci/prek/check_task_instance_dto_sync.py new file mode 100755 index 0000000000000..689d35a4d15e3 --- /dev/null +++ b/scripts/ci/prek/check_task_instance_dto_sync.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# +# 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. +""" +Verify that the duplicate ``BaseTaskInstanceDTO`` definitions in airflow-core +and task-sdk stay structurally identical. + +``BaseTaskInstanceDTO`` is duplicated (not shared) in: + +- ``airflow-core/src/airflow/executors/workloads/task.py`` +- ``task-sdk/src/airflow/sdk/execution_time/workloads/task.py`` + +This hook compares the *fields* (annotated assignments) and bases of both +``BaseTaskInstanceDTO`` classes. The concrete ``TaskInstanceDTO`` subclasses +in each file are allowed to differ (airflow-core adds an executor-specific +``key`` property that depends on ``airflow.models``, which the Task SDK +does not have access to). +""" + +from __future__ import annotations + +import ast +import sys +from pathlib import Path + +AIRFLOW_ROOT = Path(__file__).parents[3].resolve() +CORE_FILE = AIRFLOW_ROOT / "airflow-core" / "src" / "airflow" / "executors" / "workloads" / "task.py" +SDK_FILE = AIRFLOW_ROOT / "task-sdk" / "src" / "airflow" / "sdk" / "execution_time" / "workloads" / "task.py" +CLASS_NAME = "BaseTaskInstanceDTO" + + +def _find_class(tree: ast.AST, class_name: str) -> ast.ClassDef | None: + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and node.name == class_name: + return node + return None + + +def _field_signature(class_node: ast.ClassDef) -> list[tuple[str, str, str | None]]: + """Return a normalized list of ``(name, annotation, default)`` for each field.""" + fields: list[tuple[str, str, str | None]] = [] + for stmt in class_node.body: + if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name): + name = stmt.target.id + annotation = ast.unparse(stmt.annotation) + default = ast.unparse(stmt.value) if stmt.value is not None else None + fields.append((name, annotation, default)) + return fields + + +def _bases(class_node: ast.ClassDef) -> list[str]: + return [ast.unparse(base) for base in class_node.bases] + + +def _extract(file_path: Path) -> tuple[list[str], list[tuple[str, str, str | None]]]: + source = file_path.read_text() + tree = ast.parse(source, filename=str(file_path)) + class_node = _find_class(tree, CLASS_NAME) + if class_node is None: + print(f"ERROR: Could not find class {CLASS_NAME} in {file_path}", file=sys.stderr) + sys.exit(1) + return _bases(class_node), _field_signature(class_node) + + +def main() -> None: + core_bases, core_fields = _extract(CORE_FILE) + sdk_bases, sdk_fields = _extract(SDK_FILE) + + if core_bases == sdk_bases and core_fields == sdk_fields: + sys.exit(0) + + print( + f"\nERROR: {CLASS_NAME} definitions in airflow-core and task-sdk are out of sync!", + file=sys.stderr, + ) + print(f"\n airflow-core: {CORE_FILE.relative_to(AIRFLOW_ROOT)}", file=sys.stderr) + print(f" task-sdk: {SDK_FILE.relative_to(AIRFLOW_ROOT)}", file=sys.stderr) + + if core_bases != sdk_bases: + print("\nClass bases differ:", file=sys.stderr) + print(f" airflow-core: {core_bases}", file=sys.stderr) + print(f" task-sdk: {sdk_bases}", file=sys.stderr) + + if core_fields != sdk_fields: + core_set = {f[0]: f for f in core_fields} + sdk_set = {f[0]: f for f in sdk_fields} + only_in_core = sorted(set(core_set) - set(sdk_set)) + only_in_sdk = sorted(set(sdk_set) - set(core_set)) + differing = sorted(name for name in set(core_set) & set(sdk_set) if core_set[name] != sdk_set[name]) + if only_in_core: + print(f"\n Fields only in airflow-core: {only_in_core}", file=sys.stderr) + if only_in_sdk: + print(f"\n Fields only in task-sdk: {only_in_sdk}", file=sys.stderr) + for name in differing: + print( + f"\n Field {name!r} differs:" + f"\n airflow-core: {core_set[name]}" + f"\n task-sdk: {sdk_set[name]}", + file=sys.stderr, + ) + + print( + f"\nUpdate both files together so the two {CLASS_NAME} definitions stay in sync.", + file=sys.stderr, + ) + sys.exit(1) + + +if __name__ == "__main__": + main() 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/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 ced299cc41bd0..6e4a0b1017d01 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" @@ -318,7 +317,6 @@ shared_distributions = [ "apache-airflow-shared-secrets-masker", "apache-airflow-shared-serialization", "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 93e7385b56348..b407e71a9891f 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, @@ -96,6 +95,9 @@ XComSequenceSliceResponse, ) from airflow.sdk.exceptions import ErrorType +from airflow.sdk.execution_time.workloads.task import ( + TaskInstanceDTO, # noqa: TC001 -- Pydantic needs this at runtime +) 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 fabb2db246911..75ac2ce784673 100644 --- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py +++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py @@ -142,11 +142,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/shared/workloads/tests/conftest.py b/task-sdk/src/airflow/sdk/execution_time/workloads/__init__.py similarity index 83% rename from shared/workloads/tests/conftest.py rename to task-sdk/src/airflow/sdk/execution_time/workloads/__init__.py index 8b61b1b99b90d..cdf955e742dc0 100644 --- a/shared/workloads/tests/conftest.py +++ b/task-sdk/src/airflow/sdk/execution_time/workloads/__init__.py @@ -1,4 +1,3 @@ -# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information @@ -15,8 +14,10 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +"""Workload schemas for Task SDK execution-time communication.""" + from __future__ import annotations -import os +from airflow.sdk.execution_time.workloads.task import TaskInstanceDTO -os.environ["_AIRFLOW__AS_LIBRARY"] = "true" +__all__ = ["TaskInstanceDTO"] diff --git a/shared/workloads/src/airflow_shared/workloads/__init__.py b/task-sdk/src/airflow/sdk/execution_time/workloads/task.py similarity index 71% rename from shared/workloads/src/airflow_shared/workloads/__init__.py rename to task-sdk/src/airflow/sdk/execution_time/workloads/task.py index 1a30b11c4b6af..ceff200856f06 100644 --- a/shared/workloads/src/airflow_shared/workloads/__init__.py +++ b/task-sdk/src/airflow/sdk/execution_time/workloads/task.py @@ -14,7 +14,7 @@ # 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.""" +"""Task workload schemas for Task SDK execution-time communication.""" from __future__ import annotations @@ -23,8 +23,14 @@ from pydantic import BaseModel, Field -class TaskInstanceDTO(BaseModel): - """Schema for TaskInstance with minimal required fields needed for Executors and Task SDK.""" +class BaseTaskInstanceDTO(BaseModel): + """ + Base schema for TaskInstance with the minimal fields shared by Executors and the Task SDK. + + This class is duplicated in :mod:`airflow.executors.workloads.task` and the + two definitions are kept in sync by the ``check-task-instance-dto-sync`` + prek hook. Update both files together. + """ id: uuid.UUID dag_version_id: uuid.UUID @@ -43,4 +49,5 @@ class TaskInstanceDTO(BaseModel): context_carrier: dict | None = None -__all__ = ["TaskInstanceDTO"] +class TaskInstanceDTO(BaseTaskInstanceDTO): + """Task SDK TaskInstanceDTO.""" 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 61313cd83cd74..332dd1b537d8f 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 BaseOperator, timezone from airflow.sdk.api import client as sdk_client @@ -151,6 +150,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 b9a05291aac3a..476db10184713 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 @@ -55,7 +55,6 @@ timezone, ) from airflow.sdk._shared.observability.metrics.base_stats_logger import StatsLogger -from airflow.sdk._shared.workloads import TaskInstanceDTO from airflow.sdk.api.datamodels._generated import ( AssetProfile, AssetResponse, @@ -147,6 +146,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 diff --git a/uv.lock b/uv.lock index 35dfc5acb7f4c..6d95fccdcf265 100644 --- a/uv.lock +++ b/uv.lock @@ -97,7 +97,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 @@ -288,7 +287,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", @@ -1448,7 +1446,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" }, ] @@ -1724,7 +1721,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" }, ] @@ -8478,25 +8474,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 b1125a19ec005ca70cc7b1c0c5b37be7a8d334fd Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 6 May 2026 19:55:52 +0800 Subject: [PATCH 49/80] Move sdk.java out of provider as apache-airflow-coordinators-java distribution --- .../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 + 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 | 8 +- .../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 | 90 +++---- .../airflow/sdk/providers_manager_runtime.py | 28 --- .../execution_time/test_coordinator.py | 232 +++++++++++------- .../test_providers_manager_runtime.py | 27 -- uv.lock | 86 +++---- 72 files changed, 763 insertions(+), 1308 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 (80%) 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/.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 9b98798f2bedd..a5a4ff9aa2157 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -1970,17 +1970,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 67837dca498b5..1221487818387 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -819,16 +819,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 4733bf26db21e..00d17bd390c26 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -553,12 +553,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) @@ -581,44 +581,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 ce35be9d26a3a..4deaa3bf598b9 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 2f376d98e0158..19d787f2967b6 100644 --- a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py +++ b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py @@ -111,6 +111,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/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 2c733873cd796..5cb138ad5e88f 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", @@ -1222,8 +1218,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", @@ -1467,7 +1461,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 @@ -1619,7 +1612,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 @@ -1781,7 +1773,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 } @@ -1822,6 +1813,7 @@ members = [ "helm-tests", "kubernetes-tests", "task-sdk", + "sdk/coordinators/java", "providers-summary-docs", "docker-stack-docs", "shared/configuration", @@ -1919,7 +1911,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 185f2f385c344..a59fd4fd8e9cd 100644 --- a/scripts/ci/docker-compose/local.yml +++ b/scripts/ci/docker-compose/local.yml @@ -126,6 +126,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 84847ea3e3041..c8223f3eeff10 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 80% rename from providers/sdk/java/tests/conftest.py rename to sdk/coordinators/java/src/airflow/sdk/coordinators/java/__init__.py index f56ccce0a3f69..daf8fce338d23 100644 --- a/providers/sdk/java/tests/conftest.py +++ b/sdk/coordinators/java/src/airflow/sdk/coordinators/java/__init__.py @@ -14,6 +14,12 @@ # 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.""" + from __future__ import annotations -pytest_plugins = "tests_common.pytest_plugin" +from airflow.sdk.coordinators.java.coordinator import JavaCoordinator + +__all__ = ["JavaCoordinator", "__version__"] + +__version__ = "0.1.0" 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 f304b068237b3..ab6c10f417fc8 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 68a5240386af0..2917689261a1f 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -1968,84 +1968,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", @@ -2082,9 +2058,9 @@ 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: 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 6d95fccdcf265..f718e17114d6e 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 @@ -152,13 +152,13 @@ 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] members = [ "apache-airflow", "apache-airflow-breeze", + "apache-airflow-coordinators-java", "apache-airflow-core", "apache-airflow-ctl", "apache-airflow-ctl-tests", @@ -250,7 +250,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", @@ -1027,7 +1026,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" }, @@ -1347,9 +1345,6 @@ salesforce = [ samba = [ { name = "apache-airflow-providers-samba" }, ] -sdk-java = [ - { name = "apache-airflow-providers-sdk-java" }, -] segment = [ { name = "apache-airflow-providers-segment" }, ] @@ -1641,8 +1636,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" }, @@ -1693,7 +1686,7 @@ requires-dist = [ { name = "sentry-sdk", marker = "extra == 'sentry'", specifier = ">=2.30.0" }, { name = "uv", marker = "extra == 'uv'", specifier = ">=0.11.8" }, ] -provides-extras = ["all-core", "async", "graphviz", "gunicorn", "kerberos", "memray", "otel", "statsd", "all-task-sdk", "airbyte", "akeyless", "alibaba", "amazon", "apache-cassandra", "apache-drill", "apache-druid", "apache-flink", "apache-hdfs", "apache-hive", "apache-iceberg", "apache-impala", "apache-kafka", "apache-kylin", "apache-livy", "apache-pig", "apache-pinot", "apache-spark", "apache-tinkerpop", "apprise", "arangodb", "asana", "atlassian-jira", "celery", "cloudant", "cncf-kubernetes", "cohere", "common-ai", "common-compat", "common-io", "common-messaging", "common-sql", "databricks", "datadog", "dbt-cloud", "dingding", "discord", "docker", "edge3", "elasticsearch", "exasol", "fab", "facebook", "ftp", "git", "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", "all", "aiobotocore", "apache-atlas", "apache-webhdfs", "amazon-aws-auth", "cloudpickle", "github-enterprise", "google-auth", "ldap", "pandas", "polars", "rabbitmq", "sentry", "s3fs", "uv"] +provides-extras = ["all-core", "async", "graphviz", "gunicorn", "kerberos", "memray", "otel", "statsd", "all-task-sdk", "airbyte", "akeyless", "alibaba", "amazon", "apache-cassandra", "apache-drill", "apache-druid", "apache-flink", "apache-hdfs", "apache-hive", "apache-iceberg", "apache-impala", "apache-kafka", "apache-kylin", "apache-livy", "apache-pig", "apache-pinot", "apache-spark", "apache-tinkerpop", "apprise", "arangodb", "asana", "atlassian-jira", "celery", "cloudant", "cncf-kubernetes", "cohere", "common-ai", "common-compat", "common-io", "common-messaging", "common-sql", "databricks", "datadog", "dbt-cloud", "dingding", "discord", "docker", "edge3", "elasticsearch", "exasol", "fab", "facebook", "ftp", "git", "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", "all", "aiobotocore", "apache-atlas", "apache-webhdfs", "amazon-aws-auth", "cloudpickle", "github-enterprise", "google-auth", "ldap", "pandas", "polars", "rabbitmq", "sentry", "s3fs", "uv"] [package.metadata.requires-dev] dev = [ @@ -1799,6 +1792,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" @@ -7035,46 +7061,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" From 044f3ee2e92be0ac4164515c23dce8d8455bc49e Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 8 Apr 2026 14:17:15 +0800 Subject: [PATCH 50/80] 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 0c6e0ba2149f3e04e5bfece0c5f29b10f60498ad Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 8 Apr 2026 15:40:05 +0800 Subject: [PATCH 51/80] 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 5cb138ad5e88f..71891f1b4f0de 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", @@ -1170,6 +1174,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", @@ -1437,6 +1443,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 @@ -1588,6 +1595,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 @@ -1749,6 +1757,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 } @@ -1887,6 +1896,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 cf78e9258f39f..eb84a76015ce6 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 67b2590f69b61..9d6a37acb3c29 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 f718e17114d6e..4d1a0fab00286 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 @@ -226,6 +227,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", @@ -1002,6 +1004,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" }, @@ -1252,6 +1255,9 @@ kerberos = [ keycloak = [ { name = "apache-airflow-providers-keycloak" }, ] +languages-java = [ + { name = "apache-airflow-providers-languages-java" }, +] ldap = [ { name = "python-ldap" }, ] @@ -1588,6 +1594,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" }, @@ -5847,6 +5855,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.1.2" From 4b45c319f24f99a82a033ae25570082b0bab3520 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 9 Apr 2026 16:12:37 +0800 Subject: [PATCH 52/80] Implement Java DAG file processor with TCP communication bridge --- .../src/airflow/dag_processing/processor.py | 45 +++++ .../languages/java/dag_file_processors.py | 191 +++++++++++++++++- 2 files changed, 232 insertions(+), 4 deletions(-) diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index 00d17bd390c26..97f2ff1afd1d8 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -161,6 +161,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. 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 9b876b22993304d61b96582da914c8f52d7d8115 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 9 Apr 2026 17:55:06 +0800 Subject: [PATCH 53/80] 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 ac239b17bbc5fdad170a1e59c77d256ccffb27b7 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 9 Apr 2026 22:32:20 +0800 Subject: [PATCH 54/80] Fix java process startup issue --- .../src/airflow/dag_processing/processor.py | 2 +- .../providers/languages/java/dag_file_processors.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index 97f2ff1afd1d8..d8515a1b98998 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -189,7 +189,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 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 585e336af5ed0255e2f59fa84bce3b7d2e98d82e Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 9 Apr 2026 23:51:19 +0800 Subject: [PATCH 55/80] 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 d0cd6830854c61676c9d70900e8e5d535ea5b658 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 10 Apr 2026 14:38:33 +0800 Subject: [PATCH 56/80] 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 5645afc4929079101418528acc160942367b4808 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 10 Apr 2026 17:09:39 +0800 Subject: [PATCH 57/80] 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 5dc37efd621f52beefefde344bb18e5f6133c5d0 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 13 Apr 2026 09:58:29 +0800 Subject: [PATCH 58/80] Add Java task coordinator and entrypoint for locale-specific execution --- .../providers/languages/java/coordinator.py | 31 +++++++++++++++++++ .../languages/java/task_coordinators.py | 21 +++++++++++++ 2 files changed, 52 insertions(+) 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 + ) From 64416c40660235b1a24721017b8f3e420dcb2941 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 13 Apr 2026 11:30:24 +0800 Subject: [PATCH 59/80] Refactor Java provider to with generic process coordinators and update related components --- .../src/airflow/dag_processing/processor.py | 45 ---------------- .../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/providers_manager_runtime.py | 30 +++++------ .../test_providers_manager_runtime.py | 16 +++--- 14 files changed, 75 insertions(+), 238 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 d8515a1b98998..00d17bd390c26 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -161,51 +161,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. 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/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 c2c8fd96363d08c717e7f215204a22d050a854f2 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 14 Apr 2026 15:26:56 +0800 Subject: [PATCH 60/80] Fix Coordinator by getting the correct dag bundle and dag path --- .../src/airflow/providers/languages/java/coordinator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 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", From 2cf2b8587af53198fff22b89986d54a506b2a5cf Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 15 Apr 2026 14:12:17 +0800 Subject: [PATCH 61/80] Make @task.stub(language=java) works --- .../execution_api/datamodels/taskinstance.py | 1 + .../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 ++ task-sdk/src/airflow/sdk/bases/operator.py | 3 ++ .../airflow/sdk/definitions/mappedoperator.py | 4 ++ 7 files changed, 93 insertions(+) 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/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 e5b19f2768da9..191d7406f8f05 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -563,6 +563,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) @@ -750,6 +751,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, @@ -920,6 +922,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/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", []) From bc30f01098a682156cfca8f0114eb87e2ddf56f6 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 16 Apr 2026 10:14:19 +0800 Subject: [PATCH 62/80] 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 ++ 6 files changed, 291 insertions(+), 22 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"], } From d542bca4598585f7a63ed4e64f0458e1be282ff2 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 16 Apr 2026 14:57:37 +0800 Subject: [PATCH 63/80] Add get_code_from_file interface for BaseLocaleCoordinator --- .../.gradle/8.14/checksums/checksums.lock | Bin 0 -> 17 bytes .../executionHistory/executionHistory.bin | Bin 0 -> 394779 bytes .../executionHistory/executionHistory.lock | Bin 0 -> 17 bytes .../.gradle/8.14/fileChanges/last-build.bin | Bin 0 -> 1 bytes .../.gradle/8.14/fileHashes/fileHashes.bin | Bin 0 -> 72765 bytes .../.gradle/8.14/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .../8.14/fileHashes/resourceHashesCache.bin | Bin 0 -> 25433 bytes java-sdk/.gradle/8.14/gc.properties | 0 .../buildOutputCleanup.lock | Bin 0 -> 17 bytes .../buildOutputCleanup/cache.properties | 2 ++ .../buildOutputCleanup/outputFiles.bin | Bin 0 -> 20009 bytes .../16m28tq49dyxa8sxlh9trlhos/candidates.bin | 1 + .../709055z91n7x7acrnofh9dopi/candidates.bin | 1 + .../72bzbc1d4mxr7ot5a2is4jphr/candidates.bin | 1 + .../.globals.work.bin | 3 ++ .../.strings.work.bin | Bin 0 -> 26774 bytes .../_.work.bin | Bin 0 -> 2109 bytes .../buildfingerprint.bin | Bin 0 -> 20197 bytes .../entry.bin | Bin 0 -> 100 bytes .../projectfingerprint.bin | Bin 0 -> 1132 bytes .../work.bin | Bin 0 -> 1455 bytes .../.globals.work.bin | Bin 0 -> 1656 bytes .../.strings.work.bin | Bin 0 -> 72886 bytes .../_sdk.work.bin | Bin 0 -> 12037 bytes .../buildfingerprint.bin | Bin 0 -> 19997 bytes .../entry.bin | Bin 0 -> 100 bytes .../projectfingerprint.bin | Bin 0 -> 1132 bytes .../work.bin | Bin 0 -> 1860 bytes .../.globals.work.bin | Bin 0 -> 1659 bytes .../.strings.work.bin | Bin 0 -> 73280 bytes .../_sdk.work.bin | Bin 0 -> 12178 bytes .../build.work.bin | 1 + .../buildfingerprint.bin | Bin 0 -> 19997 bytes .../entry.bin | Bin 0 -> 100 bytes .../projectfingerprint.bin | Bin 0 -> 1132 bytes .../work.bin | Bin 0 -> 1916 bytes .../configuration-cache.lock | Bin 0 -> 17 bytes .../.globals.work.bin | Bin 0 -> 1656 bytes .../.strings.work.bin | Bin 0 -> 83718 bytes .../_.work.bin | Bin 0 -> 337 bytes .../_example.work.bin | Bin 0 -> 12757 bytes .../_sdk.work.bin | Bin 0 -> 6591 bytes .../buildfingerprint.bin | Bin 0 -> 25023 bytes .../entry.bin | Bin 0 -> 100 bytes .../projectfingerprint.bin | Bin 0 -> 1132 bytes .../work.bin | Bin 0 -> 2433 bytes .../f24qya0scm6443semhj8oscit/candidates.bin | 1 + .../.globals.work.bin | Bin 0 -> 1658 bytes .../.strings.work.bin | Bin 0 -> 72970 bytes .../_sdk.work.bin | Bin 0 -> 12046 bytes .../buildfingerprint.bin | Bin 0 -> 19997 bytes .../entry.bin | Bin 0 -> 100 bytes .../projectfingerprint.bin | Bin 0 -> 1132 bytes .../work.bin | Bin 0 -> 1864 bytes .../.gradle/configuration-cache/gc.properties | 0 .../wtwn4tdzghcdqk9l8l9bc9zu/candidates.bin | 1 + java-sdk/.gradle/file-system.probe | Bin 0 -> 8 bytes java-sdk/.gradle/vcs-1/gc.properties | 0 .../languages/java/bundle_scanner.py | 30 ++++++++++++++++++ .../providers/languages/java/coordinator.py | 18 ++++++++--- 60 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 java-sdk/.gradle/8.14/checksums/checksums.lock create mode 100644 java-sdk/.gradle/8.14/executionHistory/executionHistory.bin create mode 100644 java-sdk/.gradle/8.14/executionHistory/executionHistory.lock create mode 100644 java-sdk/.gradle/8.14/fileChanges/last-build.bin create mode 100644 java-sdk/.gradle/8.14/fileHashes/fileHashes.bin create mode 100644 java-sdk/.gradle/8.14/fileHashes/fileHashes.lock create mode 100644 java-sdk/.gradle/8.14/fileHashes/resourceHashesCache.bin create mode 100644 java-sdk/.gradle/8.14/gc.properties create mode 100644 java-sdk/.gradle/buildOutputCleanup/buildOutputCleanup.lock create mode 100644 java-sdk/.gradle/buildOutputCleanup/cache.properties create mode 100644 java-sdk/.gradle/buildOutputCleanup/outputFiles.bin create mode 100644 java-sdk/.gradle/configuration-cache/16m28tq49dyxa8sxlh9trlhos/candidates.bin create mode 100644 java-sdk/.gradle/configuration-cache/709055z91n7x7acrnofh9dopi/candidates.bin create mode 100644 java-sdk/.gradle/configuration-cache/72bzbc1d4mxr7ot5a2is4jphr/candidates.bin create mode 100644 java-sdk/.gradle/configuration-cache/8bd1a6aa-5b1c-42ac-b3f1-4cf982bf99a3/.globals.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/8bd1a6aa-5b1c-42ac-b3f1-4cf982bf99a3/.strings.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/8bd1a6aa-5b1c-42ac-b3f1-4cf982bf99a3/_.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/8bd1a6aa-5b1c-42ac-b3f1-4cf982bf99a3/buildfingerprint.bin create mode 100644 java-sdk/.gradle/configuration-cache/8bd1a6aa-5b1c-42ac-b3f1-4cf982bf99a3/entry.bin create mode 100644 java-sdk/.gradle/configuration-cache/8bd1a6aa-5b1c-42ac-b3f1-4cf982bf99a3/projectfingerprint.bin create mode 100644 java-sdk/.gradle/configuration-cache/8bd1a6aa-5b1c-42ac-b3f1-4cf982bf99a3/work.bin create mode 100644 java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/.globals.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/.strings.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/_sdk.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/buildfingerprint.bin create mode 100644 java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/entry.bin create mode 100644 java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/projectfingerprint.bin create mode 100644 java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/work.bin create mode 100644 java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/.globals.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/.strings.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/_sdk.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/build.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/buildfingerprint.bin create mode 100644 java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/entry.bin create mode 100644 java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/projectfingerprint.bin create mode 100644 java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/work.bin create mode 100644 java-sdk/.gradle/configuration-cache/configuration-cache.lock create mode 100644 java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/.globals.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/.strings.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/_.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/_example.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/_sdk.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/buildfingerprint.bin create mode 100644 java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/entry.bin create mode 100644 java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/projectfingerprint.bin create mode 100644 java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/work.bin create mode 100644 java-sdk/.gradle/configuration-cache/f24qya0scm6443semhj8oscit/candidates.bin create mode 100644 java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/.globals.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/.strings.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/_sdk.work.bin create mode 100644 java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/buildfingerprint.bin create mode 100644 java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/entry.bin create mode 100644 java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/projectfingerprint.bin create mode 100644 java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/work.bin create mode 100644 java-sdk/.gradle/configuration-cache/gc.properties create mode 100644 java-sdk/.gradle/configuration-cache/wtwn4tdzghcdqk9l8l9bc9zu/candidates.bin create mode 100644 java-sdk/.gradle/file-system.probe create mode 100644 java-sdk/.gradle/vcs-1/gc.properties diff --git a/java-sdk/.gradle/8.14/checksums/checksums.lock b/java-sdk/.gradle/8.14/checksums/checksums.lock new file mode 100644 index 0000000000000000000000000000000000000000..38a166fd6fb05700338ca9ebc7d973ba9f2ea7f6 GIT binary patch literal 17 TcmZQ}T)n3J;vZLK1}FdkHdzEn literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/8.14/executionHistory/executionHistory.bin b/java-sdk/.gradle/8.14/executionHistory/executionHistory.bin new file mode 100644 index 0000000000000000000000000000000000000000..cf75a404b59c22040bc4ee7e24096c724676906b GIT binary patch literal 394779 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 GwEiCxPE98O literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/8.14/executionHistory/executionHistory.lock b/java-sdk/.gradle/8.14/executionHistory/executionHistory.lock new file mode 100644 index 0000000000000000000000000000000000000000..341393d4aa0273a735e844435283af570e5207ca GIT binary patch literal 17 UcmZQ}2s~t9cKWy;0|b-<04pm4^Z)<= literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/8.14/fileChanges/last-build.bin b/java-sdk/.gradle/8.14/fileChanges/last-build.bin new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/8.14/fileHashes/fileHashes.bin b/java-sdk/.gradle/8.14/fileHashes/fileHashes.bin new file mode 100644 index 0000000000000000000000000000000000000000..b256c6c11e6dced7a66155f6d2556abd8d4b4167 GIT binary patch literal 72765 zcmeFac|4Wf8~?kvd7dMglaNelQidWKA|xc4GbA!)&OA$0GS5k-L}`)=At6+fCK08S zN=XrN?!DK#@9$&pUB7eAU*~mR*6a1$Jy{>g#>srITwR?8paLYLsV8i{dE&JcU zF8$k529`3gl!2uTEM;IR14|iL%D_?vmNKxEfu#&AWnd`-OBq`iU;UA znBSqm=)JmNCc%*t-^CdZ~3wDueY~pr2#K z^r=qyER|o8KzG=H>ajeM-b&M7dVyYMf$6%rPn*K7F%otB@;XePowejEExQct&+?;s z+}dY3?c(isfxSQ}s>ho(^D7rx^#J<|?3jMvu-5vi#~{!vm!WzB%D=5J@#IRbRkdQ?Awn{6?8B?IHl=EHQUxSJDVp##AF5H^n| z(x(}_yE^v+zr3xeeX?jtZ}WvuX2AYmj&KL#0vuq;Cb}1ofwbr1o|0wR6nh>rpqf!GY9B?*nFMd6r!+N>6;GF?Mg8F>%3gN zCyn90ky(f8si~v6lKz3_M0-519jc#^py3a@pZ){%V|5$T(=+tsxC>4JJs2BLns9Pn zq~N^{pci8En6@gp{@BntIKN)pXg_IzdG62HJ3j#X5UdWgl>}tAG75T>7tYaXF(Wow)?iW707Fw2W8%Zin|k54FVfiPo^0 zk9(bfo`{VzQ|$HVr{EJBK=-vl?KABrqcb+|vjuu`Gp4`X@{pZc>?6>1AEA1dujC=G z)4K(L?)4JWLnZHCT}@{IbVIDpvx^&KlFzVL0zFm+wa;$KZTL|W1I`ybw-2i449Pt; zvuC{l>`xD%dMBG5~1P`z;57rot4lEFZazl!MrPbOs9 z`f7+ep0yp-i@w|TAF(p|4eVnSQN5TxDlX_ps2b26<}p3Fw7zYl3A%3tV|wl7CIx?4 zn4jcRs9xf%^7M{rFCy_T^Q)4)Cg%R|L>Roi!3OkP0O!`4?tvhJI__R+Lx|5dwW%&WGCJpp4J- z^cV-UpNp?}M3k;8C=+!&3%2i7$7~T(vp)U`*eBbd_SF?klisrX;(%_C?LRdG!9Vw& z@GJ-Ya9%>~FHP^5577wj20AOYZ(j~!%`9h<0s9Z0=MidOyQb{fiZ6-$p#PKDyj)p9 z)pY2#^ID*v)<*5G`c^Z|2_Luy^fYHwzs~F4_`dBqHPEeNFx}TZIx#8K4(QQ`Fg=cO z_45rJjzBkikLjtORfNQ};C(3%%m4LEhJ6jT#ihVL&K$F!GrqP8c)^Kc0_l1iE_@rjKX|`sbWUpM#P4Xkde0QXYdKCGXcPMP^K0d(+QoKQ~=nUR89QhWmVM3aZyLWtm;&Usnk1b9*t}_hhj^d(Um4AM!=@Td{{- zZVsNP1-ctnKeuALPDGWwy9e|D1I&JIrKSnXB+O?ewjbX1WE)+#GV=kjKi-Gh-%i_I zgcFv8_srZPOh5a%1;hZ)B2K_`Rz^71$eM>$I^>wODb}PI&IPrD681oj1E^h2(&J`Y%*(N?jMW z^U16U&`mJ=yW_h0ZVMM#fqn?vmzp=6aagx^8_dIjQ)oZU&dP;~R7Hn?eFC=sxA4$z zp?&uU+!Ju=*#6L>_9ya%#GvmZ`&GM zjH*{UHURr5Y<}Aw+Ov+?-huZV=LoEy*SDUj9r(Ng*zc#u^!E#)J>T@_mG!&sNmFeKdrrU|iT1^po`t)jRZpR^9XRoB_H8R)-JP9xFP|>N5uP0xX{o z!qnw$f`#0PI-W}#?WZf^qrA59C^N86EXVX$H@g}#SWAHJaTU{tQr=UYKXxDJNry50 z$B(q>)~W@d2W?07Zob3!-8bck0bREM)3-cOjW%$X0(#0=JDajyPgN1e_aW58|=JzB>TSM^d@#ipt}sB z{XDwB)-__f>^;yAG-LYbwU4xxtyxdh@jQ&E{y0QH0Z$Wj9@rav!t}m3m3n8U;XZ8S zi|MaF7++3~PY3o%*g4qC`sb5I^(PG5N9OMR-UIB5xiOtdDV!zha}v;Z zVf)V$jysBJNgu&_#4k6+?8Vh=?xr@J1NKK9F+H81xmm6R-V^e2P`yvWBi#1tRd^qE z#KzONT|swngtim(bEF=#&j|nd^^*8cqK@Yb!}NJRjq0biFrR7jsQ&b}jyR)`JlqdG zj4{1G$6$zcyFKVX8XHglWp9Z{>F@%e?~Xw2pWS`ZANtD%ywBoUvACY~Cc9Is^22*? z-X_fcjh(?R_sB{x&SltmUa;Y>&^)K}h2tMV?O*r=ZZaDRtOtGrvGxAqT&kT}+v-7} zCr@Ga6AAfyYL#q(Zmf#xFWHL&Ha5AM!G4xu`syI<2h8r^9*Y;i_Rp7AA09OQl7V?U ziq*->Tou`0Lp(X4pCT-8FK?+%o%1h1h;+gUbkcp{hB*U0Ng3_ujhgfx=JW4f1HC8;)2)}SrB&>J`%gkO zst>b|dEPkLc?#Gc>%?^PHJ=2VqTc}hbS0*zGIeWO)RzLiPy*9$UwrUwcfCI@KsURJ>7(xK_I-)DKtGF(b7b1g*u<1S8R%|`sQp{E zl)H&?FAf0RAq&&@8u7(Q)p`QmGy&6-ynZcbz1|OW-)2;QM`xzNe=ix1Kjk*2a~D@j zXS_E8_MRcAKDtIFJL0AYyf2-{>UK2hoW@XSJG=)5v19g$T8C(}M~6T^wgZ^nKpS#3 ze*Y7oJAOm;_uJ`B#g6}g{pYt~x_ADNNT7O4qcZw39NWA*cay_5UT znU@y8ei=1t|3Sbxcc;4rTrch^m>znOP021c4D@4Ahv~x)9-lI5X#x5ftZqMgSopYZ z`3d*2W!O3zKh8hBM(iPY55n_e=f(KVyFa!ae#8O$d4%@=*@#BrN`TZVpclRWpZ>8X z?%PgqzTiq_F+HnKzQ;|G5!hR{qxzTjGxw=C(}8mn7u1jHUuC)ic&$USp?wCX@7F3} zE=~FZbT2JTzh+Ww-w?_I^b=S;e|_JrDqo{t1$6T_nEfC3;30cwQ0I6K6I7oNw2@iC z{bB^zdtmEv;{Ms4^C5hHfS&mYv!9INVZ&9y`%k3Jfp8bN>Vu?|1svoDcu77TVF@}pB!IZ z`DAK2tY_~&OrQT&${g_h0TP4^j>)UycL1E><+(NY8?PGO%?q%~0G)eYr3O=+UV8!pwy&G&@ghhWDR+*gVe0 zUb(bJ-Vyenip4vZY+U&BKs(%5_coy&%~kAH_Mq|80sSAe!}PxGRvIbeu&!LHP<`H9 zxlLZ_HLU000!&|c_1<>u%mdJmCsrqa?(6ks_)7c+`q^Em{en{BIR&DQ=Mcl}Z&D9Djd%y+@;Qv^xD6Jpry94weIr~5(+w{w7L;+V1^t`zV|r$c zUdmQ^Z=jzJM0GsBXNBv0H81eXW`pWf+ov+C6@B6R^2r!XFWN!VywOq-*mDY?I<GVwxT*sOG=K6fy5vIK|xj(pr`g@_9un$f3{Y`^&4;r)dl5r zpC@HT!E-#r64N(YABmHdodW%MVEdtQuYwgZLmBG##Z2_-z`i6-Mbj;r?|B; z^q~4PpqEKvdRgy&yZ1X_of~5HEZ8zqu2-86?+GStsJ*b#W|JW0+0&q(d__#pynMA; z!UW70P9IyR!jo1XjdM4^`rm_^FNkoy6y(1k2>0RON9b>e+*K~H;|qHV`VYhAQPio@ zW!Z>7yeC*XqV}TRv#UfyZXN;ljwdiZF1kg4Sq^^w5Yme2H#q9fsP8EP_Bqc{U5r|| z{;OZjE}$o4=Zn~;yaw?F5oVxAVEGY?)o7Xzt%d6)b`tGpr3-RB6cmB`TLm`1D_wSd zSZ%@#?}59p@vIDIV>ch#yBG9-xC`s&bjCeZi2MC~P*pT7LrtK?U5yX9?_!{W1NN`D(Az z_z<9niDUZL=+_a9wFiM-j-7W>u@d6=3wPUqUVzQ7^tuPXDsuZ_-p*&B{YWeMj%|Lw zJ`&iwV*8SG;-*XQRz861-P9blUlXsJmHws?)^picOs``sNcqqV_mg;IOdl8exgbRc z@3(tsQC)^DX&h%6ECKxHdSE)YE8p2S&uV}k^*^2GUQWbU^mAk5|LH5+{4;uK;QkYZ z&5Mls>Tor+OFKb7mP%Ov*(wSOgL+Otw=qEVwXMl&`#81V0o}L=)n#>l3vD}O3;T({ z=24FKYU=H@3-BJ5unV=96FT_gtX~m)f2_dvH+hNh>B~J*;PVRHZbQ^wQF<-oMMtUo zz;Ce@rps@fxMbW2^A=u<>341RjpeRu1ojbwsJ?Ek(X{&&C+N2jo3C{r)^&5`-IQM+`2JDZkW4fHhpp80y&g zRTTd$J1O&`&s`x~fdkea&?mM}Z#q z8P(NTUWYx3VE+X4U`A9|r*>Ey=sgDWZ@UcB<3&x{c8lo&`;t6NZ@sZi(*3SE(Czv$ zy^U$R@iWy|KtGG^|J&yz?^a#ThT}ZujM{53if9>2uVM%GdjHdZmw3L@@>maatK+D> zrfka763cb)bB0W8U)^DFxOF}6a`?V~QVg}<;o+icaoz~t8!fQ;(qhO|qRNrG2m06D zirQ;Q7)J8i|1k!7HnwkQc^6-7e75Wj&~r*L`=s~0`%ZjU1iGsSreEUqI$!Pc73gW5 zn11KVB-@Y;RT($`8rw7vyZsL*fegorjYeRM2IQyeaJ5u2XNnYicKFDvYHKP6)^kXK1 z>U!SR+qf>R+6#22PpH1@j^rg*^Y|8^pU0!RzE0TqnOT!vK+llIbR&W1deQs00KGg0 z)eS7q?AW(E;u6rau>Etls_kAQnz!eH9^`}C?{?BYD7)PO-Zw(*FugOJe}hC)H?ZG> z-E$1_!`CMBUuy$>A2(`mxWRbba>pNOpchzSx(jvSyO`q}fL?)}JBA@A0-rR@%>q5r z7qf5dIkWb+MJ&*r-(mXuc;ckmxey=0Phz#_eoUWqrYnFL}9Hk=%)-@FMFb5cMF+{!uyX7wodo-cNz?d*7pJX z1gs8C=_l>C9F0B&^i$Y4P1oOzvh9$8@2yU8s9#eJ&9C;;@$h^(HjnAyNoyW>D8hZf zRvXpLBKm&Qra#bv{bTtwJMOXRXNzMi(9@_<`+a*Sg++C(&biocb*X`Otiz zR|aBwjYDAluj;KpKTM12Hsgc58qe0i^-}m5)$Ov%gQNylz57q6h!op5U{e{Bd+HL9z$*pTmjj4(B%AH<&H$0J;MC$y*_K2TM4f zBUrqLrX^kOkGpz;{*AEtI!rC#K_@T_@9ig)(Ei=|lB-2{wK9RdeF3H$SsNV^qe%yP z6f>rOJw*F9QE?vVsoPQg2p)g7hSm|*b9e-*dnkPmbG{k(64;;X!}O}OsErTvQh}a@ z<=^A6TiBuN=$|j$D}&m5Iu+FG9nOaD<3(37{gw{fafMr#K|gjrsP1JcPcNb82G93$ z6HFiY@afynZ*U(@vB31%%Dc6yW#OP7BW&Ki8yVi4EJP#$JsP`rdC&gjVc9tF7U-ug zq5T|{JXCMhm;=wXba_k=SAqu=9qs!p9;>pzFG0`sfAwx7PU0Ku?iG^+2{8Cj`|5HUa(c zAx!59^5MDb{R`-)v3pwJvP}N-A^4j>_dSK#%k5aRdoPVR(9Nqc{o~sZ9lPgefo|e} z>Op6U{B?XUz;nlf71L{1?hW>tj|cX8S(sk;IOqZUhG?Mg#m>!RTYI~i8Xm&)(c25P zKX#_eg~i*p3)n|;V)~$W(A*JYIKL;bb1)>VX2Xk@EgZn!$rQB@Y46zO5_babhc+Xq z9_k%cds#UX=GjCM)6X54@V@>z0rcaC+T@AbVAqXYdONkID#8+N_d%~}fU^W=3* zpFexcu0;seXUcI@M}I}W55TE}I3ZkA#0dXz7<=kSZLbmvr#f|Byupg)H}Y+Pyc`;j zI4}}CVMml(OsIciuu2n1uuM>tc#A}$VD!rVT~%%QBO=psSym(j;k+doRmmz9k-55c!>V*!E>d~x(k~|H2sbV~&2|#$jT;Z3I z6I*EG)57KUs@l`@xP7$Kw}YdCky9ThCvH~cT}!$ff51NQS?7o4`N%1a%!i$q24YEQ zgk6_pR9D+7gTxa+5T7KY5^Z)I8EK3S7I@$M=ixrWpBy3h2t-Cl zXf%W!l7wjZ;&ov%g>_d8dyD5ek!qwWhy()>z{!3gCwvC7ze_3J8_%}fD174T;Wvv3 zYQj%*NqqEtJi5uwq=q%lM6+cqWXC5+hyWkugba`nZ(n~f68B9!d+U4HS!?wq3(cp65Ok7i{zNXQWZBoK1qpyKn{C=G{=db2d= zA7uPwf`lRB*J%9g!X3zu9F*MoCJjuzk82G8!4aw_B`L z?Lp5RBnXy-CWZV%k}LgNc!9P1eNwfLv#X8zwzNWmg=mk%XOk03GcyNM2d^D>)#+fB z*?bxu6`T(mVRAwhwua_?ClgX?IT}N8WXHNWjk!=*P$jDVNrtKhFAl)$G4+f9=2^ataWA z^a39=gkMPNN>Umx@l%S|dwKW=^%rq2?;ru+SLq1rgoH@)tEG0o{*cFKboj|t)!T5L z`@aT0MAwlMf4=O$V_uomnzEV}@1b=C84Yn%=75lXNKPp9_nX}sRf{cFh`piNIOqil zT|h7=k`u399+lnN$ftWJe>8sX7UvWo{DT3pR+*gm(O2s1Z9U;vT{rKlGLnpuX4_eRT#^b3qR$W> zk$$HMeZVy+O`%Heq+}dg>#!hWYA>*D^jr!x^ub4TffGz1_i?K#i(ToS+?I|%WjUG^ z3h1Z^bB(l36mDeO57B$h(i#;?>@j0_&cS#d`miMeI2I&#q;X`0k2=2By0`N*|3F-A zYZvl~KQVH`4F+j+*2se-a+TlBH0Ak^=7zsMbZV|Nf;(IQVeKJ}W)gXj5c(^4v)(Ab zRVx!sy)x-~_n%C3j}3H#mP>PN?&M3Z51a{>Yv}Xe_D7gTx2jE_?qn z`8K}tdn&s&sT01DL5#zh2;gWb4G{(VCuZRp+FbOX(%Lgb_&R$?U}D9w3${iL>I z-Pj#r=awTO;e-o5k%$9?Tp02z$*AfU-noo~b+Ir=#XDy-7{e3CzZDR)56FpI!q+}; z{-E$tYjQ*JJWZSsj3WRLbcA?Fd^ARHk=gj(sV?*7u3OsA=a5sGII8!6V6`PD`ewC- z&0Z-_T`QI3n0b?q@oMO@m)}K@`igEp%9x&YX5&HN=u*czOk^PI5 zD9={wY%Y53HCz8KvZL1mx#1Ii_!0pe&ogquWtDKL$4L)mO}7{Q{LPky&_^;LST~Uq zjAxwn>}s}&F-SI*SradLy9yi;l0(H?-DoQo*1A42v+1qKuV+xFKl;Fx}Be#7N0ZWY`+qYig0U18l5rn zAn_qRV*I8t?)!x?zn=BZ$D+_vjhzVKBsIv1hD#TJy=c=@*gk03R> zlM*Xum8iIYwjrd+!KYJw$31o*LG%g?H zL6R%E14&oiw`gR17uE*zBy5x{LFtP>LA^sJ0z&6mp5S$-kIV8wv} zNNfZI-E(r{(QCtcUgxnJR1+KDTzN1*2IGLY3|hj;L*nCB=yDaW{pv&fuWWeSH)hB| zAIJ@p(0Df>4-#U~a@E&rrALobwj#rDE_-@l{<-KynpH~6xzqpc} zk<*O0YV!a=r%z67x^*wCG(c)Y{n;Ac7k9UnLE|s8@w7v4T&itfMcbwYB&3+X;YU8-xQX1j-1!O zeFPFP4(<|iA4GrXy&zx2_Em z(!d8TVeUzCWwHP1F!RR9b7_*EE8jlLK-M?W2M-|V49SUOF)A0%kIY)#+_(Eo)msmE z;sip1k-{3Bc!In>!|hT91QV0YOKhN_Ochy}Bc^Un=3~HWlk! zdgQ%>NN@v!5!ty(NA*~uW!&sdt-XD)Ry!2~H@qhXazKK@>8#1~PHM$(Pdc%D*cC&_Mc-nx3t4+e)TemM)=d; zJa1bU6ExN2MDtrV23@_)-=~Iu?7Y>-e{eCuL7}chF9|&s-0CYY@mP|_X`_rPB;ffW zY(ws2MsWF$SEl{;Z+ki-=~a}yAQ6LDB8{3tb`|}n64?)?-5z`X^rdRumhQy_p5Til zyYK1rA0B)r_RAwrQ*~R~-gAoyfmP&$Uw?lbLsMiuV_mnqSI{WQs5scjiF>PJ^fvP9Y~;4?euKj8)}Dm!#rx>g^fAkbt>TR3;}h?k`-On0Q`j z^Ks=__LU;wbf)ouJ|td{6HhbG*BwyN?I^!%VAZobM0Bwasy=ceT3_{Nlh((^J$qau zoz;5ri{oI)Bqy##=Sr5EGuJKbzb@u?*!(;sq7h4^;fs(5$$Z2}n{B?Z`&8ngyP-y- zVga;^2}Z(>O+t7T-L3gk^M9u+Q%oCry?`{C2|O<{vYOQVg*9_PhA z@a5zL{lufepf;Vhk@s6d_A)5?bGWm-krLM6oc;u`416Gq)ntE4-y|5^41;kQ=Ro&jLN<4IS>CJ`moDCLKJ<<$r03(ePn9?i;D@DUnKx33j9h zQX(n;is>%nP=g?!iBPW(KapDnVQ)tx=Zz;P_`XT}v?&=--TLsF+JUAtBuD=x7%5bV zg?#Svg_Bo`Dpn1r9GfEiT~YrfERhvPI;x@2J*S&GNZv(CJPdEI`7pcGJIRT= zJOM3D>*r~h2H(jfEVDq?+<$#gBPS`Tj|OE`<+7eN^XuX{R+$)ZA!iejfZ1IpNKUkD z8PwQwZh7s>#)V=kUN<#Jz)>+fk`pv6l04sh+bd$Sn^)ZFUk_>*%P#d5a-w9L>hbp@ zuevVSdv3>6(UmG7=Owd)4`#7kP*_C_w#*MCfY}@SFkMCZb zD*+0X7*S>$__@8_rYie`&=Dzaw01p_zLCaejyy=JUA(bM?PR)C!$s}k1I-qS;54v9 zBd4OUC*I4xBiI?6mOfx6?W}aoR2z=UgXjXse4pIMI^Oih`!&9Go?#md*yP+Eu$W-r zCMWElXpLNuuhU)?{h_VWS_GUpcKI+4Zsf*H8pj~cYkk3!{1o*!L(81=r4B$J?qF1m z6yhjyVm+KIaK@uJHnHp0f%6iOKu%CXGem9#q&`H`kKyWN2F6)+=`_^e>?(%@@{T}g zbjU4`lu%Yba_K-u8Q;1JTh7~0;(8YoG;-twb?HF7+Ru#mU1oksjSR9+AmK*vfZUKN z#Gx_oxl`st@Zmi^SDw}yGG2oOjGP_WjYvne@>gg}&%1n)qm0kve0R4MKmum>U<)}> z*Bo?Ht-mKA>zAe9MCLW*RzY0n&<6tpIkDTAQ)fUu-0A7>#}c${NwScDxne&$POZb;y>=k)O(Bkv#qBWI)VDNeQjqr#o5W7)IbMx4z^9O;Jyti&A@B0oLTc%xll z!`UZ?eAq_B9Ka30E*}ysmgJ*a{@{`42gc28akszCNNkWtM@4u?LmG`Y@*r7J!wiDL z(ZBHBPfu-F$t#jRzc^P+6o^eUYaiRcVLPoY|Fv`Z@CKH}aj-~{`^d2V^X8m++I6P0 zEt)DuE7c(Zv%B1goY->9E^#eOdxw9jYrSs8wh~CdIp^3;PHf*tA8hc5FEC8$`_xTC zThs@f58h64q9%&z#l)*4Ww{lf6C=A7J}!=fhMk;P5yO+7o5!>67Ed>o%zRhO;y73X z$%*aS1rn}bPjXz=zEx)Sgz$F@5-ZUeu|ygbE%G3l57B3|w89VY(vJ1tP3`j5JzeaB zy@#B5#N(RSUHj72qpgX5!rpff5^&D>koh6?F($A1V*FSXlZ+}2i-fk_3?yK7S5TNi zQ4zHwfzzKBBDjUSUTw-5hXhf`I=j==o;mnJ?-PJUj&u-|0ysCW&?34WZZ z@~u49HFLe+Iuk}mkWW^Kxq{VBKTA$LZLU7~*)fsF?GJrJom$(E#e^g$IU$w*o5Lo( zTG5=<`T43kLNbXyU?uV+=R0W}&py+=ju$?0G%NkmH;LedyNlys^d~1KpZVs?3>G+S zDJzdZvriVOBcczOUG{Kt;;rJDd(UsUmF&FE=6y!TH9^XYf z787*Hnj|G&-jGwDPPlygx$aF9{M!|FBymuWFFCn zUi4)}<6;$X%H%{M+s!9eX6~vE|4RR|H|!|K;yCEX$q5lNa}oWA(IvGRbK!0yhTj&)L3M?k zaQG0Qln^T<(S;kTE%oNLSscfv1#)8T$$%#AgE!(Nu21gxLw!~c5^&Bb&4>5Ip4m+$ zoI%MYPt7_XrARK0gYhW250&>+!tt7SCY^eQTfdC_QCd7ICgfZstwe?3{^aMcIYY

xFSM-gj7sORG{eONJ%vh~GoE8C@fa0*o8fn9{R+cB zkT^sb6LK$cL>?qQ=AZB?*T@;{Q&DdhjBojZoO{H`VH~VOynr=>iXA01rp@5Z8o z^Hy0*P!ZxG@e$gekW|?Jl(VJBMYD38@Hg)geZboi-6e8DwM%vHl!URz#0MTxhTnLk zR*3|RgO)HaBtE8c_@rOCt`)w!-f`xE#vpR*CK4`)CDOQ%6-G)JXy3Em`f~7>z=@M| zuZB~r78CRo-f{M2ymg^@B6E@VXsSqs_B-^Z4}DM>ko#aX)Kj{eniNu<{>3-7dId6m z;;7&mg&QI#d>n@ox~$LKZ7KPdq{Dc>3lbL*OG4v9{vnCH+WW}q7rXQ8I6DU>f(`W6 z0ir;TXph5HlM^=7c|W3?4%oNotV!K?8E*!OEs#KN9i%?;_Xr6*V9(z>>7!C3QnViV z)SnOs`khi_Yat`VvW@e1n{N27>N$|dxI068F%eFIz$w}AQX} zoo|MV4vzJ7@v+}2JM#nm6Q(GEqY#H5uV!`FSzLeVnyJc7vC;bf=g7arw>LkulVq+A zI4r`MM)+HHiIKxxQHsOaCwBg*w#ug07IRAWuRk>PBHR7Cd=iU6uBuMFdVsmYkrU&USdVwm?al z?)ov@noQ)+77*hgSR##5B{CV)hYaZ!nXUUQ%Kd$pcE@5L+XxuRsHCE%A4?lL(u^N2 zT))sQv|UXa?`p z1uF;v;Bf2HkOxVwjgmV$K4R$WX;iis#C8Sw%WITu z=#de(F#iFGf6U-fKCaSx^6UqGaee(r^G*&~*TgjlM@1=DV#)kLueE<>4N=97541C* z`!4hmP8b}?iZZmZ88Q|WUXi18{;F|-8F~YNaVStA{&JrGV^sTw?#$owR544pbFZ*` zT+;{Rz!N&Byw3aSP17Vh66b%M@>I0D;0fOc3;wa9ShlSizF&S>L*V?;=IrMdNze!3 zE=_0@b_U98umHwEi5Tw7{jH~ZVAZRg?atY6wU9lXI3F-q;RHV<*)=zI-8ki;zJ^a? z+4>?@A7t$j2^fbc1!4IG;7l_=~i3B{=C}sCARRT{n zN(5u?#%}{0kz;8)^=yZ6hmJuXaEGHrIC8&y9yUc2E%7JB!AwJX&?PW&YfQ9|cPqp&{oPd8-NIhsG!T{*fnh$~bBjtbW9 z3RZ+7srJ9T8^BS86a0`6l;~#X^2(wk29*~N1E(R zg?q$1l?c_as-Y_ggAdV+#d4z zpnO~Umk*JiFXEeo!;Z?2h(1i5z4#LPfTtRzQBjVg;2)>K8lO|`M;A(iy?gemtQCwi zg)<26iQ$BBNa|{AJ8jmDv&UTWF9ZnB-mZmvB0A@kvMX%xiEd*>CBLC^Z<=B1c_cT) zy86d1dPgzm^A*EeQEHxr9@IuxQz1cE7lam!{6jLTzeN6z-RFT(qU-rGRi2D{MF!y< z#!QPx6;AN;f6vE1<|8`x_0-pq)%1OmFQvan>Yjl<1PGl|zP%QWI@tG`E8B6bQY;D` z^Uz&PP@40U;2d*Y1dMd_WQjs2DOHD47ZW6-@F z?unG{&A}PrLN!|j#3=?r+ZJ$8F$zm zi_AFDj<=J7KHz+W6T%@GmGF&>h4|AodyiTQNwKtF#^z+l zwV~-%GzPMqLJn|LFb+J0Pt`<xnLl9t|;F{(d>#M0O@>W+&FvXhCH85R7{3ic_7UN z7&#nOI0a&Ac6p-u{c5IWqu4O9J+!|dAxbbs8ihD^-7h_JYDeDv&k8T2EzAg?3=wM= z&IhFxwVy3%!;Rq9qjQ0MH?xjxEQJJe`y)0AKC*kRZ+h*cUTRQsRLNof5BdoUBq;fy z+bL}=Jm#Pvomi>E7~x&M*as!zwoY1yN&{D!R!`;opX&RXAc0i<-$wNpACRErqf%*V znMRhMX7J40nB=CP&CrK5^ntu3k>BL9bv+)oUx&qU*!Mzw=IP3L~0ZxfQx2ZikZ zWmIV76h`$IA7~sDeEcQ5|CsZ?R0%pN3O@dFdqvhep@kD3B>UB0J~e;@rD_j6{<>YG z)^1Z#r5bmUML04FVzsLgI;VUN{$*$QhmXI!pF@IDUH#=#H8?6tam0U5SK;qGeBrxR z{-gf={xUEQ=wmzblLyjVB{DWBvZ{vfVT}s9e58dBezsJ2mKZpW%AcG_UvtBbdaRHR zPaR@!>?uSAd=wIPXrxiyMII#c@uZoh!J~k1TiRyC5s)`jA~_91@oRLGzoOSVf<<(e&Q@ zbp}P(gFWAQku^v30kccDjhuM;SoG_PqJAH@*@M5eyfoomw9pXv;G{qtyT3dsCkDs1 z*=)S|tu!NIN%VnyVnk?EzQ}`QKKkz@RPwTSJ4sB4V`A32rHij~qTguNtANv5oN#T7ke>^WJ zOesfFKPJ3j_M-YAB#Hn*`-R-cqsfWo!Gf=uTApw3u6}h?9ukB*57L-ekq5~PwtKX( zaD5E1O_w;Eep8BbFC2(ds!@f`xd$NR3&{yr`Z*8xwe+FAA~6BEIdbrmi9%;UEXyP(j#1-2NX;`p zeT<8hwCT8n&LCkgL>eRVrw>U-)l=met;Lv_!?XL4&CN;ngD?)bYMD~WiL+PEJ>i*{ z6)e8_xZHu|7g~t~OQbO&qa^hqro|}uOsi;|?N#qu1BpHE&<9~OND~T19whU@Avt%h zuq-l%X}JVl!qx?UNc0i`948w&5!o60GuiX&jYi|~!85x(lpzrc2sUGKA}eC1mqxSt z!%$asU539mvTG7oR2d+ay(T9Dlj()G6u+9!aOIrLYwCh;UWNAnLB~W+OxH;XJz4L{ zaxg4F#gpa|vfmPYAU9<~6G!$I(#U(ae73MT+ud#Wz%Qey<_LNYdJzE}+W~T7YIRI~ zdDJT#HDh^Kov}fE=%WV^92DZX6~^dek|8(yv76?A<%1OqkT?YhI>OzWWIjUl%RArr z@OB*+8EI-}-mwl6$eu!ITomTx_alkEu7h2-Pq}pM_^M-W3yF(F0LT0s`IW@SZ2#o3 zyyw2_wvU`E>11-#fdq0>CN!2R7ILF7cM=tNG| z7l!Hm3|@3}KCQ^kMw|~qR*}X^sCp6~wJGzsoi6sj*cq=e-{rcN1qs4Ajx z{qE=HK3)NdNI+0JIpe>5-yVERCUVDeYW**AcaiEQRwCRJnYNPqaLOu3uDjmQI%ig% z{q1}ynyWV8gDH-j@IB#i|G`sPlV-{EW^K3M`=F0bNNAB0SE_%jJc`8UynOh=uHRW| z8WQ&b!Gh$DH1eXT9{JV-f`{MSn6lvNj^c*|veOY73-Y-GDIwAQb$wf-^c?`I9b4 zWCCK#h@*N7 z2v#m~VtsC?$LUk`?XNPA{SNw3i0&mLfS{)^s+WCFodrXc82v8ZJ!{APW)t)QN5$}s z+(+&&!8Zm)4OF$UZ*Uh51t7Oo;;2S}4-P?cLbyPTMks0%%Z)QmJ9YOZZGprKKnUBC z6M}EQQ9ZlqZy*{eNynGVa|{x2KBx)%FUbtX^t{Oznz$Oc1+RQsa;lI960k}H8p#P$ z+3Vfyf1att*Hp0H_Zp9e#0SI@Y4l%_2Z@g(kA87fZ4E!Kb2LAArZ5t*TvR0vnZ_WjWd^>ylyAbh@nG(@js4(#9<;qD?uPh zB9G;)d4DcXZ*Aq4>}TGls_3W)CqL3?DXfp5n0?i{eQx$ zCCgTY)m+|p?xIn)B_yf3wxpQs!r6*2oy z)eUkV?t4V6s1!J+7{>b&@Z4}zZRXONn>kjaamXFvzdkr} z$q9M8)(DZ~UiqCn6C0?5(uwCsS(gK$$`ROsLUz-NHyycDcfDF>V`0oMTzNmi2SRKo z5;(4n$gd=$`mw|R$(`GgcPeK`CG~Z2E{h2z3bm_oa3=HYeSt|Qsf|@1AJ6(NCWHtz zOyZ-(GAy`Y?WN0czK(iIt933fCYBT4DoBW5mYk>Oe|zVBe;NGK$@>ekhx|7VDr#~< ziam4ip75t=r#|kL`fv9j^+Y5z5KE-dP{`G0^Ed17+;PfUrdO4|RmS4;VuJZQf{~0W zQR0PQqNT4e)fN|Oua{QYiwOo=a^gnq3sv4mi>-CXyDsZ`7hPIR&`>zlIN#QD`O~wA zi0{;+Zq^jjT}<$^ko$0pP!?HVvpL4;ZD~eW^hPA&|3)s#Oio+Gt)oV$*|640a zbzE#Q!AzlculS74TzGl+L+DkVdmNUv#M1z&E0|rKhvYsq)(@FHtMf8_u3FT?!Dfu? zh5wyFR%LQRWLUT4)4jd*;9zk z4tbJP2}|gXj>tnFhgFNIl9|_E6NkhxNHCBS=BG7Wn-y60TFp~m6YI8fUhIQ|0FwB4 zJ>2Yb@v>{EUc33W5>bz{i-~XwM41soR&c7P;E`*xx9&fCMLcnk8H90A^6`v&)ci+f zTt|CP>XFnR$6^-yp!CxvNUsLaOYpqLL^IsR&26PyPF3#0Sf+LbD`5}9G+D+fh+lT}LGc)wH${?XZmI#n)87vNXCKamXNGC-DI8IMYgl2)?Lxe_BXtGmJe-1$ zzhw6xRgzFRoN(N0;QFX6KcDMMJLH{!xX%AkyBf2a4>`1E1=bu%U3IGM<<-S;P+20w zBAJh4e0v(^qk1fkb^K`TZ9Jp`2_yrA7EXAO5Pzv%T77<9Q2|sqi;pv^5L?nfu+y>pbs)ApaBLgF} zSMkV0n)t6vkEIMOWnd`-OBqzTL<-$qDU%>;h~I{DteEDLsIHkHV=i zOgERkcfQNFh-i-Ih{5z4xvV2k=*o#YZg(xFf7m6hEIf;RyA)~o6~U;^NL_8XG~80ax=m_G2#(e+~q;XABI3x9{{6W><{&|Z}X`rb)YXBJ+&EMdP0 z?8k5r)4eY4Z19)*1njL%P@PpQ;NV!AKl0s1Ld!UT=|!W-=|^nMfSwhB>TLA#qTDN< z62AM0G-oWIY^IlvoopRMz7Irb`^qtUn~$tqTv`c0cM`{R=Njb)gSGE~o|c8_!S)<| zE7l?3oFue#J5*<9U1ibNvvnHi*@KwQd+W}Gjv?Wj;7GF;Lv_wITkSFx%_M+c{1Vgm zHbwDnyxI!%6YZE@wvJz)jr|VLL$N$?*1uV8*)@{~^vLI!{VgwB>t#&f``CC+161dF zn|1AFt2pxQIYK*Pj_TZpzo;NucXv|ES z{PSL_(nu}nH3s&j&6s`nz4Z_0TkC;db{p0C%=Jsy4k7<%A7Zm(NA>0UQJE`yx595O zMR#HPF{cwlIJ<4Y-rWk*6I0R$rhSp`RTJ7FEYJLcAw~xcU#tRp%nWMJ|Lv2|a;b@R zKzGCR75e&g`*&|KC+c{%Q>gulV>iTq@>9dV2cS|L)dd*VO^WSnuL1qoZO3%>e6KZ~ zgU^9(5QpiBK>~t;tjIUB2racA(@zTH|7@*>^&fBv)dl5rpC@HTA>Y^}vl~#``7n*MFq$;a_~L9Xy=*JaD~-K0^P&SF4oi$PxY&_z5q=yia*3`O(Qw zG*0Bi^@razZx7q+eefG*HZDldPa0ptA6C8>_;JDOg+J;@9rt31A;81#VSVYcvQsLi zU>_l*<9R5+w7Oy_s1VlSxD7}zpzvq4&@Vpfcc>{B8Rt_Va{HahgT*AkBXNBe+`}=a zgop1L;8*ZG5!|EqR>W-aUBLBldjunxS$6iAn*;9IjN%F=OqR0tG<*eo2VU<&3&NbW zcbmKeJn|0G3oRuVd#V`2{X!qlLm~F@)<*rzN}8U?5`*>IYN9@K3j z+XKuag@uUuwe>)siRXj3Ow+e38P8xov(6yBxNP7%#et2K?;=w#kHwfDS19TiY=U*h zVkhEjkLjeI?yZ6IKW7N@vW1!PZ|}l7cFY9xkGx-hiOsVG{!Qm1zK%KeBf%yV?wc7t zn6L2QxX{}U>*V2o`O3f2$wL>jfgh89xnTXiGf(Egx_20l*E$uEh^wt+@b3O{A?SQ2;8(aYZwukyF|Sac#)&LApOV56W7k{6T7cfr2bGp8ae{#b(#v~&&>76{hypz6GUk!G+iXP%oq+o*A+E4~vW?aB?PtJaR4|vY8hLcp zLsVu8EcrE zLDLht@Vr%Cs+8{|ln3__YurxdO z7Wld&I}z(^Ng*}~UZFsrjr+A(`sc5VV3qHHCvqeG=B;X@kC`;Vd?Vz(!~7p7p&Rqo z8v*?Z7sR)$JKvxtvpoRts6oWHF6jz?L}vK_c<4gJRhXRD2b12xadBY7{20Gky`g|E z&|m%&^LwS+M7?Ww0`B+>^ZG@qChc2Z0)7GSU)x@e9C~cH81$FOgX^kltFvkIPGLcs zp11`2SDg^8y?Q+j_UnKf^1po{pN5I}8W!MR_h0_wvTwhJ&ql!QVvt^KL;TarHZt%W zo`UxgwM4>}xNue%5O)P`zq*0vy^ZXv;On>q0pv&BXU`U^EaNA@zZD*Lb@HhR!Lq1f z;D@sx=`|Ll$o`Qo@fXnRC}S>cbco&QCp_n;;rXUPx_GUoovjz>)3dQYc7WaT#6Qx2 zd-z~p#O{|>;6DU-@&n9opB%0e^3w*~mlJW#*irTB`S)A_xAsI_>p)&w;pHayI(i@O zuU6-hbn#smtbzX60Mcs{8|9NOrSU;OEtv0CSSd{Eh5hoX$2{mul+Mr5IGUcwd>3(@ zD5ro$>Phf*hhHw{-JA{@uT>rc|2y~)*Cp9+TVAxr9B|hUn2+uMD`d1z68K+Mg1Fvo z(IStX$LfGSi-@?scKF9r<7Rq*pApC0nEScz5o=Iy3AslRH`sB@*)hux)x8s#+niFj zG$a=RKdE?sG2G%{Zak+i3-Azsq&IZc+Py*58J_FH95H_o!6_#k`w-|&n-Mo6zPdGX zwo40e%N3Z*nS8WS|0xc5rXA*cn1cI{#>fGF#Q^iL6TyF1PmBY8C=m0SmQ(A0SVaQv zhUcx(z$&q@#e}WU4?d?Ei^cyaR~IJIIFVfv`7uttdN;a8*&pchRWa}ERa7W2DFEDN z9P`g{-KKYo;l2}Lgt$rD$|I|}#FT;mrC4t=`LL+xgZc^J$9n|nP4(9(51%Vu4R{V- z@1}<%4S6m2;dxaXuWQq14-9(vE82iQ8m|kB`6Eu70*)jCo{0Nvv9b2B!~J!wfV)Pa zxE9;hhMdNZ!TtBZB<2yZYnyzeVcm1kLVO3g?Z@2HO&ZWY9+w?4J_=v!TlLp<)*!8Z<;4kp3Zx%Tgn-uijJj(A%LSYL#+tBBjQyC34pw)%tSpUBCJ zxGhWMz-Zx$Wq_~r#(Z-{LeW?ZtZ&BIm_MzkZw}s@2HJT@4RO1RYh9hr4e|n>)rfiT z3;8VqGVuMVOBXSptWf1rXsQPKs0zgG9hmN4w*F}Yc4Q=9aD2d#5%d)K0@X|+{rFH zJVER$+-D7O-kfT_1WZJWwt)7evts?7H2pw#hYi3#M>XQkmlPTd#2>0SD|qL^ zd6NaMquoh}yF1Sto-;Pb5a`|Tx^_=ZwENrH2DWoA&YSy~=$?j;9=b(0y9>-Y4fx^3m=C$n?K>_#33w8&OM8jL3x#uC z;5v#RBfXF8KjCiWQ5`^ksSWcpRhOKT_@ro@xC+++pR37-k z;5eGELw=Wk+Osm&ZCRSD(~!OwZ&UmubD+9<2Kgb?EEq8RMUDmfBMO+0e_gpmzOxVT z#3ID~t=KuwY(5Xx3y}-gfdEnW3cH$gcuqKt_l1B6gUc_vUco%;<9#;Zf_c@JdX4T};8o2KVlm9G;`=pA?WNwI z7X^A9T;KM;$#?3rCn^FSFN*ZR%%vxIwsI=~?&*&C$`JpRwWM!=pJcT3fVnOpz|H)%OubYZ_ z+2fEV7P%vUo7*8Cx|rkBmAVn|J_(UK5b*=bPaZC+ZXE}Dk{{;YE6%qX3xWNJ$Wn#* zspdUPNDj?Fe~1BAcD>+Oj;0qgv(*N7h^ z9WK7Elmf@aOd9h`w!@^7!DtXS2#?Fbn+py;TAmDFA2~h3`euzl^^-a99D67m@o<5V zL51KS2Z0|qyxzk}D@&QTyj%>pLon7CmJ&A@C1W-~CGf!PeqW?(i0vl*Dp zz-$I)GccQh*$n)DG9XJarhd>YK>b<9;AsT#jzM})Bj9WKB&4u9JV5d_f9vt_Yn0Dc zXisi{@7dF{sNH=-&gJH9Z}*0c(H>Y4U89~rE0=_W%99}t7~hV!%-44${*!MIiLcnG`)T4X!4 zdEQEOsjPhhWB4()@YqebnqfBTZELu1LNhBw7h1p=^jh^}hk9S}IUdg3d(IQ=8BvrR z(RvPJ(6eChq?s9F^s;TpA4*blhN`~d4Dyo zO08&WtLWuQ=hLn17W4V~*PgHJk*%%SJ~altxvNuJ6wJDTX?cBp#9ykUBY!{(IIrln zN>tb*XTwRiA+AcrUz@)*cTJ5!uT>AKh1{fm1SH$kM24p^FZ(goVgY3?PahAnShWR` zS_NFMPjaoV&(!9D7H|#Hi!n*|9a=7@pE&2`m34RCCpAI~I3DxEeviRm1YytV<*Io@ z>!+R#Ej_x|r1I7`uv0!Ip^YH?v}&Hs?=cuWR|&R?UUsXrlkab?UY?`Tk~i>G#qth} zF+*jr;Cc~q`Nd|HYrI*J_T%Z&(87t*8tRot`I$D`8T5RHb>4J4wJfXBMN#AAnef{R zYtyH;ir#oIh~YqO66K;-6GuOO;TaEc^^;Xvbxe|%at{~S+xwrtjBeX7fJt|GwqfA4v>XRT!u z$+lHVbo2JA(`oL|!kg-xK%keaJ8M1t39Vv|HYzVPG?_gYO|_txE8(GK1HxJMg+ex& zI`_GUDfbW3ay7$l5H#4cFd&}$!6x|>-zN%jlsg`17O=w6YZZf<=s@cR-4V>2KgRst zB0BwVdnLJqaskTSp#N8C5wTi*Knqha&_&I*I z?Rk5ktm~O~JlhQ4z!<0>rj-6n8*_2zgo1~Phmsd5IrwPP1@n{8pD! zDH5+6Fns*+Y5fP&EdK8^5spU$CB*a?40=ApDdX_BZC%kmMdH1pcN&9+qw=R_cX{aV zF&OMP4%Ft*pS$YYatwX?ly7~u$@|(bpFsIUpEl-jwntFSrsayk`D%u``-HFQ)Altk zjYB`z?PZl2qugCZi!sA)V9gvWR~mXRU?RA!Jaw;pHna$&HivT28IQD<5``{*m288{ z0kY1MKhfC-TF{$`^EAcnSHE+X58pyxBh$oR0}UOWG>J8!f?eJv)w3Bnj8#{FQ>D%5j2 zwVgo>)DJo%$RN8jj5&i@f?7o<27~=P9P~Vb`fJ*3XK)IK7WBqkQjpBr#~Sg^M>27^ zq%F1`W*1t}8*>JgVTKqC&eSu+VDRL)8Dcz^k&KQnSGNdaCgiY$R}o=9;JqkIsP__1 z%awoJuctdTPqSaYa)GsvnR|j}(J>CsCG+W2>a4CnyQ(um7G@iHE{Z%A0+j(<5Z3%2 zLy0ByxkpT|j_sp}wMlL4;Pl!t?n@my%9Tj@nbxYaZ%)OL=XWJ+XX&vm9QLW5+Ny=g zzgsLlweg1ZBT;`+rOF|;JHC{&C#7C>jKlS@@WAgDPn~YL#yp7XtYpe|f3#>R<-VN% zTFjx=#%Zl;-=wc+s8iGX==&daA+Jh5PqpBpoY1CQC~_uQdSxvGx2IYxQ2pKFpYXSHk87$4D~@Wgs^xC@8(P2-WTD<~ GB=JAV4*)rf1uFmm literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/8.14/fileHashes/resourceHashesCache.bin b/java-sdk/.gradle/8.14/fileHashes/resourceHashesCache.bin new file mode 100644 index 0000000000000000000000000000000000000000..cce253cf4591aa7ddf258685a7a3dc12bed5d662 GIT binary patch literal 25433 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 ‘&™â)PþM¾ylZÒU—2üZ¨.‘ðĜ¾ÌÞ'–Cî A7XxI¡`Š;{#ata¸#Ãî7œ“¸[VëH%b�ñâ:ÃôXºlT™ål&zN±"�=® ÃÖ;Œ•x7§‘—ab«H€‰N0+ TÒħò8ïûðbúћ�f*Ý]D1Ú°û_8ÒY®=òíQ@0O󌙙™­

je-UybNjwvY` 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 vb;=#EfczYV! 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 literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/8bd1a6aa-5b1c-42ac-b3f1-4cf982bf99a3/entry.bin b/java-sdk/.gradle/configuration-cache/8bd1a6aa-5b1c-42ac-b3f1-4cf982bf99a3/entry.bin new file mode 100644 index 0000000000000000000000000000000000000000..4b626a16e6485da7dc7482ab516eca73462a34ae GIT binary patch literal 100 zcmXqJ%rPk~%_uC$FiuJ@$;&n=$u2C(%q%KSHv7oPc!*Iyv^ce>SU)SVI6qH6Ej6*E mv?x_KE3qt5w>TwRw>-ZnyQC;JwHQ?@F((J81EPk3fdK$bZX`Yc literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/8bd1a6aa-5b1c-42ac-b3f1-4cf982bf99a3/projectfingerprint.bin b/java-sdk/.gradle/configuration-cache/8bd1a6aa-5b1c-42ac-b3f1-4cf982bf99a3/projectfingerprint.bin new file mode 100644 index 0000000000000000000000000000000000000000..7fe2b2b38bbd4e23e3f42f157ea2a4abc1b37e34 GIT binary patch literal 1132 zcmV-y1e5!a4~$fji<6{$%J{kiNAj1^`i~ zXjmw`ljkYp{rT$;x0zDxDP1=V9slFnm@QOwPyCC#|4PaXHj5dJJxK{aeyZ`ECa9(@ zQ9btglbb2-QnGP@mnT`pg;AFMeYHp2}I30?C z2PE}Krn{eV{>-lh|HM(b%NMcFB5*-i1LY-CMRmuA}nh8I3k4S_sQf`Txh$ zJw<6Z%zd95{!VaUOi9(^w)LL)-u_8V(U%PBp1-?Y#c{- zg;W=Eo*l;=J^s4FBB;xd5FZXftXZp{PBEFg=N>R_CQ^P4`K(|EI~p&&o`#k8`N)vh z(cKQ{HHkQ8+`ihri|z^``@Lv7e-I0oE$g1;cG5yPO^OWX1dh1rDIhCFmOy6!yo@E$ zZooN$8qJwJnwEyeZU|PLoNHb)W$weoj?-jNqoekPH_d7QFOqibVVyc;Y2hnCJ1SOvQc1%opIE_ zK(h4rM5jIz;k6IORuahP<@0 zbja}F9V4p~=hsE4ow!nrs-3PGW-Kq!tw2!Vl)1-jQ{n0rFwXUZ2@aJK;i96pXJy60 zWhsbf?9MU&j!Le;q<6DMP1+|*XZV;FmTtteb#}^I{ZaWi=rIxui`{m`xu}u|%L>57 z_0uaGiGik8QV15Pr?!y-8&ki#>GKuh^052$7YQ9LTx7cqd_82|fS#tZeBNn7dxDyZ zJ=j6$n4gkZR0!*^x!2n%=MWMr8@Rr$IJdLz`v2GNJaMhlx|k0cdHQ`>0|kG1KI2Ly zi8b!I7lL8XF^?xzw{=_WZnlZNIZcT|U>4pyXL=q8l`EZqbqGlj%II*;hg3-_@*u|w z&lu_LO|&&>M{@2gB;6W;m!zvc*ClAW4sCYxBMA~B_335hJjgHrn+|FlE+IaxV{)o3 z8@(Yv1e+BM(i#Hq*TaerV72f=F34jR5#CWAlKV(z`0`k@Q;AwIgJ-7i&a)d=x*y|A z6Y@H<#)k9=%d6&Oo3Ewm`E}p76VxktnQE0t07lbmkLTTLu?#cUKGus^DP6%4KT&x% z*Tu8ffo+}z2wh?MdNk_m8&<)+TaJ4gC8Fxm2lUsP#5gcj_L(gIC1ftio*p4$O^cJD z#(Lcpoo@fM^)v_{sGP8y!GkHm9=;VTJ}OKUgP?hCw=T(rv&v0ln1n?>k)cq`kMriW z{jaKx~g8#1SwwGD<+C|QSGA_j%Rh2IE>!H2P>6MQpz40(OqupJ7 zcb8K2DCv`(tn6qfT@8z)NVkW3{hQ4;zbYbdrxlktP*sgxc| zP#}os&r*ZT|oSxWKYtu z>n(BnPxhTeSc0csoLI&aA4#Jw0tk3s!o@zYF%-i=VOd&urqPvu9m$WT=wwdI!bQCS z$N=0R$aO^#O8CC5hpm9dxq~-RxPd`ce{4P9r?hNbdiDFLQ}JNtMGx`6$Rlw@zK4Sn z*RpI|IjM~iq_WS{ulbhq7;a#22faFQ$J=E z3byi?kECztqSk79Lo^j$fL literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/.globals.work.bin b/java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/.globals.work.bin new file mode 100644 index 0000000000000000000000000000000000000000..c32461f94301382ebde404b74b79d3a432031c0e GIT binary patch literal 1656 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

okpjHchSC;$93}zz6;47Vytc86( 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

^)9fhKTP)mm@=1t|r;F+rWxwx1=+p!onhS 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 Mh*mqJ%~5g*Ga3opLI3~& literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/_sdk.work.bin b/java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/_sdk.work.bin new file mode 100644 index 0000000000000000000000000000000000000000..cfa7c7999461c0ce43f1f67bb03b5c61ae067a9a GIT binary patch literal 12037 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 literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/buildfingerprint.bin b/java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/buildfingerprint.bin new file mode 100644 index 0000000000000000000000000000000000000000..ff65521cfca3daa51813a72e82c8fe297aa5854a GIT binary patch literal 19997 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 literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/entry.bin b/java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/entry.bin new file mode 100644 index 0000000000000000000000000000000000000000..83289473b015e783f4086eff2ecc0ae60116856f GIT binary patch literal 100 zcmXqJEG$e(sYuT?%dX1E%1ABGDmF?pGtW0GH~GoPc!*Iyv^ce>SU)SVI6qH6Ej6*E mv?x_KE3qt5w>TwRw>-ZnyQC;JwHQ?@F((J81EPk3fdK$8wIkC2 literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/projectfingerprint.bin b/java-sdk/.gradle/configuration-cache/b4eed3f2-24f0-41ec-a33e-3015b2d72835/projectfingerprint.bin new file mode 100644 index 0000000000000000000000000000000000000000..922935b244f13c62e5b3023f9bb4c8e1c48c0629 GIT binary patch literal 1132 zcmV-y1e5y>_DHxX&9NOOsNhpNMBF0^=aYjAO+%)5d8OM1tD!G3UtJf&6t$HEXDE&CY^lR2M{v(=BU9XXkB$R!X6+ z#<$=nd?b21e6O9g;j?l)<)6wwsBmv+lwb!K!?NB}v|Dj#lLMp2`C$^9OXS;wKS}TT zIqA?T7e&9Y zRVHOZy;Y1)^`xX5fL7wDi^B#M)xaxWU1nMt1eCb`c|0*Rwfmg+X=c7RwTSNnNxqX@ zr40?mYX?hU<8sLnmVI%R-3e)_X$R|7p}g6_tfh!ZPRn6YY!FESS76`J1jN=wGS~yn zmm*+8C=?`c-TcfjSvW-~dHm=Q72pT`$?6%J=o*-6tYy16=)Wtqym^L3*0A3p>rzQuye&JYBEI)c^f_!LslLs}`4*Ncb#x3mfP3%o{THmR>*va=t=yDZVxTO?ite6EZ&!ce2L?w#;j0&?(-i9C=YB zqT8HtfQpi4&K-q)P34r2EKR@yavni9G_w5M!GENiPHF7R#sC?S|bVS>P3saFrjsURwLb_fE>td;rk|XrUnlmus+h=z|)hE$8(C zqy7Kg?*d7CC=rXY+4FMrC*O|>z!UO>Dg?&1li8#HGOOiJn$!pzGd2Lr^J6~Lu@Z`* z0QvbV+Z%?qD%6N6N$|7kGxSTIhyAnwU(KboaeJscUq*-k^+Rx!sy{r_vnX?zuorhx z3k-JSAaqfhQtIZT;1Nog#|L9%+}y7QpX$$9m^&pxZUG2^o+Bv` y5Xu6AMRtBB92cnrBSOJ7*Qef|VcGhf z*}KFzqzyl4Bgda@f{y^w|&=Nkj$tLk?hpLmvmPrVc8Hppv68a zAX3T#&k<6=DKY3?xs0o%vP;FhxEe1Rvo49_W)4})f=p5p$udtr_)#wNkQ095x3r6y zo7Kxpd|U+!#dila9$(v~lGa|&Nqv!hcFCniG`M4$Re3j7drI*=Qs68%OCpWrZ;0Af zz}T2EO=pn86k<$RNmhZ1Y@nUpYh?E;W#er6fuE>#s3w$y$W&;3NgGEKQF_0&T!~I^ zgX%M+Nf)x{IKVDvRA9Uodr*ds&7aP0SLklKY~dGD{(z?jXgp;N`q#h$N<#n>;D?!-k%pm`!3E-C=T=D@mG>*7U>Y$0Yo@i)%_ zwy4QbCSpa`SnVRP>7jAu8N%O1)?EO+O0Y%tx(GjLcrf%ou~k9%!nGd&_S;I>ZA_j* zJ7E)OBL&k z5godWB%gf>+hih{+UJzujr*+4z93PS1m;zeEOEIvELp%O%IF?O;{CboF+R<=*Hp!! z%H7u`^4Bzkgi6%ttA5qqXI{m~o(j!+88ed5-eJQE?a2YZ>h%hNtJ+^nVS2+d-TKe> zy8LLaFvn@kkhBjFjSzD;cM5s}_}!PytBN`kQ0FJ5+=a+F&JR2s!^|ewlU=cMsbU)p z+=MC11mS(-LG@Np=dvE|1coSF)f30&QY|e3qCi~1(LB1ZMz6XLh{Q>-o$;*aiI-VG z(|Id!{`0dnYD>Twq-2^H-RaSr^e(n_;=l<^Ew^-sn((uPW~wT1B!Yim?2mfMJ!He` zqIg`U40Dsw2N4MNw677pj4*|BBqZxY-w#mOW-udJIcm)A$r!;H(4*9s&;C6Pwzt}6&IaI z54rwMq9U@CcL*<*>%+2Kqj>1+i8qHro@ITz&+3RFJurZ2{@E=-Wx04s#;=QABG$@n zc|z3O3ftJdahscqGl?41v<4wd)&c1Veo#ZBeQhW}n>dZ*tgS9%8q~`pj+BzU@bzC? z^9{(}HUrTN{?20v45$i^ym5L2j3n&>N_ajDQB=sOhd|v*#O`blq1yjC882wXgL~<0 zGY>u%|0|YKkG9szj{9|8Ucy+_y^7gxmu%Cmz#btQ5u$-3B_xsdKfm7sfbPKkBW=s! zYjY3Qv9NNlxFn0X)2it|_Spj5IaCsHX^C}pB#$Z|luknF7F`GGk*hRtwv4;;<9XgU zo42W%N$wE&Uhd{uzzjXwqZyosC|V31pS6Zzw!~(8c>@lt zQnZ{LLMXL`WY3E((ti#nREc5LlRx9#@>3Ca+d}x3{PO@5s5=W@ZiM>lyewhu6AFv* z|3gBkZd*4VR^da-R;-y!mlbhSOLegn$n^0}_0OHkBVLsR42RK4(O0aBG&H%MAS93j zrYa&3JIi2eOtZ`_KmESGZrM zABVzW<7o=^?zw=Wr)Z65e^i$ZkmpHgXYUoO@&+cshe>*W9dvX0yrxfU2-t5^q#DVz zuw+{GE4e7z6jFYSZ4Da8_F8~>Ygl$srmO)f5q8t1tXVh@w)uI;s-fJvV{d{zkFxfT zU_IZxkMQ@67H%SwU}TNMr#=oqY~D9rtde>8d@k@#z!ng*>NspA(yPZ1?Czw~av1Oz y$Y)Vcq|^L@bAR!uqgHVd-eRMO)xDLF`VgjAT+cf4rao?h7tczw*vBQoAO`8Vu%s&h literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/.globals.work.bin b/java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/.globals.work.bin new file mode 100644 index 0000000000000000000000000000000000000000..0d70197d409be5c810676972d23d9aacc8adc2ea GIT binary patch literal 1659 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 Uo+`!xIY`JJi1{eNfOUUcHK424CIA2c literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/build.work.bin b/java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/build.work.bin new file mode 100644 index 0000000000000..07e28d2449c88 --- /dev/null +++ b/java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/build.work.bin @@ -0,0 +1 @@ +~I1R®ØËmé µ®½¼[2Yƒ! \ No newline at end of file diff --git a/java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/buildfingerprint.bin b/java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/buildfingerprint.bin new file mode 100644 index 0000000000000000000000000000000000000000..9c8abbf5ce7829bab52746d9f56e795e68819d9f GIT binary patch literal 19997 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 snU(hFa%p_LsSGb%Ozp&0%a_moIl`G)Xs{jB1 literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/entry.bin b/java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/entry.bin new file mode 100644 index 0000000000000000000000000000000000000000..86d3b7e60888d1fbbc1c56dced0776d984229e7b GIT binary patch literal 100 zcmXqJG)>J&DJjmXOv+0#E6Gf+s46$ktjb9*DSXMuc!*Iyv^ce>SU)SVI6qH6Ej6*E mv?x_KE3qt5w>TwRw>-ZnyQC;JwHQ?@F((J81EPk3fdK$u1|)p| literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/projectfingerprint.bin b/java-sdk/.gradle/configuration-cache/b748b3a8-bb72-430f-8fc2-4be6689e0e1f/projectfingerprint.bin new file mode 100644 index 0000000000000000000000000000000000000000..5e3d5a20b6fc1bc7749f253ef7b0214734edfb20 GIT binary patch literal 1132 zcmV-y1e5!{(`oXU#oN{F!yzwndd|Y33rC#3HCUik!PpC5v$;1$5yLfS;oLJJ^qIK7 z;Iew#Fo>d_*B0$`{&7LYSgoIpt*J(9rsb|5!EfGH&P7YIiXz=Nd_AAt>lW>=0lB*Y zPSrAF2F!kZ8}HEyhZ!CUbJ4x@!cE(o%f8lZSHBiE4v;Lw&9)IPHE}dIAQc{TS5$4H zD}hefFYr9b9~%HycOWE~;vVa->gtWT;G*4tQ~%9R$o^{tCmS)b;OPw^yT2ZcVr#t49L>z zV9G@nblv9$YFG#eIpTc`-{0m<*!qlFu2&C)E>KM!sb;%YgXzH|y3vb>8H>KD?}y}( zcCf&^_jxm<*yW)2z5-M)-o)|2NaA%PN5>uH$ zC}(iFH~VEf&sSIwj?yJe=8cZA-~5su3(j&;&?2O|h1K=D)l;t|e>mY)IN&3{nsZ6u zE{?TVZ`>)#MjlG3l@1Ar{LVAKwIGH+yQ((egX=JdC4IH&zJJ0TtiM(GMjN$TOx%&_ z-)|Q)%z^Rn{W}g^LE3Z$*)lxERGQ6Ss@U?v!d?X1OtW(77;1x`pj{6Q04Uf)~F!%%b3A48rIvepaI z9=*)AM|g>68+Vf+ub-K2WhMG3$2Q7*xEzgC#sqz8sByIwf({FknVkji$Q2(|+?g%^ zIp!CUmUw&GwRTl+#}nQ99&jKsMff}SE-%X>dvEo)O<*CDi}wYjYDDxgoQiCNaG`>R z4cq5n;SS7)pLEv-yQG@bT0e1&5DU0zFMDs7+DoJsonDG zt}_)|D*{HxkHi9ftM*S|2_ZQAuPZP?S*C>!NR|%H8)( zF*&wNC%yNOF?4`#UGxU$4L(|{F#h_usBBa)B|y4jin+YC{zG)`z2;``yJf@@ z)*i)`=$Sh`rXG6XE@`)Tw|g7{j6s|_1yXpN7houR*`B^YgVf{SBg4%Rkh1=x4(D5n zW1t1?!q*{Pt=b0iX%KKJr-T3ztq^E~V27;St05TP=NEJ2z>jGv%YO$|G_>^u*o*j^ zgCUD=xl$xPcuvR@pn+84Lc}lR4<=84JimgkUF`HsUNrdP#e`clK6C}kOv%W=E4+W% zK`%j+{R&PUY3^Bph?w~;9 zu=a{Q07+LZ_awl{g9(djSx)*&eh2g?+Rrxzi^z|4UQ8%9{7$P_66h~`+|qgmvI*7KL5tyIeKJmC+}656lX6^4#NG4 zomI#BA%&NZA;WaMCE>W~?C^XOjD${_>G&VGedpYvrl2x!k~Iu>u~TE9sEvXpAT68xQ6XIc2$ znofd!I3LKmp+TNGus#*OH_j1HEyKIy@M{7UZE2|#2R@1|j+J4(m8Pkx_8!RZF zweZOt0j!km=I5?47?a8IwYtdbxgx@3KGzaO2D?6@HSEoFz_B1`(uzU#~-x zJ%BUSSp``(SLLd`^LW|Gka?8jU@*4<_m>2H1X@fj?TU~yInWh=NJO;}+#Z~5b(8Cli`u~K*$^lXM-m>Ppf1&Y zna@~SQ@oem5~#;M9`*x)%^2-2W-QiR6lHOSAD(c%9Tol0b*X6Y?oy1--^xkQcgUR? zDfVU(#|-s<3FSPq|0OpOS`WjLHJ5R!|G27KH1)3y{3QEdAbg|1yEK=1ZEFQJa#d?U z&yQJmnvg8u9(w~)UTDWeMTk@j)Ao5jXHP{ArIth2k_l8}71oaLaJ1Bw$DEW`Qb=Du zhM|gL7T#mh(+Cqfr%p*8W-fDZ3!P#$nt%l3zrb#WG2DoO@GPH`yui>4M?W z=6S3&qS3mDl|180~{Q^N*5zM1hegUt~kbk zY{h)qlbkA#!Y9wklO6F;Gzx)FTfc*;!6daWG01Dg_tm#}CMWi=)^ZpvYpg6+DyAxB zIq}DKTiyn3JSKCDgY;1<_eS zfB~Y>{&vvOe^uTeQxFcJV(0ZJ&_SL{hFY|63==i1P~%Bo*WjejMS%0X?+JD9J6lCs+4}^VJlDCK2dYT|zk)5O CW4BKL literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/configuration-cache.lock b/java-sdk/.gradle/configuration-cache/configuration-cache.lock new file mode 100644 index 0000000000000000000000000000000000000000..7af6976d6b21ba7cb3834d135827e6d64847e159 GIT binary patch literal 17 TcmZRc&C|5^(j|sJ3{U_7Hx~qo literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/.globals.work.bin b/java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/.globals.work.bin new file mode 100644 index 0000000000000000000000000000000000000000..f962b988ecfb092df90a464bd6abb8c52e0148b7 GIT binary patch literal 1656 zcmV-;28a2gcY{*H4=6<>!k_A^#iC=XcFQ?U3Gd-UO!X#g#CmRlY*X}4=pRe6jhnhz zyZ{l@`?Oa+a&7T5rZ^iZtHKDHYzipSq5r8xgyw#^ZI!#`fGtCh-=Ue znow#Hn^g+xd#6oM+QwC55@P=|D`A`~wS~?Bk$eGmmFIrRK;c8`JDTxAe$yw_^3m}vnT*aGR7D)|nH;z3)( zY5p0dD3o9VHizmB(~B75m{51qT{F$it9_~QQvKH98he?F#b z)j|>%5`*YTvh`?3{5|da0p2XxX>;4gGL+XajYL#{+{UURzka(KK3B|5Wy{tmPu zckxSZ)7h#~@jkZ2+vh`*=`z!mQ^XnqPTY&PrO4c&HKouBNI8J%Xdx|B&Wp;NYg#`D z=+V)4+|wd<0S9R=xWxb+kPRv0q;q3827cXsMb-^zrO!J1BxAk*@hB@R)j!%N1!pz4 znFut<#~VVsK&{1MZCX+l|2+hif`GU$27IwHDk~$2y74n(ATIF8~o6dYu9;6W+3yX;agSuDG3OE-M{!a}vrc%}} z!G50&p=c!`fhX{xjoqhF`x3M~y;8y$EkL$$Dle7Z(t-mVk}Cv z&wv%IATbzvW8H^0W32T0?a?kv>g!4%PcT3#{b{39I#%JqWVr&xvavy56EsE6dvvNQ z0xOQz>A5cuVeKLtLr#v5<5Ts*X`Y4~T$c4#eXgJ=S5O#a*Tsu{CVCzbOlbECO8Z^= z?xxm<*jvfR{1J~??;8H(nY706Hx8drhJwE{hUuz9M02HyG5h*Dua8vZfv8$FiN(qg z0&8J_iLu6bOfjcy-S&^(Jc3xIV%u(EC51_X$+|w8wLNnj*c24Pj)dx*LFmg|&8bmf C94y=b literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/.strings.work.bin b/java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/.strings.work.bin new file mode 100644 index 0000000000000000000000000000000000000000..7e2cf9f2f69cc4cd5e32dbda6d760124f058e4a4 GIT binary patch literal 83718 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

g7xUTF`2ZT+jX8L1op5!ZdN1_BP# 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 literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/buildfingerprint.bin b/java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/buildfingerprint.bin new file mode 100644 index 0000000000000000000000000000000000000000..20fa56673da17f019d311ab1a681b6b96f9d46ab GIT binary patch literal 25023 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 literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/entry.bin b/java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/entry.bin new file mode 100644 index 0000000000000000000000000000000000000000..9d5699fadc021e0f396abadcad4e626be9c48d91 GIT binary patch literal 100 zcmXqJG%YU9OwTdN%Pz}EuF6d|&B;y6Evhumt$fMIc!*Iyv^ce>SU)SVI6qH6Ej6*E mv?x_KE3qt5w>TwRw>-ZnyQC;JwHQ?@F((J81EPk3fdK$a8zepe literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/projectfingerprint.bin b/java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/projectfingerprint.bin new file mode 100644 index 0000000000000000000000000000000000000000..7ba5057ea66108b5b4ec4210eaef4bb73572a3f8 GIT binary patch literal 1132 zcmV-y1e5!Dx2nUf;?_l;l85_lgNug4U(B>molvKmVnGPbrgHMzp;Pf$XsRYaFtunN zB^#H#yT(K>$xNTEF#rtrKDkp$tz^NA5af%H@6R?8XaWTXUz{z+V#elBT?AZQy*_68 z;qs(d>76+AFu$)66yY+Kg-qZSlpMSJQ0x@XqH|*^3IXg4&Rlxjq<4Z&B<4#w1DHg7 zk#8!+RtmeB!cC>d&UQ3%%uQ$-_<;r=_$tN+_m8Iv%{y~|$Kg4sx=!oT$hf%lWy58Edfsqw>q)gfUF9@ zbb~~{+?g$_zj5!Y>`=$d4nLJa6d^AR#lzUJK7V~qLgY>?_*=3ejet!5tszTOjpMygo?8} zLqHk+fkGrGnrx#6e?_QY27&kkTJhz`0!>_%v#>H<@;jsYlxvIJ1^$DDR!8h3-qglS z9!1=d76O%p=~(P&wen4QF~8Bpd(3LrRwG6v>6Af)VY#a(%rSBP_Tp#(IPMPu7>pgI zYQMtFQ+VucRapkQF>4G}B}4+4qwx;heYsCWT@`AYJiHIX-Rq}60;)8dI9$d;7`=tt zHKRnEGc0o7fRtXDm0t~KbiTc$=}CK)|h!Q%pREdqmSnQuI~Jubl`^X6syS$>#m z`Mf9FY-w1XHs?FS(4Y1-^Yvio+ROzzOMuPxAfMd*>!Z3o5H{jlynemI9g zoyKhn0j6JbG0c3E(hiLv&Na{iI;O=OQ5Rx<=-_XOSDqCF%IcDoG}EyvuML9jnPqHK zKtBcKkmYIn$af*VsZ~fIX?0W1S7$|^2&dMvNcS3U+9o871McsZ%ECr8J9Lw>3m3TC zH?5phnY5^{L}{&7aXk$H_2XS!!ZkL-PT&vube&9XoKAAD^n>;n+rbyPfF}e=3p=%^g&3x{2BlGnHY=5u(|g4xj`o9(dJH&<>xKVr8o8jQua>KZGeL~^ap4b_pY1uY_CyVUwM zn56Nuhn;jdh+1$uV`dyCr{6VTceph-gIuM9&YB7zmfu-b>}#D>Fg> literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/work.bin b/java-sdk/.gradle/configuration-cache/ec822442-c7b6-4940-8dcd-f1d4ce58c109/work.bin new file mode 100644 index 0000000000000000000000000000000000000000..f860003426b02ba1c9c9ef39adeaaf7a538400b6 GIT binary patch literal 2433 zcmV-{34ZomC+w^A0wv3iV-t>3SPJ@&o?%W%ssy&UM{PQgb9kvIBC9?A4#VsgJc4x< z%a?Jf9%~*~Na zQW;?xTsDWLbv-vEAUYBQKmI4+ZgrPQW@c2DNplw%Vvg*LY%DiZn>*44_KZ?|W?i6& zk3xS~R3Rgs(brYOw;@}RQ8o(#xM6JQ8%={<{YRAOpalFK`>9S?^OA6E(W1cqAk)Jq zGG=29#&k;LmIQ^+Bma{FtP6F3e)!8h%+v|pzA&Hmn5xTkp#G3m7HA9D?A#UMA!zCN z*^$*@>h~aPrkFU;ni98Yrp-f3PutqVSzmAllmTHhqV80FB8DmiQX~p&Co`?C*VgK^k%i;&g+4krvm>? z1q5k=^MbV1w`m*JLI@$4zU+M^V6P)D?=R~(lU%?E&0MeL35PrgA0^QKMdB##sB^Ev zKL-M-bnogTH5n~So5w*u^6epWX~VF0vF z*YNI^8fh5u$Gean=tfYiw9om!#|yUXOcqPBW~%xzRUI(`il<0@@3$aTG!2eKn3Zt3 z`Z>Y(9If&5NXQ!(s{t)BKt{kQ{Lx+`qe|UiLuzx*!_89lO=p)~dT$EFT&A@Ay}g>5 zK;}N&Ju>ca@EikeB>kW#$iqPNPE$*QGg#y+8QTvy!G0ZB+NKRY(lh?O+`=(K;>5z( z=`Evj0Jm{F%c0#`AbC&2YwH2eW%yYV>VbWN{g@5ptOG2bg0)kF#3c0p>b_SmfXVlcO+Ia*I zR8a91LV;`WZI4BQkKzVvGq1wRwD;VFrGsr&ZK9@~ff}EEj#^@iZo`%fi6c#ApscJF z(JFq5JTTE>)#G%BTZRWH3d6G?7j3DJqg<8sVeNOpP2u+xYlzR90I51Z*L`bRXfMnV zLNj(Iosq9Aw#Hy48Zl3{LnTjgy7fhG4g!j+t@dFTAE`WFN+Cgqg|R3j^aa)daL|?(F^+#x4&FBSnD_@o=~DB3c=W9kYZJ zkNS_dE`F<7O1cwY++<*XWB6XCR)mwBA}Lsik3eH4{~g1JqFaWH<$&c02{S$p59}e{ zxB_zvCp|PA0GiNl-@#L>aY=uK__IyiG!NBYkcHA`F3bTYx?-+asU|-fEJzr3&BlN! z#fOwRD*+};&y=}i209ll*55d=m>XsRcqU{orH1|vD=4PCB&Vv)OQsF?vc^yKYBpG4 zqiZ`1x-c)04D~qB)Wz(PdWw6IKY?t{Of5#g_dreb52J6GP`UHQMkMhk`iT2n*t_1w~nk>67+JD#g@ zskEccp?Bo$Sv$VKFOfW5aZ;;6+5 zt!TY2{x(7)H8gI0AVTfmvWgCQ^v`JomfJI=KFOo@k&dH)Pg9CJkErX_ogGqySbkhL z`g!0NJPb{lZaI`M*ompZtioj9WV^yvk^_Q<8pTG6rzmB7PAY@BHXRx9o zeIz?bO`1Fc;z&Q2pRg$17FA}p%>YSi5fuZ6YMMnoq&vob=!`+hbGXa?#L4p^H~W9A zgU`%)T`@p~KG*APJ2OOiV=3Bh_6M)artQb%DycE`2RteAIq>AJAGC1;%9z?*Mn=Da zH!gRx^)MY2qxiYS@YXAOB_n#zgNVXTh%jH2U;i8P;zzXox6KALyt!vy9XV2M^aJUz zj)B0YSK8-evK`Y-nk+z^xPMEGk#2yXBUrMKF$-jcyD>0_uKW3jSJY2;f5Hpa)szmSAIeHMNEA^b za%*Ax#vxjB=G4(HDe7RqokKg>XzMB=f)WS(S_BYSxJYB2_!q(R9eIzf)r~Nl-nvkEt26(eghP-nQQr<~Bf8&{% literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/f24qya0scm6443semhj8oscit/candidates.bin b/java-sdk/.gradle/configuration-cache/f24qya0scm6443semhj8oscit/candidates.bin new file mode 100644 index 0000000000000..4e34d5dc7c5a0 --- /dev/null +++ b/java-sdk/.gradle/configuration-cache/f24qya0scm6443semhj8oscit/candidates.bin @@ -0,0 +1 @@ +fdc8c704-e698-47ae-a583-b9b4ac77683± \ No newline at end of file diff --git a/java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/.globals.work.bin b/java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/.globals.work.bin new file mode 100644 index 0000000000000000000000000000000000000000..7f63bc7ff61f168c8aba19c508e4c0fc77c85705 GIT binary patch literal 1658 zcmV-=28H=UN7vHluxyemIHKTkGE#+v&Mj{E$lXQoANU@qIq+Nuo!n_alVi^8j$D~H_s(!8LMOr| zxk}vGW#U3vqPQl}r_$gPtJcNvXU5$z3Szk7EGOzzw6#`AsNYe1DVWf~9Scgp4XQt_ zV&`Qb=KqEEuj+B(Kvv(u9x#LOX9n}J0c2vYtL1apo-6AB&%Dopwnn}N(KLQ5oDb8? zI>A^*zFu#v#l$&cS8x^EnX9%RT)OslU1qJrK4!ZJ_6sZi>PAim5TJ|CL9t4(j(*E} zUedMAs0`L)sBO8XDR;^tU`GtfT_D=KS4e7DeJln0yY(gP{%!4Q0W0aWysX}mZ8t-e z)_J<#;(i2GohAdf%2numkLn({)=jULK=C)TrcU&yB$ZX0A?shgGdUO^H>?jEO9x~44!`+S1c=@}V-Ka&=smGEhY9Wt!{(sNZ~q3S){4OxwPY6Y8XmcBY_iA$R>~lehFw;<;36|8R(3OlGCBk8yPjn%>a9p95J*aaewi7(=Em6A zQh&izP5Ej!PZnS7fT~9td zG2RTdRwArpshsE~C4vIUtBC9uSBDWLvmuXcP}8y1Se3npO*YC=N7dgWjzYk?WY*9_ zm=NBc9uTjyFgpYd7z*UFJBRVsz87yLgTuGc!!-MV;@i6tMK!0Q4jvi#Pw4e5*lwxD z)`Fa+_c+a3xC9eiRZGj5WADu)Q=Zi@bElJZqPF#QN28h`xQ5KJj(F)=j_x^ZSZ3Qy z74zdieEJzm=U)|#vbIn6KcpgPn@bHr+-s&PaLz0U-dWr}3(+=`?K?hh!ykKB-VYgs z30BdVyLiuY2of2PE;egqXqg_OIzm#1I#6S71}S4Eq8Ng2F(D#=CRBFi%-$FHV*&B( z@n?%b4>js)cmdCBCOwK_;(hfnU%Yr@=s04jfPl__b5b~CSTwLf?anxz)kBCni>bv% z;t)ha>kQd|QMdTDb=`uz`RGOkrcv0Fg`z0N4|_|)Wzgcs3RTCgr1 zF7bee(HVGEd|R!z)+O**Oya-}I@fLV4N$0GO)68-}@$l}RsxWpvg8%8JIo ze-P}lFMX5ab;e6%LBVqi+F093==8%L`ch4Ig|rxZ(uGWWSyqK$D)cT=`>lcsx@xCs z4EV+lTDpSnJBIy9Y2P}-VVBiZYZXs#xxDnc{Fz6w!C+MHB{VMB EWlqvKu>b%7 literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/.strings.work.bin b/java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/.strings.work.bin new file mode 100644 index 0000000000000000000000000000000000000000..0bd6b10bed21d47f738dbcf3e3b0a863a8c93e55 GIT binary patch literal 72970 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

c{wmOv>2pu+LDqmiIt{(~&5 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 literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/buildfingerprint.bin b/java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/buildfingerprint.bin new file mode 100644 index 0000000000000000000000000000000000000000..cf08b97ed689098378298d68faee52403473e18a GIT binary patch literal 19997 zcmV(tKiO`s0`d@+DP)S|KGhoCgDP~`Sn)p zwzOW7BVT5H6Dp*?u(0=rTq`DQeK`WKGUH(LY#-%kIB|;ur^<*-vGsz%M<4XDl9@5# zR@9S*%49BaM$_P+dqSE->XK_QOquBSeK|3*LrNh=inhUhIY!!5Fs@ML&SLp7qVbh1 zem#bdK7hDuq+$&?bYbLz6@YRd0##XPT zZ-Khexu&nOM*RX5ct-1P0Jv!S!fZny+M-US`t&?qPTR9B3}>1X1o7OTx(`E(t zHZRrKv!U$8+zn|APId?5 zH7ar9OKq@$;{Hi)#7nr72l;2at)7V~49WbsZ{WUuq6PN%`bPMeNSx0n?oV9EP%M?3 zRMeni53uLAeoV}S1go!^jqCms~-nciDZmTiVG1>AQuMEsJN|%}|o&V}F_EbCw zjkLMt4%7XFHGlR9z2utmRV*}5x`9QS}koTNUo|Ez(oGOnJCV2Q~i!mvL+=4rhVp|#JK=QaL{Y6T80@8QJ> z%#b*DR?_Q*-wC5Y;F9g^LP7v`22c;DQbLdw^DHJF8OX^EBD|8*AG7zzWg}O+M@dA5 z*k>FuH0OvsHokyOi%fhW-;r_^`EJDx=w#Hv6L4%;+Xz1p z(5G5%+WQ+!2Va$(e5aAW4W2{X38?*#Ss-BYxr&AlcF#Kl{)^!p$;Xt;%(Bp#_i$E+ai#!n4V$x>_b15LHC2Yu|`2p7$Z&Kc25L#UrzVt(Tk5`?=hTeHlR17%faE?45}xE-4k7 zFH?r&u8ELKEoxJqcY-@;*7vJ3;W(;taj*WqfK4;*aFc`9)D zIAsqN9Le4KLEXNZRt(2f-8vX}!`-SwH5XujR{N6d_{mKcz4Nv9#&yxnxF|e0*0vsx zpq#aJu3%6IQyZ3r5KgU=g#m|OLw1}Bn5~?3Xs&X0xv1=Z;|FWH+IVHdp;`|ba5u=i zyoi3m#+L3CeytiwCGBOfU_Em=6)uAh2=4*PC|c$OVPyxAOTs_gtuZr~BYNtG%;K{< zGluqieauCLqT{VdCa)|Q1AgX=4WfY^!94=h z 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 literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/entry.bin b/java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/entry.bin new file mode 100644 index 0000000000000000000000000000000000000000..3c26d34eec28ebd27aa0f1eae668417ae06b2255 GIT binary patch literal 100 zcmXqJEXyr1swy)xDNRbvHqA~mPsz(l%}>lK$oSU)SVI6qH6Ej6*E mv?x_KE3qt5w>TwRw>-ZnyQC;JwHQ?@F((J81EPk3fdK$9+9UA* literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/projectfingerprint.bin b/java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/projectfingerprint.bin new file mode 100644 index 0000000000000000000000000000000000000000..68ea84de088cdeb80c1d426686b910ed31df9531 GIT binary patch literal 1132 zcmV-y1e5#Gh2pKQ9%sZ*N$i`RKrK68&M2y2uK6UJTmOIz-C?{@vB&Q<9QH#y^pv-! z8bg1}#ub6zJnHe)`8y@A-MS4uGcU&i&&G_@^X(U2(~n0BLI)`fl5avY-BquX3Muj^~h? z=v7dk#o(&u@bpi`7j3zth6#lRE7A)BZxqQUD`|EU5QH8BLz_)xnkVB~d^-(cURqC% z)*!b2eD1tE%9wGISO$X-Hbv~jdSty>9P|@1GftYJt{t5NbX*IrZ@UZ8j%S9*Bp3gu z7b#SF?srpAz~ianU~2uqR9Y)xRHs#oA4MPH@aAO@VT|KJ9+qt6BPq&!`y`098R5*T zp79iW9w*UZ-iqhZ@1E*MyL;Ob3iUE2QcslK6P6JqjDqQdL>c}b3?0YZifOKFGt@lY z8ubI3f4a?jsX6N^>{p4X{U<<)@i7 zp+D(nctb+y&k<{O%(^hVjY&wLEBjjN20ZgAC-jJC z9o5)fekZX)75+0p$^fH_i=a!65pEM-eZC3$e7iYLy-iQwLLNWD9s>MQq3I<@UwYZ2 zzEe{K^lC+3AyG$bXrIpg;f?*?Fdt+mTl4(hSuA1fbiSepxqEu2;~r`w=O2579z9p}F7aVXnV$ z$+}QdeF+H${;RpR_^sQjK#VIvb#R9>#hfcq)o5LRP}P{#1~QnvCd!`+{k6hD0s^e1 ze}SxBIm!zV2;<(r`aObgh`taiVNuW(%bzg1%EKgVT4Tq1#aU z&1t7WR$2qJgKfG9yU?XzYZNd?rs(moR$hGjJTZDMA~Xm!;jsDDl9}D_1l$_CP1>ra zFD-^tXSt9r;7i`qB01q_SoYJnh;a7c`rgcTH*cY)C|H1T|TbE_@I}82OVM7aGXe$6l?Tli1BCUAmTsc zy0x*eH948yDfJkU=W6W3RB`~6yavLha;D{SShEm7`eE%f-I%&{tsPQYl3%0N{^sDT z#yv3Uwms)iRNU^v$S$Jp4<&_Q9fHlvpSh@}@DO(Mi#fXYl^duPV`7h*?1S#pZHym~ zB<0W2G!OH*EUpz1P&yuDb`8H1adDq~1b(MzorM8s*}Uxp7M-?{c!NY*TqKr!_Van2 y)~3VC4zZib*C9BiQ%pkhLba0=G|pv|d7-CjN}>WZr<9bf^;F0pA>6-VG%dx4DmbkG literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/work.bin b/java-sdk/.gradle/configuration-cache/fdc8c704-e698-47ae-a583-b9b4ac776831/work.bin new file mode 100644 index 0000000000000000000000000000000000000000..e32dac8d78c2e454f42a9991d737865255a31941 GIT binary patch literal 1864 zcmV-O2e%F~wdxs+f2Bl_NSpg&yjPCa8%XzZEKfzSv-`Hd0 z=MavbK&?n-FWuoq%$bpqxihNnz0!{tD4(M{vBkw@GiOJLUlp){x`mm~W=V1Yh1>nZ zwC1|r(yXNsMrd$Rq}hERL|VImIV&uhSiQHsWhWxoKC?XBl*Y6EY*F-lK}qDyD-BmU zQ0O75NZ+B737ffiC22@;45(2c*!B8xbS4q2t$&VN_vkwQPpo6N)ypoS_* zN!ZQ?^7hfcBut9$hxQ_m(@DJMm*lgpJP=KCOm-|@$)u(Dq`z?eWCT%C+;S`rvS96* zvmyv}XB!7GI_6op28n{evrO^t5`a$tIG0ueq9_&AFjEHc1gk-zUv2X4P;D@+Fv2#| z5ErhZ6~nbq_NyDRwPwPb@)UWV_561s8QFc;=u~o#_Y8)=-1uevSKd>M@mN~rINYp? zfbgY(-bv1Qz<;t^Q+jf<)f0lzOM4Tpc{lD zCUQBs@K>GynZX)uACF{+Es!|R=z?~0@sTvdKpa?^r126P)Uv0KPS`jNUdenC+!21T zj#i8W|hY~$4!eF}`7a@3l^4C>USprSzImf;Z3&*1^!6uh;Pyw&~@kKun; zg#S0j8L{o%mNu|@>W^(P8xTYny^0no!0o6_a`5my3M3Y7i6tpx3G%$~DsKYk9|DIi zZmk`?0sF_!KEw7x?X!b__J|kreKL{QzNi`ll&mWN2N>OHQFC9(z+FxUz7YS5H!@eif=IVn6PM=B3Nn0^XZ*M1M9ID zqHsp*RCs|Sm|c#eu8;m|?LnD<9ViOiEl9H|dU6B$R{s;{yu5j565OcQ4t=Qt$}4Wr zL{o#9KuxEX1)IbkY&uy0EL9yOzv9^fM&G-wEVAo~(G||^xPx%MD;$@01!c8C>%~4& zrrzugvS+aIf1oeI16g)OYQX)9RBhJj^nWTJ!oyEe*IqXwhw#f2p@uU&47J742Vg*4 zkNQgn8-}*1oNXW(F%xrLg|`GyIHjHdru&TOBtKU@3b@;8V}sd}=X|hTp&>V4fP?LT zBXNF1T6jNB`ioYL-=lbQ(K(0YNV-?*~OQIoCQabl^`e2TMXi`Q?;MSjcGC`DSWhc~ zMn(&zsP1KJRu0P*CpUfwiDrQblAut8^V>Htw_n91ajL5FPKiuwS}*zkC6hK6brXbw zX(6DA!9WVSSoqDyN(bPNGh(3)Sw2j%(zZI|l^KDh;T>PnM#c24c#WwAklXpGT5h7( zV4nITGI!KH*M#wpn`qWiy4#m1Bf#7F%WjTVWu_49<*n@-t_nO6OIvXke?(b5*?uDq z?Tg&n%tXQ=iBqmv-V+UlZZn-oheAoyiFhp(Sm}Dqh60f-A*MQd7KegE%m6t*!|1z8 z4`rJK&w3oEudEcxxh;X3r{~v4nWne1*#+{cZF&S`pKE>3KWVFn#hbi zD+O+tnn2FM>0x1QsKFk^e~5d?kMo_LevpY8Eo>Ru6`@8xTdz+Is00#HeW(`D6LWZ> z9Z;i@q5gNaW_LjX45mjhQvkJN@0JC@?&NX7I+?f2495-QbtC98zWcu2IYon!I20a( zKp6iZ2~monff)M~crh=4Yx0({eNRV4YUi6Bum<(zP#yOyPwTf{!`rCFl@`fAc@(hm za)HAV3|I$M?ODi^QPrtt&_K@)*OB=vTbQU59G4(g38~mkhB73!YapK`5!ecGZ8`!s zJ$d$9xq(nvk-XKWzVyY^ta7j+2-0@D3V^shCfu|16mf<+jd{NWEu7pPjG-_bm?znG zyFv9HpCL%dTHMCxSOFFrLiMzgyXH&0+oyqWAr{ZS+<~e;>J(ejR#$^S+y+^+=`YOR zJ;YSPE}r}kS46#8<6SXp%4Y%vTVYnUh)uMh$*dSt*MLC2RHL>F^}g(_8T|)$OpcOQ C364kr literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/configuration-cache/gc.properties b/java-sdk/.gradle/configuration-cache/gc.properties new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/java-sdk/.gradle/configuration-cache/wtwn4tdzghcdqk9l8l9bc9zu/candidates.bin b/java-sdk/.gradle/configuration-cache/wtwn4tdzghcdqk9l8l9bc9zu/candidates.bin new file mode 100644 index 0000000000000..02f3dac2f812e --- /dev/null +++ b/java-sdk/.gradle/configuration-cache/wtwn4tdzghcdqk9l8l9bc9zu/candidates.bin @@ -0,0 +1 @@ +b748b3a8-bb72-430f-8fc2-4be6689e0e1æ \ No newline at end of file diff --git a/java-sdk/.gradle/file-system.probe b/java-sdk/.gradle/file-system.probe new file mode 100644 index 0000000000000000000000000000000000000000..ffc7a046d0b1e9ba7e31125dc1748de352aeae4d GIT binary patch literal 8 PcmZQzV4S<+#H3CD3B&^` literal 0 HcmV?d00001 diff --git a/java-sdk/.gradle/vcs-1/gc.properties b/java-sdk/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000000000..e69de29bb2d1d 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) From ac21b39fd8ccf0423d71fe98c10132c816a998ba Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 17 Apr 2026 13:10:53 +0800 Subject: [PATCH 64/80] Fix the 'Pure Java Dag' disappear in metadata DB issue --- .../java/src/airflow/providers/languages/java/coordinator.py | 1 + 1 file changed, 1 insertion(+) 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: From 0891041e02c677f76e042b06dd7ba0f564384432 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 17 Apr 2026 15:31:09 +0800 Subject: [PATCH 65/80] 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/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 ++--- .../sdk/execution_time/task_coordinator.py | 20 ---------- .../airflow/sdk/providers_manager_runtime.py | 38 +++++++++++-------- .../test_providers_manager_runtime.py | 20 ++++++---- 12 files changed, 91 insertions(+), 87 deletions(-) delete mode 100644 task-sdk/src/airflow/sdk/execution_time/task_coordinator.py 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/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/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 8462e1c9fbab37af9d1c1585bad64ca3ebffdc3b Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Fri, 17 Apr 2026 13:27:43 +0800 Subject: [PATCH 66/80] Initial Java-SDK --- java-sdk/.editorconfig | 13 + java-sdk/.gitattributes | 11 + java-sdk/.gitignore | 56 +++ java-sdk/README.md | 74 ++++ java-sdk/build.gradle.kts | 33 ++ java-sdk/dags/stub_dag.py | 51 +++ java-sdk/example/build.gradle.kts | 55 +++ .../apache/airflow/example/JavaExample.java | 69 +++ java-sdk/gradle.properties | 23 + java-sdk/gradle/libs.versions.toml | 19 + java-sdk/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45633 bytes .../gradle/wrapper/gradle-wrapper.properties | 23 + java-sdk/gradlew | 248 +++++++++++ java-sdk/gradlew.bat | 93 +++++ java-sdk/sdk/build.gradle.kts | 125 ++++++ .../kotlin/org/apache/airflow/sdk/Bundle.kt | 18 + .../org/apache/airflow/sdk/BundleInspector.kt | 40 ++ .../org/apache/airflow/sdk/BundleScanner.kt | 117 ++++++ .../kotlin/org/apache/airflow/sdk/Client.kt | 59 +++ .../kotlin/org/apache/airflow/sdk/Config.kt | 136 ++++++ .../org/apache/airflow/sdk/Connection.kt | 12 + .../main/kotlin/org/apache/airflow/sdk/Dag.kt | 86 ++++ .../org/apache/airflow/sdk/DagBundle.kt | 12 + .../kotlin/org/apache/airflow/sdk/Server.kt | 79 ++++ .../kotlin/org/apache/airflow/sdk/Task.kt | 8 + .../apache/airflow/sdk/execution/Client.kt | 143 +++++++ .../org/apache/airflow/sdk/execution/Comms.kt | 240 +++++++++++ .../apache/airflow/sdk/execution/DagParser.kt | 17 + .../apache/airflow/sdk/execution/JarUtils.kt | 25 ++ .../apache/airflow/sdk/execution/Logger.kt | 108 +++++ .../apache/airflow/sdk/execution/MsgPack.kt | 125 ++++++ .../org/apache/airflow/sdk/execution/Serde.kt | 275 ++++++++++++ .../airflow/sdk/execution/Supervisor.kt | 394 ++++++++++++++++++ .../airflow/sdk/execution/TaskRunner.kt | 28 ++ .../airflow/sdk/execution/TaskSdkFrames.kt | 235 +++++++++++ .../apache/airflow/sdk/BundleScannerTest.kt | 197 +++++++++ .../org/apache/airflow/sdk/BundleTest.kt | 28 ++ .../org/apache/airflow/sdk/ConfigTest.kt | 138 ++++++ .../apache/airflow/sdk/CoordinatorCommTest.kt | 113 +++++ .../apache/airflow/sdk/execution/CommsTest.kt | 86 ++++ .../airflow/sdk/execution/DagParserTest.kt | 28 ++ .../SerializationCompatibilityTest.kt | 151 +++++++ .../airflow/sdk/execution/TaskRunnerTest.kt | 110 +++++ java-sdk/settings.gradle.kts | 13 + java-sdk/validation/serialization/compare.py | 171 ++++++++ .../serialization/serialize_python.py | 125 ++++++ .../validation/serialization/test_dags.yaml | 198 +++++++++ 47 files changed, 4408 insertions(+) create mode 100644 java-sdk/.editorconfig create mode 100644 java-sdk/.gitattributes create mode 100644 java-sdk/.gitignore create mode 100644 java-sdk/README.md create mode 100644 java-sdk/build.gradle.kts create mode 100644 java-sdk/dags/stub_dag.py create mode 100644 java-sdk/example/build.gradle.kts create mode 100644 java-sdk/example/src/java/org/apache/airflow/example/JavaExample.java create mode 100644 java-sdk/gradle.properties create mode 100644 java-sdk/gradle/libs.versions.toml create mode 100644 java-sdk/gradle/wrapper/gradle-wrapper.jar create mode 100644 java-sdk/gradle/wrapper/gradle-wrapper.properties create mode 100755 java-sdk/gradlew create mode 100644 java-sdk/gradlew.bat create mode 100644 java-sdk/sdk/build.gradle.kts create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Bundle.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/BundleInspector.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/BundleScanner.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Client.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Config.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Connection.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Dag.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/DagBundle.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Server.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Task.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Client.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Comms.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/DagParser.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/JarUtils.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Logger.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/MsgPack.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Serde.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Supervisor.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/TaskRunner.kt create mode 100644 java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/TaskSdkFrames.kt create mode 100644 java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BundleScannerTest.kt create mode 100644 java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BundleTest.kt create mode 100644 java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/ConfigTest.kt create mode 100644 java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/CoordinatorCommTest.kt create mode 100644 java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/CommsTest.kt create mode 100644 java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/DagParserTest.kt create mode 100644 java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/SerializationCompatibilityTest.kt create mode 100644 java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/TaskRunnerTest.kt create mode 100644 java-sdk/settings.gradle.kts create mode 100644 java-sdk/validation/serialization/compare.py create mode 100644 java-sdk/validation/serialization/serialize_python.py create mode 100644 java-sdk/validation/serialization/test_dags.yaml diff --git a/java-sdk/.editorconfig b/java-sdk/.editorconfig new file mode 100644 index 0000000000000..37bdc0ac6ea59 --- /dev/null +++ b/java-sdk/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.java] +indent_size = 2 + +[*.kt] +indent_size = 2 diff --git a/java-sdk/.gitattributes b/java-sdk/.gitattributes new file mode 100644 index 0000000000000..a87d264c425cf --- /dev/null +++ b/java-sdk/.gitattributes @@ -0,0 +1,11 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# Binary files should be left untouched +*.jar binary diff --git a/java-sdk/.gitignore b/java-sdk/.gitignore new file mode 100644 index 0000000000000..bf1f44332ebc8 --- /dev/null +++ b/java-sdk/.gitignore @@ -0,0 +1,56 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +.kotlin + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store +# Ignore Gradle build output directory +build + +### Artifacts of airflow standalone command ### +airflow.cfg +airflow.db +simple_auth_manager_passwords.json.generated +logs/dag_id=* +logs/dag_processor + +### Compatibility Test Results ### +validation/serialization/serialized_java.json +validation/serialization/serialized_python.json diff --git a/java-sdk/README.md b/java-sdk/README.md new file mode 100644 index 0000000000000..92ef9ebdc5dd8 --- /dev/null +++ b/java-sdk/README.md @@ -0,0 +1,74 @@ + + +# Airflow Java SDK + +A **JVM** SDK for Apache Airflow. You can use any JVM-compatible language to write +workflow bundles, and have Airflow consume the result. + +The SDK and execution-time logic is implemented in Kotlin. +An example is bundled showing how the SDK can be used in Java. + +## Building + +```bash +./gradlew build +``` + +## Technical Details + +The Java program is launched as a subprocess by the Airflow worker and communicates +through TCP sockets. The Java program accepts flags `--comm` and `--logs` from the +command line. + +The Java program "parses" DAGs on launch, and then connects to the specified TCP servers. +The rest is similar to the standard Airflow: + +* DAG-parsing: + 1. On connection, the parent immediately sends a DagParsingRequest through the socket. + 2. The Java program sends back a DagParsingResult to the parent. + 3. The Java program exits. +* Execution: + 1. On connection, the parent immediately sends a StartupDetails through the socket. + 2. The Java program uses the information to find the relevant task to execute. + 3. The task is run. + 4. The Java program tells the parent to update the task's terminal state. + 5. The Java program exits. + +Communication uses the same formats as the Python-based processes. + +## Serialization Validation + +Workflow: + +```bash +# 1. Generate Java output (runs as part of normal test suite) +# More specifically, the test `sdk/src/test/kotlin/org/apache/airflow/sdk/execution/SerializationCompatibilityTest.kt` generates the output file `validation/serialization/serialized_java.json`. +./gradlew sdk:test + +# 2. Generate Python output (requires Airflow env) +uv run validation/serialization/serialize_python.py \ + validation/serialization/test_dags.yaml \ + validation/serialization/serialized_python.json + +# 3. Compare +uv run validation/serialization/compare.py \ + validation/serialization/serialized_python.json \ + validation/serialization/serialized_java.json +``` diff --git a/java-sdk/build.gradle.kts b/java-sdk/build.gradle.kts new file mode 100644 index 0000000000000..a9fb8d993533b --- /dev/null +++ b/java-sdk/build.gradle.kts @@ -0,0 +1,33 @@ +import com.diffplug.gradle.spotless.SpotlessExtension +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("jvm") version "2.3.0" + id("com.diffplug.spotless") version "7.2.1" // Last version supporting JDK 11. + id("org.jlleitschuh.gradle.ktlint") version "14.0.1" +} + +allprojects { + apply(plugin = "com.diffplug.spotless") + apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "org.jlleitschuh.gradle.ktlint") + + repositories { mavenCentral() } + + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } + sourceCompatibility = JavaVersion.VERSION_11 + } + kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_11 } } + + configure { + java { + target("**/*.java") + googleJavaFormat().formatJavadoc(false) + trimTrailingWhitespace() + endWithNewline() + } + } +} diff --git a/java-sdk/dags/stub_dag.py b/java-sdk/dags/stub_dag.py new file mode 100644 index 0000000000000..65a0832e6a8d3 --- /dev/null +++ b/java-sdk/dags/stub_dag.py @@ -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. + +from __future__ import annotations + +from airflow.sdk import dag, task + + +@task() +def python_task_1(ti): + print("python_task_1") + print("Push Python Task 'python_task_1' XCom:") + ti.xcom_push(value="value-pushed-from-python_task_1", key="return_value") + + +@task.stub(language="java") +def extract(): ... + + +@task.stub(language="java") +def transform(): ... + + +@task() +def python_task_2(ti): + print("python_task_2") + print("Pull Java Task 'transform' XCom:") + print(ti.xcom_pull(task_ids="transform")) + + +@dag(dag_id="java_example") +def simple_dag(): + + python_task_1() >> extract() >> transform() >> python_task_2() + + +simple_dag() diff --git a/java-sdk/example/build.gradle.kts b/java-sdk/example/build.gradle.kts new file mode 100644 index 0000000000000..e36ff5bd68199 --- /dev/null +++ b/java-sdk/example/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + application +} + +dependencies { + implementation(project(":sdk")) + implementation("org.slf4j:slf4j-simple:2.0.17") +} + +sourceSets { + main { + java.srcDir("src/java") + } +} + +application { + mainClass = "org.apache.airflow.example.JavaExample" +} + +val bundleMainClass = application.mainClass.get() +val metadataFileName = "airflow-metadata.yaml" +val metadataOutputDir = layout.buildDirectory.dir("airflow-metadata") +val dagCodeSourcePath = bundleMainClass.replace('.', '/') + ".java" +val dagCodeFileName = bundleMainClass.substringAfterLast('.') + ".java" + +val inspectBundle = + tasks.register("inspectBundle") { + dependsOn("classes") + classpath = sourceSets.main.get().runtimeClasspath + mainClass.set("org.apache.airflow.sdk.BundleInspector") + args = + listOf( + bundleMainClass, + metadataOutputDir + .get() + .file(metadataFileName) + .asFile.absolutePath, + ) + } + +tasks.withType { + dependsOn(inspectBundle) + from(metadataOutputDir) + from("src/java/$dagCodeSourcePath") + manifest { + attributes( + "Main-Class" to bundleMainClass, + "Airflow-Java-SDK-Version" to project.version, + "Airflow-Java-SDK-Metadata" to metadataFileName, + "Airflow-Java-SDK-Dag-Code" to dagCodeFileName, + "Implementation-Title" to "Example Java bundle", + "Implementation-Version" to "1", + ) + } +} diff --git a/java-sdk/example/src/java/org/apache/airflow/example/JavaExample.java b/java-sdk/example/src/java/org/apache/airflow/example/JavaExample.java new file mode 100644 index 0000000000000..1a681dd2c9391 --- /dev/null +++ b/java-sdk/example/src/java/org/apache/airflow/example/JavaExample.java @@ -0,0 +1,69 @@ +package org.apache.airflow.example; + +import java.util.Date; +import java.util.List; +import org.apache.airflow.sdk.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JavaExample implements DagBundle { + private static final Logger logger = LoggerFactory.getLogger(JavaExample.class); + + public static class Extract implements Task { + public void execute(Client client) throws Exception { + logger.info("Hello from task"); + + var python_xcom = client.getXCom("python_task_1"); + logger.info("Got XCom from Python Task 'python_task_1' {}", python_xcom); + + var connection = client.getConnection("test_http"); + logger.info("Got con {}", connection); + + for (var i = 0; i < 3; i++) { + logger.info("Beep {}, next time will be {}", i, new Date()); + Thread.sleep(2 * 1000); + } + + client.setXCom(new Date().getTime()); + logger.info("Goodbye from task"); + } + } + + public static class Transform implements Task { + public void execute(Client client) { + var extract_xcom = client.getXCom("extract"); + logger.info("Got XCom from 'extract' {}", extract_xcom); + + var variable = client.getVariable("my_variable"); + logger.info("Got variable {}", variable); + + logger.info("Push XCom to python task 2"); + client.setXCom(new Date().getTime()); + } + } + + public static class Load implements Task { + public void execute(Client client) { + var xcom = client.getXCom("transform"); + logger.info("Got XCom from 'transform' {}", xcom); + throw new RuntimeException("I failed"); + } + } + + @Override + public List getDags() { + var javaExample = new Dag("java_example", /* description= */ null, /* schedule= */ "@daily"); + javaExample.addTask("extract", Extract.class, List.of()); + javaExample.addTask("transform", Transform.class, List.of("extract")); + javaExample.addTask("load", Load.class, List.of("transform")); + return List.of(javaExample); + } + + public static void main(String[] args) { + var example = new JavaExample(); + var bundle = + new Bundle(JavaExample.class.getPackage().getImplementationVersion(), example.getDags()); + + Server.create(args).serve(bundle); + } +} diff --git a/java-sdk/gradle.properties b/java-sdk/gradle.properties new file mode 100644 index 0000000000000..7ec9aa8974afa --- /dev/null +++ b/java-sdk/gradle.properties @@ -0,0 +1,23 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.configuration-cache=true + +airflowExecApiVersion=2025-11-05 diff --git a/java-sdk/gradle/libs.versions.toml b/java-sdk/gradle/libs.versions.toml new file mode 100644 index 0000000000000..a0ac505d527fe --- /dev/null +++ b/java-sdk/gradle/libs.versions.toml @@ -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. + +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format diff --git a/java-sdk/gradle/wrapper/gradle-wrapper.jar b/java-sdk/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f8e1ee3125fe0768e9a76ee977ac089eb657005e GIT binary patch literal 45633 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�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 literal 0 HcmV?d00001 diff --git a/java-sdk/gradle/wrapper/gradle-wrapper.properties b/java-sdk/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000000..bc660c8e3d572 --- /dev/null +++ b/java-sdk/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,23 @@ +# 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. + +#Mon Jan 19 21:06:09 CST 2026 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java-sdk/gradlew b/java-sdk/gradlew new file mode 100755 index 0000000000000..adff685a0348c --- /dev/null +++ b/java-sdk/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# 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 +# +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/java-sdk/gradlew.bat b/java-sdk/gradlew.bat new file mode 100644 index 0000000000000..c4bdd3ab8e3cc --- /dev/null +++ b/java-sdk/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java-sdk/sdk/build.gradle.kts b/java-sdk/sdk/build.gradle.kts new file mode 100644 index 0000000000000..08d0b9c9639ef --- /dev/null +++ b/java-sdk/sdk/build.gradle.kts @@ -0,0 +1,125 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jlleitschuh.gradle.ktlint.tasks.KtLintCheckTask +import java.time.ZonedDateTime + +buildscript { + repositories { + mavenCentral() + } +} + +val airflowExecApiVersion: String by project + +plugins { + kotlin("plugin.serialization") version "2.3.0" + id("org.openapi.generator") version "7.19.0" +} + +val constantsDir = layout.buildDirectory.dir("generate-constants/main/src/main/kotlin") + +dependencies { + compileOnly("com.github.spotbugs:spotbugs-annotations:4.9.8") + compileOnly("javax.annotation:javax.annotation-api:1.3.2") + compileOnly("org.apache.oltu.oauth2:org.apache.oltu.oauth2.client:1.0.1") + + implementation("com.fasterxml.jackson.core:jackson-annotations:2.21") + implementation("com.fasterxml.jackson.core:jackson-core:2.21.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.21.0") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.21.0") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.0") + implementation("com.squareup.retrofit2:converter-jackson:3.0.0") + implementation("com.squareup.retrofit2:converter-scalars:3.0.0") + implementation("com.squareup.retrofit2:retrofit:3.0.0") + implementation("com.xenomachina:kotlin-argparser:2.0.7") + implementation("io.ktor:ktor-network:3.3.3") + implementation("javax.ws.rs:javax.ws.rs-api:2.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0") + implementation("org.msgpack:msgpack-core:0.9.11") + implementation("org.msgpack:jackson-dataformat-msgpack:0.9.11") + + testImplementation(kotlin("test")) +} + +openApiGenerate { + generatorName = "java" + library = "retrofit2" + + remoteInputSpec = "https://airflow.apache.org/schemas/execution-api/$airflowExecApiVersion.json" + apiPackage = "org.apache.airflow.sdk.execution.api.route" + modelPackage = "org.apache.airflow.sdk.execution.api.model" + invokerPackage = "org.apache.airflow.sdk.execution.api.client" + + generateApiDocumentation = false + generateApiTests = false + generateModelDocumentation = false + generateModelTests = false + + // The spec on arbitrary mapping (e.g. 'extra') causes the OpenAPI generator to output JsonValue. + // We should probably fix the spec instead, but this should work before that. + // Suggested fix: + // type: object + // additionalProperties: true + schemaMappings.put("JsonValue", "java.lang.Object") + + additionalProperties = + mapOf( + "dateLibrary" to "java8", + "openApiNullable" to false, + "serializationLibrary" to "jackson", + "withXml" to false, + ) +} + +sourceSets { + main { + java.srcDir(layout.buildDirectory.dir("generate-resources/main/src/main/java")) + kotlin.srcDir(constantsDir) + } +} + +abstract class GenerateConstantsTask : DefaultTask() { + @get:Input + abstract val airflowExecApiVersionProp: Property + + @get:OutputDirectory + abstract val outputDirProp: DirectoryProperty + + @TaskAction + fun generate() { + val dir = outputDirProp.get().asFile.resolve("org/apache/airflow/sdk/execution") + dir.mkdirs() + dir.resolve("BuildConstants.kt").writeText( + """ + // File generated at ${ZonedDateTime.now()} + package org.apache.airflow.sdk.execution + + const val AIRFLOW_EXEC_API_VERSION = "${airflowExecApiVersionProp.get()}" + """.trimIndent() + "\n", + ) + } +} + +tasks.register("generateConstants") { + airflowExecApiVersionProp = airflowExecApiVersion + outputDirProp = constantsDir +} + +tasks.named("compileJava") { + dependsOn("openApiGenerate") +} + +tasks.named("compileKotlin") { + dependsOn("openApiGenerate") + dependsOn("generateConstants") +} + +tasks.named("runKtlintCheckOverMainSourceSet") { + dependsOn("openApiGenerate") + dependsOn("generateConstants") +} + +tasks.named("test") { + useJUnitPlatform() +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Bundle.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Bundle.kt new file mode 100644 index 0000000000000..56a4f0f65343c --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Bundle.kt @@ -0,0 +1,18 @@ +package org.apache.airflow.sdk + +class Bundle( + val version: String, + dags: Iterable, +) { + val dags: Map = dags.associateByDagId() +} + +private fun Iterable.associateByDagId(): Map { + val dagMap = linkedMapOf() + for (dag in this) { + require(dagMap.putIfAbsent(dag.dagId, dag) == null) { + "Duplicate dagId in bundle: ${dag.dagId}" + } + } + return dagMap +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/BundleInspector.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/BundleInspector.kt new file mode 100644 index 0000000000000..2da6b34c19829 --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/BundleInspector.kt @@ -0,0 +1,40 @@ +package org.apache.airflow.sdk + +import java.io.File + +/** + * Build-time utility that inspects a [DagBundle] implementation and writes + * dag_ids and task_ids to a YAML metadata file for inclusion in the JAR. + * + * Usage: {@code java -cp org.apache.airflow.sdk.BundleInspector } + */ +object BundleInspector { + @JvmStatic + fun main(args: Array) { + require(args.size == 2) { "Usage: BundleInspector " } + val className = args[0] + val outputPath = args[1] + + val clazz = Class.forName(className) + val instance = + clazz.getDeclaredConstructor().newInstance() as? DagBundle + ?: error("$className does not implement ${DagBundle::class.qualifiedName}") + val dags = instance.getDags() + + val outputFile = File(outputPath) + outputFile.parentFile.mkdirs() + outputFile.writeText(toYaml(dags)) + } + + internal fun toYaml(dags: List): String = + buildString { + appendLine("dags:") + for (dag in dags) { + appendLine(" ${dag.dagId}:") + appendLine(" tasks:") + for (taskId in dag.tasks.keys) { + appendLine(" - $taskId") + } + } + } +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/BundleScanner.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/BundleScanner.kt new file mode 100644 index 0000000000000..32fc591f7003c --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/BundleScanner.kt @@ -0,0 +1,117 @@ +package org.apache.airflow.sdk + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import org.apache.airflow.sdk.execution.containsJars +import org.apache.airflow.sdk.execution.isJarFile +import org.apache.airflow.sdk.execution.jarFiles +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.util.jar.JarFile + +const val METADATA_MANIFEST_KEY = "Airflow-Java-SDK-Metadata" + +private val yamlMapper = ObjectMapper(YAMLFactory()) + +/** + * A fully resolved bundle: everything needed to start the bundle process. + */ +data class ResolvedBundle( + val mainClass: String, + val classpath: String, +) + +/** + * Scans [bundlesDir] for Java DAG bundles by checking JAR manifests for the + * [METADATA_MANIFEST_KEY] attribute and reading the referenced YAML metadata. + * + * Supports two layouts: + * - **Nested**: each immediate subdirectory of [bundlesDir] is a bundle home. + * - **Flat**: [bundlesDir] itself contains the bundle JARs. + * + * Returns a mapping from dag_id to a [ResolvedBundle] with mainClass and classpath. + */ +fun scanBundles(bundlesDir: Path): Map { + if (!Files.isDirectory(bundlesDir)) return emptyMap() + val result = mutableMapOf() + + // Check each immediate subdirectory as a potential bundle home. + Files.list(bundlesDir).use { paths -> + paths.filter { Files.isDirectory(it) }.forEach { candidate -> + collectBundleDags(candidate, result) + } + } + + // Also check bundlesDir itself (flat layout). + collectBundleDags(bundlesDir, result) + + return result +} + +private fun collectBundleDags( + candidate: Path, + result: MutableMap, +) { + val bundleHome = normalizeBundleHome(candidate) + val resolved = resolveBundle(bundleHome) ?: return + for (dagId in resolved.first) { + result.putIfAbsent(dagId, resolved.second) + } +} + +/** + * Inspects JARs in [bundleHome] for [METADATA_MANIFEST_KEY] and Main-Class. + * Returns (dagIds, ResolvedBundle) or null if no JAR carries the metadata attribute. + */ +private fun resolveBundle(bundleHome: Path): Pair, ResolvedBundle>? { + val jars = jarFiles(bundleHome) + if (jars.isEmpty()) return null + + for (jarPath in jars) { + JarFile(jarPath.toFile()).use { jar -> + val attrs = jar.manifest?.mainAttributes ?: return@use + val metadataFile = attrs.getValue(METADATA_MANIFEST_KEY) ?: return@use + val mainClass = attrs.getValue("Main-Class") ?: return@use + val entry = jar.getJarEntry(metadataFile) ?: return@use + val content = jar.getInputStream(entry).bufferedReader().readText() + val dagIds = parseDagIdsFromYaml(content) + if (dagIds.isEmpty()) return@use + + val classpath = + jars + .map { it.toAbsolutePath().normalize().toString() } + .joinToString(File.pathSeparator) + + return dagIds to ResolvedBundle(mainClass, classpath) + } + } + return null +} + +fun readBundleDagIds(bundleHome: Path): Set { + for (jarPath in jarFiles(bundleHome)) { + JarFile(jarPath.toFile()).use { jar -> + val metadataFile = jar.manifest?.mainAttributes?.getValue(METADATA_MANIFEST_KEY) ?: return@use + val entry = jar.getJarEntry(metadataFile) ?: return@use + val content = jar.getInputStream(entry).bufferedReader().readText() + return parseDagIdsFromYaml(content) + } + } + return emptySet() +} + +fun parseDagIdsFromYaml(yaml: String): Set { + val root = yamlMapper.readTree(yaml) + val dagsNode = root.get("dags") ?: return emptySet() + val dagIds = mutableSetOf() + dagsNode.fieldNames().forEachRemaining { dagIds.add(it) } + return dagIds +} + +private fun normalizeBundleHome(path: Path): Path { + val normalized = path.toAbsolutePath().normalize() + if (normalized.isJarFile()) return normalized.parent + val lib = normalized.resolve("lib") + return if (containsJars(lib)) lib else normalized +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Client.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Client.kt new file mode 100644 index 0000000000000..582496ff0e2e0 --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Client.kt @@ -0,0 +1,59 @@ +package org.apache.airflow.sdk + +import org.apache.airflow.sdk.execution.Client +import org.apache.airflow.sdk.execution.StartupDetails + +class Client( + val details: StartupDetails, + val impl: Client, +) { + companion object { + const val XCOM_RETURN_KEY = "return_value" + } + + fun getConnection(id: String): Connection = + with(impl.getConnection(id)) { + Connection( + id = connId, + type = connType, + host = host, + schema = schema, + login = login, + password = password, + port = port, + extra = extra, + ) + } + + fun getVariable(key: String): Any? = impl.getVariable(key).value + + @JvmOverloads fun getXCom( + key: String = XCOM_RETURN_KEY, + dagId: String = details.ti.dagId, + taskId: String, + runId: String = details.ti.runId, + mapIndex: Int? = null, + includePriorDates: Boolean = false, + ): Any? = + impl + .getXCom( + key = key, + dagId = dagId, + taskId = taskId, + runId = runId, + mapIndex = mapIndex, + includePriorDates = includePriorDates, + ).value + + @JvmOverloads fun setXCom( + key: String = XCOM_RETURN_KEY, + value: Any, + ) = impl.setXCom( + key = key, + value = value, + dagId = details.ti.dagId, + taskId = details.ti.taskId, + runId = details.ti.runId, + mapIndex = details.ti.mapIndex ?: -1, + ) +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Config.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Config.kt new file mode 100644 index 0000000000000..1a6c69b2b901f --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Config.kt @@ -0,0 +1,136 @@ +package org.apache.airflow.sdk + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +private const val CONFIG_FILE_NAME = "java-sdk.yaml" + +open class WorkerError( + message: String, +) : IllegalStateException(message) + +class NoBody : WorkerError("No body") + +/** + * SDK configuration resolved from environment variables and an optional YAML config file. + * + * Resolution order (highest priority first): + * 1. Environment variable `AIRFLOW__

__` (uppercase, double-underscore delimited) + * 2. YAML config file value at `
.` (lowercase) + * 3. Default value (where applicable) + * + * Only the canonical `AIRFLOW__
__` env var form is recognised. + * Single-underscore variants (`AIRFLOW_SECTION_KEY`) are **not** supported — use the + * YAML file for a more readable alternative. + * + * The YAML file is loaded from `$AIRFLOW_HOME/java-sdk.yaml` when present. + * + * ```yaml + * core: + * execution_api_server_url: "http://localhost:8080/execution/" + * + * sdk: + * bundles_dir: "./bin" + * + * api_auth: + * jwt_secret: "your-secret-key" + * jwt_issuer: "airflow" + * jwt_expiration_time: 30 + * ``` + * + * Each YAML key corresponds directly to the env-var option name: + * `core.execution_api_server_url` ↔ `AIRFLOW__CORE__EXECUTION_API_SERVER_URL`. + */ +class SdkConfig( + private val env: Map = System.getenv(), + yamlOverride: Path? = null, +) { + @Suppress("UNCHECKED_CAST") + private val yaml: Map> = + run { + val path = yamlOverride ?: resolveConfigPath(env) + if (path != null && Files.isRegularFile(path)) { + val raw = ObjectMapper(YAMLFactory()).readValue(path.toFile(), Map::class.java) as? Map ?: emptyMap() + raw.entries.associate { (k, v) -> + k to ((v as? Map<*, *>)?.entries?.associate { (ik, iv) -> ik.toString() to iv } ?: emptyMap()) + } + } else { + emptyMap() + } + } + + /** + * Look up a config value by section and key. + * Checks `AIRFLOW__
__` env var first, then YAML `
.`. + */ + fun get( + section: String, + key: String, + ): String? { + val envKey = "AIRFLOW__${section.uppercase()}__${key.uppercase()}" + env[envKey]?.takeIf { it.isNotBlank() }?.let { return it } + return yaml[section]?.get(key)?.toString()?.takeIf { it.isNotBlank() } + } + + /** Like [get] but throws [WorkerError] when the value is missing. */ + fun require( + section: String, + key: String, + ): String = + get(section, key) + ?: throw WorkerError( + "$section.$key must be configured " + + "(AIRFLOW__${section.uppercase()}__${key.uppercase()} or $CONFIG_FILE_NAME)", + ) + + /** Resolve a positive long, falling back to [default]. */ + fun getPositiveLong( + section: String, + key: String, + default: Long, + ): Long { + val raw = get(section, key) ?: return default + val parsed = + raw.toLongOrNull() + ?: throw WorkerError("$section.$key must be an integer") + if (parsed <= 0) throw WorkerError("$section.$key must be greater than 0") + return parsed + } + + // -- Execution API -- + + val executionApiUrl: String + get() { + val url = + get("core", "execution_api_server_url") + ?: get("execution", "api_url") + return url?.ensureTrailingSlash() + ?: throw WorkerError( + "core.execution_api_server_url must be configured " + + "(AIRFLOW__CORE__EXECUTION_API_SERVER_URL or $CONFIG_FILE_NAME)", + ) + } + + // -- JWT -- + + val jwtSecret: String get() = require("api_auth", "jwt_secret") + val jwtIssuer: String? get() = get("api_auth", "jwt_issuer") + val jwtExpirationTime: Long get() = getPositiveLong("api_auth", "jwt_expiration_time", 30) + + // -- Bundle resolution -- + + val bundlesDir: Path? + get() = get("sdk", "bundles_dir")?.let(Paths::get) + + companion object { + private fun resolveConfigPath(env: Map): Path? { + val home = env["AIRFLOW_HOME"]?.takeIf { it.isNotBlank() } ?: return null + return Paths.get(home, CONFIG_FILE_NAME) + } + } +} + +internal fun String.ensureTrailingSlash() = if (endsWith('/')) this else "$this/" diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Connection.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Connection.kt new file mode 100644 index 0000000000000..98a7dab39bded --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Connection.kt @@ -0,0 +1,12 @@ +package org.apache.airflow.sdk + +data class Connection( + val id: String, + val type: String?, + val host: String?, + val schema: String?, + val login: String?, + val password: String?, + val port: Int?, + val extra: String?, +) diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Dag.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Dag.kt new file mode 100644 index 0000000000000..0df3f1251f105 --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Dag.kt @@ -0,0 +1,86 @@ +package org.apache.airflow.sdk + +import java.time.Duration +import java.time.Instant + +/** + * A Dag (Directed Acyclic Graph) is a collection of tasks with directional dependencies. + * + * A Dag has a schedule, a start date and an end date (optional). For each schedule + * (say daily or hourly), the Dag needs to run each individual task as their + * dependencies are met. + * + * @param dagId The id of the Dag; must consist exclusively of alphanumeric characters, + * dashes, dots and underscores (all ASCII). + * @param description The description for the Dag to e.g. be shown on the webserver. + * @param schedule Defines when Dag runs are scheduled. Can be a cron expression string + * (e.g. "0 0 * * *"), a preset (e.g. "@daily", "@hourly", "@once", "@continuous"), + * or null for no schedule. + * @param startDate The timestamp from which the scheduler will attempt to backfill. + * @param endDate A date beyond which your Dag won't run; leave null for open-ended scheduling. + * @param defaultArgs A map of default parameters to be used as constructor keyword + * parameters when initialising operators. + * @param maxActiveTasks The number of task instances allowed to run concurrently per Dag run. + * @param maxActiveRuns Maximum number of active Dag runs. + * @param maxConsecutiveFailedDagRuns Maximum number of consecutive failed Dag runs, + * beyond this the scheduler will disable the Dag. + * @param dagrunTimeout Duration a DagRun is allowed to run before it times out or fails. + * @param catchup Perform scheduler catchup (or only run latest)? Defaults to false. + * @param docMd Documentation in markdown format. + * @param accessControl Optional Dag-level access control actions, e.g. + * mapOf("role1" to mapOf("DAGs" to setOf("can_read", "can_edit"))). + * @param isPausedUponCreation Whether the Dag is paused when created for the first time. + * @param tags Set of tags to help filtering Dags in the UI. + * @param ownerLinks Map of owners and their links, clickable on the Dags view UI. + * @param failFast Fails currently running tasks when a task in Dag fails. + * @param dagDisplayName The display name of the Dag on the UI. Defaults to dagId. + * @param renderTemplateAsNativeObj If true, uses native rendering for templates. + * @param params A map of Dag-level parameters accessible in templates, namespaced under + * "params". These can be overridden at the task level. + */ +class Dag + @JvmOverloads + constructor( + val dagId: String, + val description: String? = null, + val schedule: String? = null, + val startDate: Instant? = null, + val endDate: Instant? = null, + val defaultArgs: Map = emptyMap(), + val maxActiveTasks: Int = DEFAULT_MAX_ACTIVE_TASKS, + val maxActiveRuns: Int = DEFAULT_MAX_ACTIVE_RUNS, + val maxConsecutiveFailedDagRuns: Int = DEFAULT_MAX_CONSECUTIVE_FAILED_DAG_RUNS, + val dagrunTimeout: Duration? = null, + val catchup: Boolean = false, + val docMd: String? = null, + val accessControl: Map>>? = null, + val isPausedUponCreation: Boolean? = null, + val tags: Set = emptySet(), + val ownerLinks: Map = emptyMap(), + val failFast: Boolean = false, + val dagDisplayName: String? = null, + val renderTemplateAsNativeObj: Boolean = false, + val params: Map? = null, + ) { + internal var tasks = mutableMapOf>() + internal var dependants = mutableMapOf>() + + @JvmOverloads + fun addTask( + id: String, + definition: Class, + dependsOn: List = emptyList(), + ) { + // TODO: Check duplicate key. + tasks[id] = definition + for (parent in dependsOn) { + dependants.getOrPut(parent) { mutableSetOf() }.add(id) + } + } + + companion object { + const val DEFAULT_MAX_ACTIVE_TASKS = 16 + const val DEFAULT_MAX_ACTIVE_RUNS = 16 + const val DEFAULT_MAX_CONSECUTIVE_FAILED_DAG_RUNS = 0 + } + } diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/DagBundle.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/DagBundle.kt new file mode 100644 index 0000000000000..9890bd20b6076 --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/DagBundle.kt @@ -0,0 +1,12 @@ +package org.apache.airflow.sdk + +/** + * Interface for declaring DAGs in a bundle. + * + *

Implement this interface in the class specified as {@code Main-Class} in your JAR manifest. + * The build system instantiates this class at compile time to extract dag_ids and task_ids + * into the JAR manifest, enabling inspection of bundled DAGs without running the full process. + */ +interface DagBundle { + fun getDags(): List +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Server.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Server.kt new file mode 100644 index 0000000000000..bc72bfa3296cf --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Server.kt @@ -0,0 +1,79 @@ +package org.apache.airflow.sdk + +import com.xenomachina.argparser.ArgParser +import io.ktor.network.selector.SelectorManager +import io.ktor.network.sockets.InetSocketAddress +import io.ktor.network.sockets.aSocket +import io.ktor.network.sockets.openReadChannel +import io.ktor.network.sockets.openWriteChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.apache.airflow.sdk.execution.CoordinatorComm +import org.apache.airflow.sdk.execution.LogSender +import org.apache.airflow.sdk.execution.Logger +import kotlin.text.substringAfterLast +import kotlin.text.substringBeforeLast + +private class Args( + parser: ArgParser, +) { + private fun parseAddress(address: String): InetSocketAddress = + InetSocketAddress( + address.substringBeforeLast(':'), + address.substringAfterLast(':').toInt(), + ) + + val comm by parser.storing("--comm", help = "Address (host:port) to communicate with parent") { + parseAddress(this) + } + val logs by parser.storing("--logs", help = "Address (host:port) to send Airflow logs to") { + parseAddress(this) + } +} + +class ApiError( + message: String, +) : IllegalStateException(message) + +class Server( + private val comm: InetSocketAddress, + private val logs: InetSocketAddress, +) { + companion object { + @JvmStatic + fun create(args: Array): Server { + val args = ArgParser(args).parseInto(::Args) + return Server(args.comm, args.logs) + } + } + + private val logger = Logger(Server::class) + + fun serve(bundle: Bundle) { + runBlocking { + launch { + awaitAll( + async { + aSocket(SelectorManager(Dispatchers.IO)).tcp().connect(comm).use { socket -> + logger.debug("Connected comm", mapOf("addr" to comm)) + CoordinatorComm( + bundle, + socket.openReadChannel(), + socket.openWriteChannel(autoFlush = true), + ).startProcessing() + } + }, + async { + aSocket(SelectorManager(Dispatchers.IO)).tcp().connect(logs).use { socket -> + logger.debug("Connected logs", mapOf("addr" to logs)) + LogSender.configure(socket.openWriteChannel(autoFlush = true)) + } + }, + ) + } + } + } +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Task.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Task.kt new file mode 100644 index 0000000000000..08ca52e2ae5c1 --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Task.kt @@ -0,0 +1,8 @@ +package org.apache.airflow.sdk + +import kotlin.Throws + +interface Task { + @Throws(Exception::class) + fun execute(client: Client) +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Client.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Client.kt new file mode 100644 index 0000000000000..7388b308e6d3c --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Client.kt @@ -0,0 +1,143 @@ +package org.apache.airflow.sdk.execution + +import kotlinx.coroutines.runBlocking +import org.apache.airflow.sdk.execution.api.client.ApiClient +import org.apache.airflow.sdk.execution.api.model.ConnectionResponse +import org.apache.airflow.sdk.execution.api.model.VariableResponse +import org.apache.airflow.sdk.execution.api.model.XComResponse +import org.apache.airflow.sdk.execution.api.route.ConnectionsApi +import org.apache.airflow.sdk.execution.api.route.VariablesApi +import org.apache.airflow.sdk.execution.api.route.XComsApi +import java.time.LocalDate + +interface Client { + fun getConnection(id: String): ConnectionResponse + + fun getVariable(key: String): VariableResponse + + fun getXCom( + key: String, + dagId: String, + taskId: String, + runId: String, + mapIndex: Int? = null, + includePriorDates: Boolean = false, + ): XComResponse + + fun setXCom( + key: String, + value: Any, + dagId: String, + taskId: String, + runId: String, + mapIndex: Int, + ) +} + +class CoordinatorClient( + val exec: CoordinatorComm, +) : Client { + override fun getConnection(id: String) = runBlocking { exec.communicate(GetConnection(id)) } + + override fun getVariable(key: String) = runBlocking { exec.communicate(GetVariable(key)) } + + override fun setXCom( + key: String, + value: Any, + dagId: String, + taskId: String, + runId: String, + mapIndex: Int, + ) { + val message = + SetXCom( + key = key, + value = value, + dagId = dagId, + taskId = taskId, + runId = runId, + mapIndex = mapIndex, + ) + runBlocking { exec.communicate(message) } + } + + override fun getXCom( + key: String, + dagId: String, + taskId: String, + runId: String, + mapIndex: Int?, + includePriorDates: Boolean, + ): XComResponse { + val message = + GetXCom( + key = key, + dagId = dagId, + taskId = taskId, + runId = runId, + mapIndex = mapIndex, + includePriorDates = includePriorDates, + ) + return runBlocking { exec.communicate(message) } + } +} + +class HttpExecApiClient( + val http: ApiClient, +) : Client { + companion object { + val version: LocalDate = LocalDate.parse(AIRFLOW_EXEC_API_VERSION) + } + + override fun getConnection(id: String) = + http.communicate { + getConnection(id, version) + } + + override fun getVariable(key: String) = + http.communicate { + getVariable(key, version) + } + + override fun getXCom( + key: String, + dagId: String, + taskId: String, + runId: String, + mapIndex: Int?, + includePriorDates: Boolean, + ) = http.communicate { + getXcom( + dagId, + runId, + taskId, + key, + mapIndex, + includePriorDates, + 0, + version, + ) + } + + override fun setXCom( + key: String, + value: Any, + dagId: String, + taskId: String, + runId: String, + mapIndex: Int, + ) { + http.communicate { + setXcom( + dagId, + runId, + taskId, + key, + mapIndex, + null, + version, + value, + ) + } + } +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Comms.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Comms.kt new file mode 100644 index 0000000000000..acc0e5bb4154f --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Comms.kt @@ -0,0 +1,240 @@ +package org.apache.airflow.sdk.execution + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.ByteWriteChannel +import io.ktor.utils.io.readByteArray +import io.ktor.utils.io.writeByteArray +import org.apache.airflow.sdk.ApiError +import org.apache.airflow.sdk.Bundle +import org.apache.airflow.sdk.execution.api.client.ApiClient +import org.apache.airflow.sdk.execution.api.model.AssetProfile +import org.apache.airflow.sdk.execution.api.model.BundleInfo +import org.apache.airflow.sdk.execution.api.model.TIRunContext +import org.apache.airflow.sdk.execution.api.model.TISuccessStatePayload +import org.apache.airflow.sdk.execution.api.model.TaskInstance +import retrofit2.Call +import java.time.OffsetDateTime +import kotlin.concurrent.atomics.AtomicInt +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.system.exitProcess + +data class IncomingFrame( + val id: Int, + val body: Any?, +) + +data class OutgoingFrame( + val id: Int, + val body: Any, +) + +class ErrorResponse { + @JsonProperty("error") + var error: String = "" // TODO: Use enum. + + @JsonProperty("detail") + var detail: Any? = null +} + +class DagFileParseRequest { + var file: String = "" + + @JsonProperty("bundle_path") + var bundlePath: String = "" +} + +class StartupDetails { + @JsonProperty("ti") + lateinit var ti: TaskInstance + + @JsonProperty("dag_rel_path") + var dagRelPath: String = "" + + @JsonProperty("bundle_info") + lateinit var bundleInfo: BundleInfo + + @JsonProperty("start_date") + lateinit var startDate: OffsetDateTime + + @JsonProperty("ti_context") + lateinit var tiContext: TIRunContext + + @JsonProperty("sentry_integration") + var sentryIntegration: String = "" +} + +class SucceedTask : TISuccessStatePayload { + constructor( + endDate: OffsetDateTime = OffsetDateTime.now(), + taskOutlets: List = emptyList(), + outletEvents: List> = emptyList(), + ) { + endDate(endDate) + taskOutlets(taskOutlets) + outletEvents(outletEvents) + } + + val type = "SucceedTask" +} + +@JsonPropertyOrder(value = ["state", "end_date", "type"]) +data class TaskState( + val state: String, // TODO: Use enum (failed, removed, skipped) and custom serialization. + @get:JsonProperty("end_date") val endDate: OffsetDateTime = OffsetDateTime.now(), +) { + val type = "TaskState" +} + +data class GetConnection( + @get:JsonProperty("conn_id") val id: String, +) { + val type = "GetConnection" +} + +data class GetVariable( + val key: String, +) { + val type = "GetVariable" +} + +data class GetXCom( + val key: String, + @get:JsonProperty("dag_id") val dagId: String, + @get:JsonProperty("task_id") val taskId: String, + @get:JsonProperty("run_id") val runId: String, + @get:JsonProperty("map_index") val mapIndex: Int? = null, + @get:JsonProperty("include_prior_dates") val includePriorDates: Boolean = false, +) { + val type = "GetXCom" +} + +data class SetXCom( + val key: String, + val value: Any, + @get:JsonProperty("dag_id") val dagId: String, + @get:JsonProperty("task_id") val taskId: String, + @get:JsonProperty("run_id") val runId: String, + @get:JsonProperty("map_index") val mapIndex: Int, + @get:JsonProperty("mapped_length") val mappedLength: Int? = null, +) { + val type = "SetXCom" +} + +@OptIn(ExperimentalAtomicApi::class) +class CoordinatorComm( + private val bundle: Bundle, + private val reader: ByteReadChannel, + private val writer: ByteWriteChannel, +) { + internal companion object { + private val logger = Logger(CoordinatorComm::class) + + fun encode(outgoing: OutgoingFrame): ByteArray { + val body = + when (val message = outgoing.body) { + is DagParsingResult -> message.serialize() + else -> message + } + return TaskSdkFrames.encodeRequest(outgoing.id, body) + } + + fun decode(bytes: ByteArray): IncomingFrame = TaskSdkFrames.decode(bytes, TaskSdkFrames.toBundleProcessTypes) + } + + private val nextId = AtomicInt(0) + private var shutDownRequested = false + + suspend fun startProcessing() { + while (!shutDownRequested) { + processOnce(::handleIncoming) + } + logger.debug("Goodbye") + } + + private suspend fun processOnce(handle: suspend (IncomingFrame) -> Unit) { + val prefix = reader.readByteArray(4) // First 4 bytes as length. + if (prefix.size != 4) { // Something is terribly wrong. Let's bail. + logger.error("Need 4 prefix bytes", mapOf("actual" to prefix.size)) + shutDownRequested = true + return + } + + val payloadLength = TaskSdkFrames.parseLengthPrefix(prefix) + val payload = reader.readByteArray(payloadLength) + if (payload.size != payloadLength) { // Something is terribly wrong. Let's bail. + logger.error( + "Payload length not right", + mapOf("expect" to payloadLength, "receive" to payload.size), + ) + shutDownRequested = true + return + } + val frame = decode(payload) + logger.debug("Handling", mapOf("id" to frame.id)) + handle(frame) + } + + private suspend fun sendMessage( + id: Int, + body: Any, + ) { + val data = encode(OutgoingFrame(id, body)) + logger.debug("Sending", mapOf("id" to id, "body" to body)) + writer.writeByteArray(TaskSdkFrames.lengthPrefix(data.size)) + writer.writeByteArray(data) + } + + suspend fun handleIncoming(frame: IncomingFrame) { + when (val request = frame.body) { + null -> {} + is ErrorResponse -> { + println("Error!! id=${frame.id} [${request.error}] ${request.detail}") // TODO: Handle error. + exitProcess(1) + } + is DagFileParseRequest -> { + val body = DagParser(request.file, request.bundlePath).parse(bundle) + sendMessage(frame.id, body) + shutDownRequested = true + } + is StartupDetails -> { + sendMessage(frame.id, TaskRunner.run(bundle, request, this)) + shutDownRequested = true + } + } + } + + @Throws(ApiError::class) + suspend fun communicateImpl(body: Any): Any { + var frame: IncomingFrame? = null + + suspend fun handle(f: IncomingFrame) { + frame = f + } + sendMessage(nextId.fetchAndAdd(1), body) + processOnce(::handle) + if (frame == null) { + throw ApiError("No response received") + } + return frame.body ?: Unit + } + + @Throws(ApiError::class) + suspend inline fun communicate(request: Any): T { + when (val response = communicateImpl(request)) { + is ErrorResponse -> throw ApiError("[${response.error}] ${response.detail}") + is T -> return response + else -> throw ApiError("Unexpected response type ${response::class.java}") + } + } +} + +internal inline fun ApiClient.communicate(block: S.() -> Call): R { + val service = createService(S::class.java) + val response = block(service).execute() + if (!response.isSuccessful) { + throw ApiError("[${response.message()}] $response (from $service") + } + return response.body() ?: throw ApiError("No body") +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/DagParser.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/DagParser.kt new file mode 100644 index 0000000000000..4cba6fd775365 --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/DagParser.kt @@ -0,0 +1,17 @@ +package org.apache.airflow.sdk.execution + +import org.apache.airflow.sdk.Bundle +import org.apache.airflow.sdk.Dag + +data class DagParsingResult( + val fileloc: String, + val bundlePath: String, + val dags: Map, +) + +class DagParser( + val file: String, + val bundlePath: String, +) { + fun parse(bundle: Bundle): DagParsingResult = DagParsingResult(file, bundlePath, bundle.dags) +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/JarUtils.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/JarUtils.kt new file mode 100644 index 0000000000000..1b62162ab0ee3 --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/JarUtils.kt @@ -0,0 +1,25 @@ +package org.apache.airflow.sdk.execution + +import java.nio.file.Files +import java.nio.file.Path + +/** True when [this] points to a regular file whose name ends with `.jar`. */ +fun Path.isJarFile(): Boolean = Files.isRegularFile(this) && fileName.toString().endsWith(".jar") + +/** Lists JAR files in [directory], sorted by path name. */ +fun jarFiles(directory: Path): List { + if (!Files.isDirectory(directory)) return emptyList() + val jars = mutableListOf() + Files.list(directory).use { paths -> + paths + .filter { it.isJarFile() } + .sorted() + .forEach { jars.add(it) } + } + return jars +} + +/** True when [directory] contains at least one JAR file. */ +fun containsJars(directory: Path): Boolean = + Files.isDirectory(directory) && + Files.list(directory).use { paths -> paths.anyMatch { it.isJarFile() } } diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Logger.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Logger.kt new file mode 100644 index 0000000000000..1378531673dbb --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Logger.kt @@ -0,0 +1,108 @@ +package org.apache.airflow.sdk.execution + +import io.ktor.utils.io.ByteWriteChannel +import io.ktor.utils.io.writeString +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlin.reflect.KClass +import kotlin.time.Clock + +enum class Level { ERROR, DEBUG, } + +internal data class LogMessage( + val event: String, + val arguments: Map, + val logger: Logger, + val level: Level, + val timestamp: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()), +) + +internal class Logger( + cls: KClass<*>, +) { + val name: String? = cls.java.typeName + + // TODO: Actually implement level filtering. + @Suppress("UNUSED_PARAMETER") + fun isEnabledForLevel(level: Level): Boolean = true + + fun debug( + message: String, + arguments: Map = emptyMap(), + ) { + log(Level.DEBUG, message, arguments) + } + + fun error( + message: String, + arguments: Map = emptyMap(), + ) { + log(Level.ERROR, message, arguments) + } + + private fun log( + level: Level, + event: String, + arguments: Map, + ) { + if (!isEnabledForLevel(level)) return + LogSender.send(LogMessage(event, arguments, this, level)) + } +} + +internal object LogSender { + private var writer: ByteWriteChannel? = null + val messages: MutableList = mutableListOf() + + fun configure(channel: ByteWriteChannel) { + writer = channel + if (!channel.isClosedForWrite) { + while (messages.isNotEmpty()) { + sendTo(channel, messages.removeAt(0)) + } + } + } + + fun send(message: LogMessage) { + val channel = writer + if (channel == null || channel.isClosedForWrite) { + messages.add(message) + } else { + sendTo(channel, message) + } + } + + private fun sendTo( + writer: ByteWriteChannel, + message: LogMessage, + ) { + val map = message.arguments.toMutableMap() + map["event"] = message.event + map["level"] = message.level.name.lowercase() + map["logger"] = message.logger.name ?: "(java)" + map["timestamp"] = message.timestamp + // TODO: Can this be done asynchronously instead? + runBlocking { writer.writeString("${map.toJsonElement()}\n") } + } +} + +private fun Any?.toJsonElement(): JsonElement = + when (this) { + is JsonElement -> this + is Map<*, *> -> + buildJsonObject { + forEach { (k, v) -> put(k.toString(), v.toJsonElement()) } + } + is Iterable<*> -> buildJsonArray { forEach { add(it.toJsonElement()) } } + is Number -> JsonPrimitive(this) + is String -> JsonPrimitive(this) + null -> JsonNull + else -> JsonPrimitive(toString()) // Also correctly handles Kotlinx DateTime. + } diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/MsgPack.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/MsgPack.kt new file mode 100644 index 0000000000000..f951248bf6952 --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/MsgPack.kt @@ -0,0 +1,125 @@ +package org.apache.airflow.sdk.execution + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonToken +import com.fasterxml.jackson.core.util.JacksonFeatureSet +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature +import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer +import org.msgpack.core.ExtensionTypeHeader +import org.msgpack.core.MessagePack +import org.msgpack.core.MessagePacker +import org.msgpack.core.MessageUnpacker +import org.msgpack.jackson.dataformat.MessagePackExtensionType +import org.msgpack.value.ArrayValue +import org.msgpack.value.MapValue +import org.msgpack.value.Value +import org.msgpack.value.ValueType +import java.math.BigInteger +import java.time.OffsetDateTime +import java.time.ZoneOffset + +private fun MessagePacker.packByteArray(data: ByteArray) { + packBinaryHeader(data.size) + data.forEach { packByte(it) } +} + +private fun MessagePacker.packMap(data: Map<*, *>) { + packMapHeader(data.size) + data.forEach { (k, v) -> + check(k is String) + packString(k) + packAny(v) + } +} + +private fun MessagePacker.packCollection(data: Collection<*>) { + packArrayHeader(data.size) + data.forEach { packAny(it) } +} + +fun MessagePacker.packAny(data: Any?) { + when (data) { + null -> packNil() + is Boolean -> packBoolean(data) + is Byte -> packByte(data) + is Short -> packShort(data) + is Int -> packInt(data) + is Long -> packLong(data) + is BigInteger -> packBigInteger(data) + is Float -> packFloat(data) + is Double -> packDouble(data) + is ByteArray -> packByteArray(data) + is String -> packString(data) + is Map<*, *> -> packMap(data) + is Collection<*> -> packCollection(data) + else -> throw IllegalArgumentException("Unsupported data type: $data") + } +} + +private fun ArrayValue.decodeArray(): List<*> = + mutableListOf().also { + iterator().forEach { v -> it.add(v.decode()) } + } + +private fun MapValue.decodeMap(): Map<*, *> = + mutableMapOf().also { + entrySet().forEach { (k, v) -> it[k.asStringValue().asString()] = v.decode() } + } + +private fun Value.decode(): Any? = + when (valueType) { + ValueType.NIL -> null + ValueType.BOOLEAN -> asBooleanValue().boolean + ValueType.INTEGER -> + with(asIntegerValue()) { + if (isInLongRange) asLong() else asBigInteger() + } + ValueType.FLOAT -> asFloatValue().toDouble() + ValueType.STRING -> asStringValue().asString() + ValueType.BINARY -> asBinaryValue().asByteArray() + ValueType.ARRAY -> asArrayValue().decodeArray() + ValueType.MAP -> asMapValue().decodeMap() + else -> throw IllegalArgumentException("Unsupported data type: $this") + } + +fun MessageUnpacker.unpackAny(): Any? = unpackValue().decode() + +class TimestampToJavaOffsetDateTimeModule : SimpleModule() { + companion object { + const val EXT_TYPE: Byte = -1 + } + + class OffsetDateTimeDeserializer : StdDeserializer(OffsetDateTime::class.java) { + val instantDeserializer = + InstantDeserializer.OFFSET_DATE_TIME.withFeatures( + JacksonFeatureSet.fromDefaults(JavaTimeFeature.entries.toTypedArray()), + ) + + override fun deserialize( + p: JsonParser, + ctxt: DeserializationContext, + ): OffsetDateTime { + if (p.currentToken == JsonToken.VALUE_EMBEDDED_OBJECT) { + deserializeMsgPackTimestamp(p)?.let { return it } + } + return instantDeserializer.deserialize(p, ctxt) + } + + private fun deserializeMsgPackTimestamp(p: JsonParser): OffsetDateTime? { + val ext = p.readValueAs(MessagePackExtensionType::class.java) + if (ext.type != EXT_TYPE) { + return null + } + val unpacker = MessagePack.newDefaultUnpacker(ext.data) + val instant = unpacker.unpackTimestamp(ExtensionTypeHeader(EXT_TYPE, ext.data.size)) + return instant.atOffset(ZoneOffset.UTC) + } + } + + init { + addDeserializer(OffsetDateTime::class.java, OffsetDateTimeDeserializer()) + } +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Serde.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Serde.kt new file mode 100644 index 0000000000000..21a398298662e --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Serde.kt @@ -0,0 +1,275 @@ +package org.apache.airflow.sdk.execution + +import org.apache.airflow.sdk.Dag +import org.apache.airflow.sdk.Task +import java.nio.file.Path +import java.time.Duration +import java.time.Instant + +/** + * Serialization logic decoupled from user-facing SDK classes. + * + * Produces output compatible with Python Airflow's DagSerialization format (version 3). + */ +typealias Serialized = Map + +private object SerdeScope + +private val logger = Logger(SerdeScope::class) + +// --------------------------------------------------------------------------- +// Value encoding — matches Python's BaseSerialization.serialize +// --------------------------------------------------------------------------- + +/** + * Recursively serialize a value with Airflow's type/var encoding. + * + * Primitives pass through; complex types are wrapped in {"__type": ..., "__var": ...}. + * This matches the Python BaseSerialization.serialize() output exactly: + * - dict -> {"__type": "dict", "__var": {k: serialize(v), ...}} + * - set -> {"__type": "set", "__var": [sorted items]} + * - datetime -> {"__type": "datetime", "__var": epoch_seconds} + * - timedelta -> {"__type": "timedelta", "__var": total_seconds} + */ +internal fun serializeValue(value: Any?): Any? = + when (value) { + null -> null + is String, is Boolean, is Int, is Long, is Float, is Double -> value + is Instant -> + mapOf( + "__type" to "datetime", + "__var" to (value.epochSecond.toDouble() + value.nano.toDouble() / 1_000_000_000.0), + ) + is Duration -> + mapOf( + "__type" to "timedelta", + "__var" to (value.toMillis().toDouble() / 1000.0), + ) + is Map<*, *> -> + mapOf( + "__type" to "dict", + "__var" to value.entries.associate { (k, v) -> k.toString() to serializeValue(v) }, + ) + is Set<*> -> { + val items = value.map { serializeValue(it) } + mapOf( + "__type" to "set", + "__var" to + try { + items.sortedBy { it?.toString() ?: "" } + } catch (_: Exception) { + items + }, + ) + } + is List<*> -> value.map { serializeValue(it) } + else -> value.toString() + } + +/** + * Unwrap a single level of type encoding, extracting the "__var" part. + * + * In Python's serialize_to_json, non-decorated fields are serialized then unwrapped: + * value = cls.serialize(value) + * if isinstance(value, dict) and Encoding.TYPE in value: + * value = value[Encoding.VAR] + */ +private fun unwrapTypeEncoding(value: Any?): Any? = + if (value is Map<*, *> && "__type" in value && "__var" in value) { + value["__var"] + } else { + value + } + +// --------------------------------------------------------------------------- +// Timetable serialization +// --------------------------------------------------------------------------- + +private fun serializeTimetable(schedule: String?): Serialized = + when (schedule) { + null -> + mapOf( + "__type" to "airflow.timetables.simple.NullTimetable", + "__var" to emptyMap(), + ) + "@once" -> + mapOf( + "__type" to "airflow.timetables.simple.OnceTimetable", + "__var" to emptyMap(), + ) + "@continuous" -> + mapOf( + "__type" to "airflow.timetables.simple.ContinuousTimetable", + "__var" to emptyMap(), + ) + else -> + mapOf( + "__type" to "airflow.timetables.trigger.CronTriggerTimetable", + "__var" to + mapOf( + "expression" to schedule, + "timezone" to "UTC", + "interval" to 0.0, + "run_immediately" to false, + ), + ) + } + +// --------------------------------------------------------------------------- +// Task serialization +// --------------------------------------------------------------------------- + +private fun Class.serialize( + id: String, + dependants: Collection?, +): Serialized { + val data = + mutableMapOf( + "task_id" to id, + "task_type" to simpleName, + "_task_module" to name.substringBeforeLast('.'), + "language" to "java", + ) + if (!dependants.isNullOrEmpty()) { + data["downstream_task_ids"] = dependants.sorted() + } + return mapOf("__type" to "operator", "__var" to data) +} + +// --------------------------------------------------------------------------- +// Task group serialization (flat root group from task list) +// --------------------------------------------------------------------------- + +private fun serializeTaskGroup(taskIds: Collection): Serialized = + mapOf( + "_group_id" to null, + "group_display_name" to "", + "prefix_group_id" to true, + "tooltip" to "", + "ui_color" to "CornflowerBlue", + "ui_fgcolor" to "#000", + "children" to taskIds.associateWith { listOf("operator", it) }, + "upstream_group_ids" to emptyList(), + "downstream_group_ids" to emptyList(), + "upstream_task_ids" to emptyList(), + "downstream_task_ids" to emptyList(), + ) + +// --------------------------------------------------------------------------- +// Params serialization +// --------------------------------------------------------------------------- + +private fun serializeParams(params: Map): List> = + params.entries.map { (k, v) -> + listOf( + k, + mapOf( + "__class" to "airflow.sdk.definitions.param.Param", + "default" to serializeValue(v), + "description" to null, + "schema" to serializeValue(emptyMap()), + "source" to null, + ), + ) + } + +// --------------------------------------------------------------------------- +// DAG serialization — matches Python's DagSerialization.serialize_dag +// --------------------------------------------------------------------------- + +private fun Dag.serialize( + id: String, + fileloc: String, + relativeFileloc: String, +): Serialized { + val result = + mutableMapOf( + // Required fields (always present) + "dag_id" to id, + "fileloc" to fileloc, + "relative_fileloc" to relativeFileloc, + // Always serialized + "timezone" to "UTC", + "timetable" to serializeTimetable(schedule), + "tasks" to tasks.entries.map { (taskId, task) -> task.serialize(taskId, dependants[taskId]) }, + "dag_dependencies" to emptyList(), + "task_group" to serializeTaskGroup(tasks.keys), + "edge_info" to emptyMap(), + "params" to (params?.let { serializeParams(it) } ?: emptyList()), + "deadline" to null, + "allowed_run_types" to null, + ) + + // Optional fields — only include if non-null. + // Non-decorated fields are serialized then unwrapped (matching Python's serialize_to_json). + description?.let { result["description"] = it } + startDate?.let { result["start_date"] = unwrapTypeEncoding(serializeValue(it)) } + endDate?.let { result["end_date"] = unwrapTypeEncoding(serializeValue(it)) } + dagrunTimeout?.let { result["dagrun_timeout"] = unwrapTypeEncoding(serializeValue(it)) } + docMd?.let { result["doc_md"] = it } + isPausedUponCreation?.let { result["is_paused_upon_creation"] = it } + + // Decorated fields (kept with __type/__var encoding, NOT unwrapped) + if (defaultArgs.isNotEmpty()) { + result["default_args"] = serializeValue(defaultArgs) + } + if (accessControl != null) { + // access_control is always serialized when not null, even if empty + result["access_control"] = serializeValue(accessControl) + } + + // Fields excluded when matching schema defaults + if (catchup) result["catchup"] = true + if (failFast) result["fail_fast"] = true + if (maxActiveTasks != Dag.DEFAULT_MAX_ACTIVE_TASKS) result["max_active_tasks"] = maxActiveTasks + if (maxActiveRuns != Dag.DEFAULT_MAX_ACTIVE_RUNS) result["max_active_runs"] = maxActiveRuns + if (maxConsecutiveFailedDagRuns != Dag.DEFAULT_MAX_CONSECUTIVE_FAILED_DAG_RUNS) { + result["max_consecutive_failed_dag_runs"] = maxConsecutiveFailedDagRuns + } + if (renderTemplateAsNativeObj) result["render_template_as_native_obj"] = true + + // dag_display_name — excluded when it equals dag_id (the default) + if (dagDisplayName != null && dagDisplayName != id) { + result["dag_display_name"] = dagDisplayName + } + + // Collection fields — serialized then unwrapped; excluded when empty + if (tags.isNotEmpty()) result["tags"] = unwrapTypeEncoding(serializeValue(tags)) + if (ownerLinks.isNotEmpty()) result["owner_links"] = unwrapTypeEncoding(serializeValue(ownerLinks)) + + return result +} + +/** Serialize a single DAG to a dict. Exposed for cross-language validation testing. */ +internal fun serializeDag(dag: Dag): Serialized = dag.serialize(dag.dagId, "", "") + +// --------------------------------------------------------------------------- +// Top-level envelope — matches Python's DagSerialization.to_dict +// --------------------------------------------------------------------------- + +private fun computeRelativeFileloc( + fileloc: String, + bundlePath: String, +): String { + if (fileloc.isEmpty()) return "" + if (bundlePath.isEmpty()) return "." + val rel = Path.of(bundlePath).relativize(Path.of(fileloc)).toString() + return rel.ifEmpty { "." } +} + +internal fun DagParsingResult.serialize(): Serialized { + val relativeFileloc = computeRelativeFileloc(fileloc, bundlePath) + val result = + mapOf( + "type" to "DagFileParsingResult", + "fileloc" to fileloc, + "serialized_dags" to + dags.entries.map { (id, d) -> + mapOf("data" to mapOf("__version" to 3, "dag" to d.serialize(id, fileloc, relativeFileloc))) + }, + ) + + logger.debug("Serialized DAG parsing result", mapOf("result" to result)) + + return result +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Supervisor.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Supervisor.kt new file mode 100644 index 0000000000000..190eb1706b7e2 --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Supervisor.kt @@ -0,0 +1,394 @@ +package org.apache.airflow.sdk.execution + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.apache.airflow.sdk.ensureTrailingSlash +import org.apache.airflow.sdk.execution.api.client.ApiClient +import org.apache.airflow.sdk.execution.api.model.TIEnterRunningPayload +import org.apache.airflow.sdk.execution.api.model.TIRunContext +import org.apache.airflow.sdk.execution.api.model.TISuccessStatePayload +import org.apache.airflow.sdk.execution.api.model.TITerminalStatePayload +import org.apache.airflow.sdk.execution.api.model.TerminalStateNonSuccess +import org.apache.airflow.sdk.execution.api.route.TaskInstancesApi +import org.apache.airflow.sdk.execution.api.route.XComsApi +import retrofit2.Call +import java.io.InputStream +import java.io.OutputStream +import java.net.InetAddress +import java.net.ServerSocket +import java.net.Socket +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.UUID +import org.apache.airflow.sdk.execution.api.model.BundleInfo as ExecutionBundleInfo +import org.apache.airflow.sdk.execution.api.model.TaskInstance as ExecutionTaskInstance +import org.apache.airflow.sdk.execution.api.model.TaskInstanceState as ExecutionTaskInstanceState + +data class SupervisorTaskInstance( + val id: UUID, + val taskId: String, + val dagId: String, + val runId: String, + val tryNumber: Int, + val dagVersionId: UUID, + val mapIndex: Int?, + val contextCarrier: Map? = null, +) + +data class SupervisorBundleInfo( + val name: String, + val version: String?, +) + +data class SupervisorRequest( + val mainClass: String, + val classpath: String, + val executionApiBaseUrl: String, + val token: String, + val workerName: String, + val userName: String, + val dagRelPath: String, + val bundleInfo: SupervisorBundleInfo, + val taskInstance: SupervisorTaskInstance, + val sentryIntegration: String = "", + val onLogLine: suspend (String) -> Unit = {}, +) + +data class SupervisorResult( + val finalState: ExecutionTaskInstanceState, + val exitCode: Int, +) + +/** + * Retrofit interface for reporting task instance terminal state to the Execution API. + * + * Mirrors the Python SDK's `TaskInstanceOperations.succeed()` and `.finish()` methods + * (see `airflow/sdk/api/client.py`), both of which call `PATCH /task-instances/{id}/state` + * with [TISuccessStatePayload] or [TITerminalStatePayload] respectively. + * + * Why not use the generated [TaskInstancesApi.tiUpdateState]? + * The OpenAPI code generator flattens the endpoint's `oneOf` discriminated union into a single + * class [org.apache.airflow.sdk.execution.api.model.TiPatchPayload] whose `StateEnum` only + * contains `UP_FOR_RETRY`. It cannot represent `"success"`, `"failed"`, or `"skipped"`, and its + * method signature does not accept [TISuccessStatePayload] or [TITerminalStatePayload]. + * This interface works around that limitation by binding the same endpoint with the correct + * payload types that the generator *did* produce correctly as standalone classes. + */ +private interface TaskInstanceStateApi { + @retrofit2.http.Headers("Content-Type:application/json") + @retrofit2.http.PATCH("task-instances/{task_instance_id}/state") + fun succeed( + @retrofit2.http.Path("task_instance_id") id: UUID, + @retrofit2.http.Body payload: TISuccessStatePayload, + @retrofit2.http.Header("Airflow-API-Version") version: LocalDate?, + ): Call + + @retrofit2.http.Headers("Content-Type:application/json") + @retrofit2.http.PATCH("task-instances/{task_instance_id}/state") + fun finish( + @retrofit2.http.Path("task_instance_id") id: UUID, + @retrofit2.http.Body payload: TITerminalStatePayload, + @retrofit2.http.Header("Airflow-API-Version") version: LocalDate?, + ): Call +} + +object Supervisor { + private const val CONNECT_TIMEOUT_MS = 15_000 + private val loopback: InetAddress = InetAddress.getByName("127.0.0.1") + + suspend fun run(request: SupervisorRequest): SupervisorResult { + val execApi = executionApiClient(request.executionApiBaseUrl, request.token) + val execClient = HttpExecApiClient(execApi) + val startDate = OffsetDateTime.now() + + return withContext(Dispatchers.IO) { + coroutineScope { + ServerSocket(0, 1, loopback).use { commServer -> + ServerSocket(0, 1, loopback).use { logsServer -> + commServer.soTimeout = CONNECT_TIMEOUT_MS + logsServer.soTimeout = CONNECT_TIMEOUT_MS + + val process = startBundleProcess(request.classpath, request.mainClass, commServer.localPort, logsServer.localPort) + val stdoutPump = + launch(Dispatchers.IO) { + streamLines(process.inputStream, request.onLogLine) + } + val stderrPump = + launch(Dispatchers.IO) { + streamLines(process.errorStream, request.onLogLine) + } + try { + val (commSocket, logsSocket) = acceptConnections(commServer, logsServer) + + commSocket.use { comm -> + logsSocket.use { logs -> + val logPump = + launch(Dispatchers.IO) { + streamLines(logs.getInputStream(), request.onLogLine) + } + + val taskInstance = request.taskInstance.toExecutionTaskInstance(request.workerName) + val tiContext = startTask(execApi, taskInstance, startDate, process, request.workerName, request.userName) + + TaskSdkFrames.writeRequest( + comm.getOutputStream(), + 0, + request.toStartupDetails(taskInstance, tiContext, startDate), + ) + + val finalState = serveTaskSdkRequests(comm, execApi, execClient, taskInstance.id) + val exitCode = process.waitFor() + logPump.join() + stdoutPump.join() + stderrPump.join() + + SupervisorResult( + finalState = if (exitCode == 0) finalState else ExecutionTaskInstanceState.FAILED, + exitCode = exitCode, + ) + } + } + } catch (e: Exception) { + process.destroy() + throw e + } + } + } + } + } + } + + internal suspend fun streamLines( + input: InputStream, + onLogLine: suspend (String) -> Unit, + ) { + withContext(Dispatchers.IO) { + input.bufferedReader().useLines { lines -> + for (line in lines) { + onLogLine(line) + } + } + } + } + + private fun serveTaskSdkRequests( + comm: Socket, + execApi: ApiClient, + execClient: HttpExecApiClient, + taskInstanceId: UUID, + ): ExecutionTaskInstanceState { + val input = comm.getInputStream() + val output = comm.getOutputStream() + + while (true) { + val frame = TaskSdkFrames.readFrame(input, TaskSdkFrames.toSupervisorTypes) + when (val message = frame.body ?: return ExecutionTaskInstanceState.FAILED) { + is GetConnection -> + reply(frame.id, output) { + execClient.getConnection(message.id) + } + is GetVariable -> + reply(frame.id, output) { + execClient.getVariable(message.key) + } + is GetXCom -> + reply(frame.id, output) { + execClient.getXCom( + key = message.key, + dagId = message.dagId, + taskId = message.taskId, + runId = message.runId, + mapIndex = message.mapIndex, + includePriorDates = message.includePriorDates, + ) + } + is SetXCom -> + reply(frame.id, output) { + setXCom(execApi, message) + null + } + is SucceedTask -> { + succeed(execApi, taskInstanceId, message) + return ExecutionTaskInstanceState.SUCCESS + } + is TaskState -> { + finish(execApi, taskInstanceId, message) + return ExecutionTaskInstanceState.fromValue(message.state) + } + is ErrorResponse -> throw IllegalStateException("[${message.error}] ${message.detail}") + else -> throw IllegalStateException("Unsupported Task SDK message type ${message::class.java.name}") + } + } + } + + private fun succeed( + execApi: ApiClient, + taskInstanceId: UUID, + message: SucceedTask, + ) { + execApi.send { + succeed( + taskInstanceId, + TISuccessStatePayload() + .endDate(message.endDate) + .taskOutlets(message.taskOutlets) + .outletEvents(message.outletEvents), + HttpExecApiClient.version, + ) + } + } + + private fun finish( + execApi: ApiClient, + taskInstanceId: UUID, + message: TaskState, + ) { + execApi.send { + finish( + taskInstanceId, + TITerminalStatePayload() + .state(TerminalStateNonSuccess.fromValue(message.state)) + .endDate(message.endDate), + HttpExecApiClient.version, + ) + } + } + + private fun reply( + requestId: Int, + output: OutputStream, + block: () -> Any?, + ) { + try { + TaskSdkFrames.writeResponse(output, requestId, body = block()) + } catch (e: Exception) { + TaskSdkFrames.writeResponse( + output, + requestId, + error = + ErrorResponse().also { + it.error = "generic_error" + it.detail = mapOf("message" to (e.message ?: e::class.java.name)) + }, + ) + } + } + + private suspend fun acceptConnections( + commServer: ServerSocket, + logsServer: ServerSocket, + ): Pair = + coroutineScope { + val comm = async(Dispatchers.IO) { commServer.accept() } + val logs = async(Dispatchers.IO) { logsServer.accept() } + comm.await() to logs.await() + } + + private fun startBundleProcess( + classpath: String, + mainClass: String, + commPort: Int, + logsPort: Int, + ): Process { + val command = + listOf( + "java", + "-classpath", + classpath, + mainClass, + "--comm=${loopback.hostAddress}:$commPort", + "--logs=${loopback.hostAddress}:$logsPort", + ) + return ProcessBuilder(command) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + .start() + } + + private fun executionApiClient( + baseUrl: String, + token: String, + ) = ApiClient("JWTBearer").apply { + setBearerToken(token) + adapterBuilder.baseUrl(baseUrl.ensureTrailingSlash()) + } + + private fun setXCom( + execApi: ApiClient, + request: SetXCom, + ) { + execApi.send { + setXcom( + request.dagId, + request.runId, + request.taskId, + request.key, + request.mapIndex, + null, + HttpExecApiClient.version, + request.value, + ) + } + } + + private fun startTask( + api: ApiClient, + taskInstance: ExecutionTaskInstance, + startDate: OffsetDateTime, + process: Process, + workerName: String, + userName: String, + ): TIRunContext = + api.communicate { + tiRun( + taskInstance.id, + TIEnterRunningPayload() + .hostname(workerName) + .unixname(userName) + .pid(process.pid().toInt()) + .startDate(startDate), + HttpExecApiClient.version, + ) + } + + private fun SupervisorTaskInstance.toExecutionTaskInstance(workerName: String) = + ExecutionTaskInstance().also { + it.id = id + it.taskId = taskId + it.dagId = dagId + it.runId = runId + it.tryNumber = tryNumber + it.dagVersionId = dagVersionId + it.mapIndex = mapIndex + it.hostname = workerName + it.contextCarrier = contextCarrier + } + + private fun SupervisorRequest.toStartupDetails( + taskInstance: ExecutionTaskInstance, + tiContext: TIRunContext, + startDate: OffsetDateTime, + ) = StartupDetails().also { + it.ti = taskInstance + it.dagRelPath = dagRelPath + it.bundleInfo = + ExecutionBundleInfo().also { info -> + info.name = bundleInfo.name + info.version = bundleInfo.version + } + it.tiContext = tiContext + it.startDate = startDate + it.sentryIntegration = sentryIntegration + } +} + +private inline fun ApiClient.send(block: Q.() -> Call<*>) { + val service = createService(Q::class.java) + val response = block(service).execute() + if (!response.isSuccessful) { + throw IllegalStateException("[${response.message()}] $response (from $service)") + } +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/TaskRunner.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/TaskRunner.kt new file mode 100644 index 0000000000000..ed61d57149923 --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/TaskRunner.kt @@ -0,0 +1,28 @@ +package org.apache.airflow.sdk.execution + +import org.apache.airflow.sdk.Bundle +import org.apache.airflow.sdk.Client + +object TaskRunner { + fun run( + bundle: Bundle, + request: StartupDetails, + comm: CoordinatorComm, + ): Any = run(bundle, request, Client(request, CoordinatorClient(comm))) + + internal fun run( + bundle: Bundle, + request: StartupDetails, + client: Client, + ): Any { + val task = bundle.dags[request.ti.dagId]?.tasks[request.ti.taskId] ?: return TaskState("removed") + val instance = task.getDeclaredConstructor().newInstance() + return try { + instance.execute(client) + SucceedTask() + } catch (e: Exception) { + e.printStackTrace() + TaskState("failed") + } + } +} diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/TaskSdkFrames.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/TaskSdkFrames.kt new file mode 100644 index 0000000000000..76e666dbb23f5 --- /dev/null +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/TaskSdkFrames.kt @@ -0,0 +1,235 @@ +package org.apache.airflow.sdk.execution + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.util.StdDateFormat +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import org.apache.airflow.sdk.execution.api.model.ConnectionResponse +import org.apache.airflow.sdk.execution.api.model.VariableResponse +import org.apache.airflow.sdk.execution.api.model.XComResponse +import org.msgpack.core.MessagePack +import java.io.ByteArrayOutputStream +import java.io.EOFException +import java.io.InputStream +import java.io.OutputStream + +typealias TaskSdkMessageDecoder = (Map<*, *>) -> Any + +object TaskSdkFrames { + private val mapper = + ObjectMapper().apply { + configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + registerModule(JavaTimeModule()) + registerModule(TimestampToJavaOffsetDateTimeModule()) + setDateFormat(StdDateFormat().withColonInTimeZone(true)) + } + + private val inferredTypes = + mapOf( + ConnectionResponse::class to "ConnectionResult", + ErrorResponse::class to "ErrorResponse", + StartupDetails::class to "StartupDetails", + VariableResponse::class to "VariableResult", + XComResponse::class to "XComResult", + ) + + private val toBundleClientTypes: Map = + mapOf( + "ConnectionResult" to mapperDecoder(ConnectionResponse::class.java), + "ErrorResponse" to mapperDecoder(ErrorResponse::class.java), + "VariableResult" to mapperDecoder(VariableResponse::class.java), + "XComResult" to mapperDecoder(XComResponse::class.java), + ) + + val toDagProcessorTypes: Map = + toBundleClientTypes + + mapOf( + "DagFileParseRequest" to mapperDecoder(DagFileParseRequest::class.java), + ) + + val toTaskTypes: Map = + toBundleClientTypes + + mapOf( + "StartupDetails" to mapperDecoder(StartupDetails::class.java), + ) + + // The Java bundle process can act as either Python's DagProcessor or Task runtime, so + // its inbound decoder is the union of both message sets. + val toBundleProcessTypes: Map = toDagProcessorTypes + toTaskTypes + + val toSupervisorTypes: Map = + mapOf( + "ErrorResponse" to mapperDecoder(ErrorResponse::class.java), + "GetConnection" to { body -> GetConnection(id = body.string("conn_id")) }, + "GetVariable" to { body -> GetVariable(key = body.string("key")) }, + "GetXCom" to { + GetXCom( + key = it.string("key"), + dagId = it.string("dag_id"), + taskId = it.string("task_id"), + runId = it.string("run_id"), + mapIndex = it.intOrNull("map_index"), + includePriorDates = it.boolean("include_prior_dates", default = false), + ) + }, + "SetXCom" to { + SetXCom( + key = it.string("key"), + value = it["value"] ?: error("Missing 'value'"), + dagId = it.string("dag_id"), + taskId = it.string("task_id"), + runId = it.string("run_id"), + mapIndex = it.int("map_index"), + ) + }, + "SucceedTask" to { SucceedTask() }, + "TaskState" to { body -> TaskState(state = body.string("state")) }, + ) + + fun encodeRequest( + id: Int, + body: Any, + ): ByteArray = encodeFrame(id, body, error = null, isResponse = false) + + fun encodeResponse( + id: Int, + body: Any? = null, + error: ErrorResponse? = null, + ): ByteArray = encodeFrame(id, body, error = error, isResponse = true) + + fun writeRequest( + output: OutputStream, + id: Int, + body: Any, + ) = writeFrame(output, encodeRequest(id, body)) + + fun writeResponse( + output: OutputStream, + id: Int, + body: Any? = null, + error: ErrorResponse? = null, + ) = writeFrame(output, encodeResponse(id, body, error)) + + fun decode( + bytes: ByteArray, + bodyTypes: Map, + ): IncomingFrame { + val unpacker = MessagePack.newDefaultUnpacker(bytes) + val headerSize = unpacker.unpackArrayHeader() + check(headerSize >= 2) { "Unexpected Task SDK frame arity $headerSize" } + + val id = unpacker.unpackInt() + val rawBody = unpacker.unpackAny() + val rawError = if (headerSize >= 3) unpacker.unpackAny() else null + unpacker.close() + + val body = + decodeMessage(rawError, bodyTypes = mapOf("ErrorResponse" to mapperDecoder(ErrorResponse::class.java))) + ?: decodeMessage(rawBody, bodyTypes) + + return IncomingFrame(id, body) + } + + fun readFrame( + input: InputStream, + bodyTypes: Map, + ): IncomingFrame = decode(readBytes(input, readLengthPrefix(input)), bodyTypes) + + fun lengthPrefix(length: Int) = + byteArrayOf( + (length shr 24).toByte(), + (length shr 16).toByte(), + (length shr 8).toByte(), + length.toByte(), + ) + + fun readLengthPrefix(input: InputStream): Int = parseLengthPrefix(readBytes(input, 4)) + + fun parseLengthPrefix(prefix: ByteArray): Int { + check(prefix.size == 4) { "Need 4 prefix bytes" } + return prefix.fold(0) { acc, byte -> (acc shl 8) or (byte.toInt() and 0xff) } + } + + fun readBytes( + input: InputStream, + length: Int, + ): ByteArray { + val bytes = input.readNBytes(length) + if (bytes.size != length) { + throw EOFException("Expected $length bytes but only received ${bytes.size}") + } + return bytes + } + + private fun writeFrame( + output: OutputStream, + payload: ByteArray, + ) { + output.write(lengthPrefix(payload.size)) + output.write(payload) + output.flush() + } + + private fun encodeFrame( + id: Int, + body: Any?, + error: ErrorResponse?, + isResponse: Boolean, + ): ByteArray { + val payload = ByteArrayOutputStream() + val packer = MessagePack.newDefaultPacker(payload) + packer.packArrayHeader(if (isResponse) 3 else 2) + packer.packInt(id) + packer.packAny(body?.let(::toBody)) + if (isResponse) { + packer.packAny(error?.let(::toBody)) + } + packer.close() + return payload.toByteArray() + } + + private fun decodeMessage( + raw: Any?, + bodyTypes: Map, + ): Any? { + val body = raw as? Map<*, *> ?: return raw + val typeName = body["type"] as? String ?: return body + val decoder = bodyTypes[typeName] ?: error("Unsupported Task SDK message type $typeName") + return decoder(body) + } + + private fun mapperDecoder(targetType: Class<*>): TaskSdkMessageDecoder = { body -> mapper.convertValue(body, targetType) } + + @Suppress("UNCHECKED_CAST") + private fun toBody(value: Any): Map = + when (value) { + is Map<*, *> -> value as Map + else -> + (mapper.convertValue(value, MutableMap::class.java) as MutableMap).also { body -> + inferredTypes[value::class]?.let { typeName -> body.putIfAbsent("type", typeName) } + } + } + + private fun Map<*, *>.string(key: String): String = this[key] as? String ?: error("Missing '$key'") + + private fun Map<*, *>.int(key: String): Int = intOrNull(key) ?: error("Missing integer '$key'") + + private fun Map<*, *>.intOrNull(key: String): Int? = + when (val value = this[key]) { + null -> null + is Number -> value.toInt() + else -> error("Expected integer '$key', got ${value::class.java}") + } + + private fun Map<*, *>.boolean( + key: String, + default: Boolean, + ): Boolean = + when (val value = this[key]) { + null -> default + is Boolean -> value + else -> error("Expected boolean '$key', got ${value::class.java}") + } +} diff --git a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BundleScannerTest.kt b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BundleScannerTest.kt new file mode 100644 index 0000000000000..8bdacd8ce4e79 --- /dev/null +++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BundleScannerTest.kt @@ -0,0 +1,197 @@ +package org.apache.airflow.sdk + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.util.jar.JarOutputStream +import java.util.jar.Manifest +import java.util.zip.ZipEntry + +private const val STUB_MAIN_CLASS = "com.example.Main" + +class BundleScannerTest { + @Test + @DisplayName("parseDagIdsFromYaml extracts dag ids from metadata YAML") + fun parseDagIdsFromYaml() { + val yaml = + """ + dags: + java_example: + tasks: + - extract + - transform + - load + another_dag: + tasks: + - task_a + """.trimIndent() + + assertEquals(setOf("java_example", "another_dag"), parseDagIdsFromYaml(yaml)) + } + + @Test + @DisplayName("parseDagIdsFromYaml returns empty set for missing dags key") + fun parseDagIdsFromYamlEmpty() { + assertEquals(emptySet(), parseDagIdsFromYaml("other_key: value")) + } + + @Test + @DisplayName("readBundleDagIds reads metadata from JAR with Airflow-Java-SDK-Metadata manifest") + fun readBundleDagIdsFromJar( + @TempDir tempDir: Path, + ) { + createBundleJar(tempDir, mapOf("my_dag" to listOf("t1", "t2"))) + + assertEquals(setOf("my_dag"), readBundleDagIds(tempDir)) + } + + @Test + @DisplayName("readBundleDagIds returns empty set when no JAR has metadata") + fun readBundleDagIdsNoMetadata( + @TempDir tempDir: Path, + ) { + val manifest = Manifest() + manifest.mainAttributes.putValue("Manifest-Version", "1.0") + JarOutputStream(Files.newOutputStream(tempDir.resolve("plain.jar")), manifest).use {} + + assertEquals(emptySet(), readBundleDagIds(tempDir)) + } + + @Test + @DisplayName("scanBundles discovers bundles in subdirectories") + fun scanBundlesNestedLayout( + @TempDir tempDir: Path, + ) { + val bundleA = Files.createDirectory(tempDir.resolve("bundle-a")) + createBundleJar(bundleA, mapOf("dag_a" to listOf("t1"))) + + val bundleB = Files.createDirectory(tempDir.resolve("bundle-b")) + createBundleJar(bundleB, mapOf("dag_b" to listOf("t2"), "dag_c" to listOf("t3"))) + + val result = scanBundles(tempDir) + + assertEquals(STUB_MAIN_CLASS, result["dag_a"]?.mainClass) + assertEquals(STUB_MAIN_CLASS, result["dag_b"]?.mainClass) + assertEquals(STUB_MAIN_CLASS, result["dag_c"]?.mainClass) + // dag_a classpath should point to bundle-a JARs, not bundle-b + assertTrue(result["dag_a"]!!.classpath.contains("bundle-a")) + assertTrue(result["dag_b"]!!.classpath.contains("bundle-b")) + } + + @Test + @DisplayName("scanBundles supports flat layout where bundlesDir itself contains JARs") + fun scanBundlesFlatLayout( + @TempDir tempDir: Path, + ) { + createBundleJar(tempDir, mapOf("flat_dag" to listOf("t1"))) + + val result = scanBundles(tempDir) + + assertNotNull(result["flat_dag"]) + assertEquals(STUB_MAIN_CLASS, result["flat_dag"]!!.mainClass) + } + + @Test + @DisplayName("scanBundles finds metadata JAR among many dependency JARs") + fun scanBundlesFlatWithDependencyJars( + @TempDir tempDir: Path, + ) { + // Simulate installDist layout: one bundle JAR with metadata among plain dependency JARs. + val plainManifest = Manifest() + plainManifest.mainAttributes.putValue("Manifest-Version", "1.0") + JarOutputStream(Files.newOutputStream(tempDir.resolve("aaa-dep.jar")), plainManifest).use {} + JarOutputStream(Files.newOutputStream(tempDir.resolve("zzz-dep.jar")), plainManifest).use {} + + // A JAR with no manifest at all. + JarOutputStream(Files.newOutputStream(tempDir.resolve("no-manifest.jar"))).use {} + + createBundleJar(tempDir, mapOf("my_dag" to listOf("t1"))) + + val result = scanBundles(tempDir) + + assertNotNull(result["my_dag"]) + assertEquals(STUB_MAIN_CLASS, result["my_dag"]!!.mainClass) + // All 4 JARs should be on the classpath. + val cpEntries = result["my_dag"]!!.classpath.split(File.pathSeparator) + assertEquals(4, cpEntries.size) + } + + @Test + @DisplayName("scanBundles resolves distZip layout where bundlesDir is the lib directory") + fun scanBundlesDistZipLibDir( + @TempDir tempDir: Path, + ) { + // Simulate: unzip example.zip → example/lib/*.jar, BUNDLES_DIR=.../example/lib + val libDir = Files.createDirectories(tempDir.resolve("example").resolve("lib")) + + // 30 plain dependency JARs + val plainManifest = Manifest() + plainManifest.mainAttributes.putValue("Manifest-Version", "1.0") + for (name in listOf( + "annotations-23.0.0", + "converter-jackson-3.0.0", + "jackson-core-2.21.1", + "jackson-databind-2.21.1", + "kotlin-stdlib-2.3.0", + "kotlinx-coroutines-core-jvm-1.10.2", + "msgpack-core-0.9.11", + "okhttp-4.12.0", + "retrofit-3.0.0", + "sdk", + )) { + JarOutputStream(Files.newOutputStream(libDir.resolve("$name.jar")), plainManifest).use {} + } + + // The bundle JAR with metadata, named "example.jar" (alphabetically after some deps) + createBundleJar(libDir, mapOf("java_example" to listOf("extract", "transform", "load")), "example.jar") + + // bundlesDir points directly at lib/ + val result = scanBundles(libDir) + + assertNotNull(result["java_example"], "java_example should be discovered in flat lib/ layout") + assertEquals(STUB_MAIN_CLASS, result["java_example"]!!.mainClass) + assertEquals(11, result["java_example"]!!.classpath.split(File.pathSeparator).size) + } + + @Test + @DisplayName("scanBundles returns empty map for nonexistent directory") + fun scanBundlesNonexistentDir() { + assertEquals(emptyMap(), scanBundles(Path.of("/nonexistent/dir"))) + } + + private fun createBundleJar( + dir: Path, + dags: Map>, + fileName: String = "bundle.jar", + ): Path { + val manifest = Manifest() + manifest.mainAttributes.putValue("Manifest-Version", "1.0") + manifest.mainAttributes.putValue("Main-Class", STUB_MAIN_CLASS) + manifest.mainAttributes.putValue(METADATA_MANIFEST_KEY, "airflow-metadata.yaml") + + val jarPath = dir.resolve(fileName) + JarOutputStream(Files.newOutputStream(jarPath), manifest).use { jos -> + jos.putNextEntry(ZipEntry("airflow-metadata.yaml")) + val yaml = + buildString { + appendLine("dags:") + for ((dagId, tasks) in dags) { + appendLine(" $dagId:") + appendLine(" tasks:") + for (task in tasks) { + appendLine(" - $task") + } + } + } + jos.write(yaml.toByteArray()) + jos.closeEntry() + } + return jarPath + } +} diff --git a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BundleTest.kt b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BundleTest.kt new file mode 100644 index 0000000000000..785394185e7cd --- /dev/null +++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BundleTest.kt @@ -0,0 +1,28 @@ +package org.apache.airflow.sdk + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +internal class BundleTest { + @Test + @DisplayName("Should index dags by dagId") + fun shouldIndexDagsByDagId() { + val dag = Dag("dag") + + val bundle = Bundle("0", listOf(dag)) + + Assertions.assertEquals(mapOf("dag" to dag), bundle.dags) + } + + @Test + @DisplayName("Should reject duplicate dag ids") + fun shouldRejectDuplicateDagIds() { + val error = + Assertions.assertThrows(IllegalArgumentException::class.java) { + Bundle("0", listOf(Dag("dag"), Dag("dag"))) + } + + Assertions.assertEquals("Duplicate dagId in bundle: dag", error.message) + } +} diff --git a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/ConfigTest.kt b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/ConfigTest.kt new file mode 100644 index 0000000000000..6ccdeef500e42 --- /dev/null +++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/ConfigTest.kt @@ -0,0 +1,138 @@ +package org.apache.airflow.sdk + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Files +import java.nio.file.Path + +class ConfigTest { + // -- SdkConfig: env var resolution -- + + @Test + @DisplayName("executionApiUrl uses AIRFLOW__CORE__EXECUTION_API_SERVER_URL env var") + fun executionApiUrlFromEnv() { + val config = SdkConfig(env = mapOf("AIRFLOW__CORE__EXECUTION_API_SERVER_URL" to "http://127.0.0.1:8080/execution")) + + assertEquals("http://127.0.0.1:8080/execution/", config.executionApiUrl) + } + + @Test + @DisplayName("executionApiUrl throws when missing") + fun executionApiUrlThrowsWhenMissing() { + val config = SdkConfig(env = emptyMap()) + + val error = assertThrows(WorkerError::class.java) { config.executionApiUrl } + assertTrue(error.message!!.contains("execution_api_server_url")) + } + + @Test + @DisplayName("executionApiUrl falls back to execution.api_url") + fun executionApiUrlFallback() { + val config = SdkConfig(env = mapOf("AIRFLOW__EXECUTION__API_URL" to "http://127.0.0.1:8080/execution")) + + assertEquals("http://127.0.0.1:8080/execution/", config.executionApiUrl) + } + + @Test + @DisplayName("jwtExpirationTime defaults to 30 seconds") + fun jwtExpirationTimeDefault() { + val config = SdkConfig(env = emptyMap()) + + assertEquals(30, config.jwtExpirationTime) + } + + // -- SdkConfig: YAML resolution -- + + @Test + @DisplayName("config values are loaded from YAML file") + fun yamlConfigLoading( + @TempDir tempDir: Path, + ) { + val yamlContent = + """ + core: + execution_api_server_url: "http://yaml-host:8080/execution/" + + sdk: + bundles_dir: "./bundles" + + api_auth: + jwt_secret: "yaml-secret" + jwt_issuer: "yaml-issuer" + jwt_expiration_time: 45 + """.trimIndent() + + val yamlPath = tempDir.resolve("java-sdk.yaml") + Files.writeString(yamlPath, yamlContent) + + val config = SdkConfig(env = emptyMap(), yamlOverride = yamlPath) + + assertEquals("http://yaml-host:8080/execution/", config.executionApiUrl) + assertEquals("yaml-secret", config.jwtSecret) + assertEquals("yaml-issuer", config.jwtIssuer) + assertEquals(45, config.jwtExpirationTime) + assertEquals(Path.of("./bundles"), config.bundlesDir) + } + + @Test + @DisplayName("env vars take precedence over YAML values") + fun envTakesPrecedenceOverYaml( + @TempDir tempDir: Path, + ) { + val yamlContent = + """ + core: + execution_api_server_url: "http://yaml-host:8080/execution/" + api_auth: + jwt_secret: "yaml-secret" + """.trimIndent() + + val yamlPath = tempDir.resolve("java-sdk.yaml") + Files.writeString(yamlPath, yamlContent) + + val config = + SdkConfig( + env = + mapOf( + "AIRFLOW__CORE__EXECUTION_API_SERVER_URL" to "http://env-host:9090/execution/", + "AIRFLOW__API_AUTH__JWT_SECRET" to "env-secret", + ), + yamlOverride = yamlPath, + ) + + assertEquals("http://env-host:9090/execution/", config.executionApiUrl) + assertEquals("env-secret", config.jwtSecret) + } + + @Test + @DisplayName("config works with no YAML file and no env vars for optional values") + fun noYamlFile() { + val config = SdkConfig(env = emptyMap()) + + assertEquals(30, config.jwtExpirationTime) + assertNull(config.bundlesDir) + } + + @Test + @DisplayName("YAML file is resolved from AIRFLOW_HOME") + fun yamlFromAirflowHome( + @TempDir tempDir: Path, + ) { + val yamlContent = + """ + api_auth: + jwt_secret: "home-secret" + """.trimIndent() + + Files.writeString(tempDir.resolve("java-sdk.yaml"), yamlContent) + + val config = SdkConfig(env = mapOf("AIRFLOW_HOME" to tempDir.toString())) + + assertEquals("home-secret", config.jwtSecret) + } +} diff --git a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/CoordinatorCommTest.kt b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/CoordinatorCommTest.kt new file mode 100644 index 0000000000000..d53f8f09c795d --- /dev/null +++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/CoordinatorCommTest.kt @@ -0,0 +1,113 @@ +package org.apache.airflow.sdk + +import io.ktor.utils.io.ByteChannel +import io.ktor.utils.io.availableForRead +import io.ktor.utils.io.readAvailable +import kotlinx.coroutines.runBlocking +import org.apache.airflow.sdk.execution.CoordinatorComm +import org.apache.airflow.sdk.execution.DagFileParseRequest +import org.apache.airflow.sdk.execution.IncomingFrame +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.text.split + +fun byteArrayFromHexString(hexString: String): ByteArray = + hexString + .split(' ', '\r', '\n') + .filter { it.isNotEmpty() } + .map { it.toUByte(16).toByte() } + .toByteArray() + +@OptIn(ExperimentalUnsignedTypes::class) +internal class CoordinatorCommTest { + lateinit var comm: CoordinatorComm + lateinit var reader: ByteChannel + lateinit var writer: ByteChannel + + @BeforeEach + fun setUp() { + reader = ByteChannel(autoFlush = true) + writer = ByteChannel(autoFlush = true) + comm = CoordinatorComm(Bundle("0", listOf(Dag("dag"))), reader, writer) + } + + @Test + @DisplayName("handleIncoming should produce parse result") + fun handleIncomingShouldProduceParseResult() { + val frame = IncomingFrame(0, DagFileParseRequest().apply { file = ":memory:" }) + + // prefix + DagFileParsingResult payload for a minimal DAG. + + /* prefix + + [ + 0, + { + "type": "DagFileParsingResult", + "fileloc": ":memory:", + "serialized_dags": [ + { + "data": { + "__version": 3, + "dag": { + "dag_id": "dag", + "fileloc": ":memory:", + "relative_fileloc": ".", + "timezone": "UTC", + "timetable": { + "__type": "airflow.timetables.simple.NullTimetable", + "__var": {} + }, + "tasks": [] + } + } + } + ] + } + ] + */ + val expected = + byteArrayFromHexString( + """ + | 00 00 01 e6 + | 92 00 83 a4 74 79 70 65 b4 44 61 67 46 69 6c 65 50 61 72 73 69 6e 67 52 + | 65 73 75 6c 74 a7 66 69 6c 65 6c 6f 63 a8 3a 6d 65 6d 6f 72 79 3a af 73 + | 65 72 69 61 6c 69 7a 65 64 5f 64 61 67 73 91 81 a4 64 61 74 61 82 a9 5f + | 5f 76 65 72 73 69 6f 6e 03 a3 64 61 67 8c a6 64 61 67 5f 69 64 a3 64 61 + | 67 a7 66 69 6c 65 6c 6f 63 a8 3a 6d 65 6d 6f 72 79 3a b0 72 65 6c 61 74 + | 69 76 65 5f 66 69 6c 65 6c 6f 63 a1 2e a8 74 69 6d 65 7a 6f 6e 65 a3 55 + | 54 43 a9 74 69 6d 65 74 61 62 6c 65 82 a6 5f 5f 74 79 70 65 d9 27 61 69 + | 72 66 6c 6f 77 2e 74 69 6d 65 74 61 62 6c 65 73 2e 73 69 6d 70 6c 65 2e + | 4e 75 6c 6c 54 69 6d 65 74 61 62 6c 65 a5 5f 5f 76 61 72 80 a5 74 61 73 + | 6b 73 90 b0 64 61 67 5f 64 65 70 65 6e 64 65 6e 63 69 65 73 90 aa 74 61 + | 73 6b 5f 67 72 6f 75 70 8b a9 5f 67 72 6f 75 70 5f 69 64 c0 b2 67 72 6f + | 75 70 5f 64 69 73 70 6c 61 79 5f 6e 61 6d 65 a0 af 70 72 65 66 69 78 5f + | 67 72 6f 75 70 5f 69 64 c3 a7 74 6f 6f 6c 74 69 70 a0 a8 75 69 5f 63 6f + | 6c 6f 72 ae 43 6f 72 6e 66 6c 6f 77 65 72 42 6c 75 65 aa 75 69 5f 66 67 + | 63 6f 6c 6f 72 a4 23 30 30 30 a8 63 68 69 6c 64 72 65 6e 80 b2 75 70 73 + | 74 72 65 61 6d 5f 67 72 6f 75 70 5f 69 64 73 90 b4 64 6f 77 6e 73 74 72 + | 65 61 6d 5f 67 72 6f 75 70 5f 69 64 73 90 b1 75 70 73 74 72 65 61 6d 5f + | 74 61 73 6b 5f 69 64 73 90 b3 64 6f 77 6e 73 74 72 65 61 6d 5f 74 61 73 + | 6b 5f 69 64 73 90 a9 65 64 67 65 5f 69 6e 66 6f 80 a6 70 61 72 61 6d 73 + | 90 a8 64 65 61 64 6c 69 6e 65 c0 b1 61 6c 6c 6f 77 65 64 5f 72 75 6e 5f + | 74 79 70 65 73 c0 + """.trimMargin(), + ) + + val buffer = ByteArray(1024) { 0 } // Change ByteArray size if assertTrue below fails. + var count = 0 + runBlocking { + comm.handleIncoming(frame) + if (writer.availableForRead > 0) { + count = writer.readAvailable(buffer) + } + } + Assertions.assertTrue(count < buffer.size, "Please increase buffer size above") + + Assertions.assertEquals(expected.size, count) + + val received = buffer.sliceArray(0.until(count)) + Assertions.assertArrayEquals(expected, received) + } +} diff --git a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/CommsTest.kt b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/CommsTest.kt new file mode 100644 index 0000000000000..fcca3ebf45f2b --- /dev/null +++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/CommsTest.kt @@ -0,0 +1,86 @@ +package org.apache.airflow.sdk.execution + +import org.apache.airflow.sdk.byteArrayFromHexString +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.time.OffsetDateTime +import java.time.ZoneOffset + +class CommsTest { + @Test + @DisplayName("Should decode startup details") + fun shouldDecodeStartupDetails() { + // [2, msg, null] with msg coming from + // https://github.com/astronomer/airflow/blob/f39c8da8/task-sdk/tests/task_sdk/execution_time/test_comms.py#L73-L108 + val data = + """ + 92 02 88 a4 74 79 70 65 ae 53 74 61 72 74 75 70 44 65 74 61 69 6c 73 a2 74 69 86 a2 69 64 d9 24 + 34 64 38 32 38 61 36 32 2d 61 34 31 37 2d 34 39 33 36 2d 61 37 61 36 2d 32 62 33 66 61 62 61 63 + 65 63 61 62 a7 74 61 73 6b 5f 69 64 a1 61 aa 74 72 79 5f 6e 75 6d 62 65 72 01 a6 72 75 6e 5f 69 + 64 a1 62 a6 64 61 67 5f 69 64 a1 63 ae 64 61 67 5f 76 65 72 73 69 6f 6e 5f 69 64 d9 24 34 64 38 + 32 38 61 36 32 2d 61 34 31 37 2d 34 39 33 36 2d 61 37 61 36 2d 32 62 33 66 61 62 61 63 65 63 61 + 62 aa 74 69 5f 63 6f 6e 74 65 78 74 85 a7 64 61 67 5f 72 75 6e 8c a6 64 61 67 5f 69 64 a1 63 a6 + 72 75 6e 5f 69 64 a1 62 ac 6c 6f 67 69 63 61 6c 5f 64 61 74 65 b4 32 30 32 34 2d 31 32 2d 30 31 + 54 30 31 3a 30 30 3a 30 30 5a b3 64 61 74 61 5f 69 6e 74 65 72 76 61 6c 5f 73 74 61 72 74 b4 32 + 30 32 34 2d 31 32 2d 30 31 54 30 30 3a 30 30 3a 30 30 5a b1 64 61 74 61 5f 69 6e 74 65 72 76 61 + 6c 5f 65 6e 64 b4 32 30 32 34 2d 31 32 2d 30 31 54 30 31 3a 30 30 3a 30 30 5a aa 73 74 61 72 74 + 5f 64 61 74 65 b4 32 30 32 34 2d 31 32 2d 30 31 54 30 31 3a 30 30 3a 30 30 5a a9 72 75 6e 5f 61 + 66 74 65 72 b4 32 30 32 34 2d 31 32 2d 30 31 54 30 31 3a 30 30 3a 30 30 5a a8 65 6e 64 5f 64 61 + 74 65 c0 a8 72 75 6e 5f 74 79 70 65 a6 6d 61 6e 75 61 6c a5 73 74 61 74 65 a7 73 75 63 63 65 73 + 73 a4 63 6f 6e 66 c0 b5 63 6f 6e 73 75 6d 65 64 5f 61 73 73 65 74 5f 65 76 65 6e 74 73 90 a9 6d + 61 78 5f 74 72 69 65 73 00 ac 73 68 6f 75 6c 64 5f 72 65 74 72 79 c2 a9 76 61 72 69 61 62 6c 65 + 73 c0 ab 63 6f 6e 6e 65 63 74 69 6f 6e 73 c0 a4 66 69 6c 65 a9 2f 64 65 76 2f 6e 75 6c 6c aa 73 + 74 61 72 74 5f 64 61 74 65 b4 32 30 32 34 2d 31 32 2d 30 31 54 30 31 3a 30 30 3a 30 30 5a ac 64 + 61 67 5f 72 65 6c 5f 70 61 74 68 a9 2f 64 65 76 2f 6e 75 6c 6c ab 62 75 6e 64 6c 65 5f 69 6e 66 + 6f 82 a4 6e 61 6d 65 a8 61 6e 79 2d 6e 61 6d 65 a7 76 65 72 73 69 6f 6e ab 61 6e 79 2d 76 65 72 + 73 69 6f 6e b2 73 65 6e 74 72 79 5f 69 6e 74 65 67 72 61 74 69 6f 6e a0 c0 + """.trimIndent() + val result = CoordinatorComm.decode(byteArrayFromHexString(data)) + Assertions.assertInstanceOf(IncomingFrame::class.java, result) + Assertions.assertInstanceOf(StartupDetails::class.java, result.body) + } + + @Test + @DisplayName("Should serialize all fields") + fun shouldEncodeSucceedTask() { + val endDate = OffsetDateTime.of(2024, 12, 1, 1, 0, 0, 0, ZoneOffset.UTC) + val bytes = CoordinatorComm.encode(OutgoingFrame(3, SucceedTask(endDate))) + val actual = bytes.toHexString(HexFormat { bytes { byteSeparator = " " } }) + + val expected = + """ + 92 03 86 a5 73 74 61 74 65 a7 73 75 63 63 65 73 73 a8 65 6e 64 5f 64 61 74 65 b4 32 30 32 34 2d + 31 32 2d 30 31 54 30 31 3a 30 30 3a 30 30 5a ac 74 61 73 6b 5f 6f 75 74 6c 65 74 73 90 ad 6f 75 + 74 6c 65 74 5f 65 76 65 6e 74 73 90 b2 72 65 6e 64 65 72 65 64 5f 6d 61 70 5f 69 6e 64 65 78 c0 + a4 74 79 70 65 ab 53 75 63 63 65 65 64 54 61 73 6b + """.trimIndent().replace('\n', ' ') + + Assertions.assertEquals(expected, actual) + } + + @Test + @DisplayName("Should decode requests to the supervisor") + fun shouldDecodeSupervisorRequest() { + val result = TaskSdkFrames.decode(TaskSdkFrames.encodeRequest(5, GetVariable("demo")), TaskSdkFrames.toSupervisorTypes) + + Assertions.assertEquals(5, result.id) + Assertions.assertEquals(GetVariable("demo"), result.body) + } + + @Test + @DisplayName("Should decode protocol errors from the response error slot") + fun shouldDecodeErrorResponseFromErrorSlot() { + val error = + ErrorResponse().also { + it.error = "generic_error" + it.detail = mapOf("message" to "boom") + } + + val result = CoordinatorComm.decode(TaskSdkFrames.encodeResponse(7, error = error)) + + Assertions.assertEquals(7, result.id) + Assertions.assertInstanceOf(ErrorResponse::class.java, result.body) + Assertions.assertEquals("generic_error", (result.body as ErrorResponse).error) + } +} diff --git a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/DagParserTest.kt b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/DagParserTest.kt new file mode 100644 index 0000000000000..8781e437fe268 --- /dev/null +++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/DagParserTest.kt @@ -0,0 +1,28 @@ +package org.apache.airflow.sdk.execution + +import org.apache.airflow.sdk.Bundle +import org.apache.airflow.sdk.Dag +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +internal class DagParserTest { + lateinit var parser: DagParser + + @BeforeEach + fun setUp() { + parser = DagParser(":memory:", "") + } + + @Test + @DisplayName("Should produce serialized dag") + fun shouldProduceSerializedDag() { + val bundle = Bundle("0", listOf(Dag("dag"))) + val result = parser.parse(bundle) + Assertions.assertEquals( + DagParsingResult(":memory:", "", bundle.dags), + result, + ) + } +} diff --git a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/SerializationCompatibilityTest.kt b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/SerializationCompatibilityTest.kt new file mode 100644 index 0000000000000..a2a6118777793 --- /dev/null +++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/SerializationCompatibilityTest.kt @@ -0,0 +1,151 @@ +package org.apache.airflow.sdk.execution + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import org.apache.airflow.sdk.Dag +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import java.io.File +import java.time.Duration +import java.time.Instant + +/** + * Reads test_dags.yaml, constructs Dags from the parameters, serialises each + * one with the Java SDK, and writes the result to serialized_java.json for + * cross-language comparison with the Python output. + * + * Each YAML test-case is turned into a JUnit 5 dynamic test so failures are + * reported individually. + * + * After running: + * python validation/serialization/compare.py \ + * validation/serialization/serialized_python.json \ + * validation/serialization/serialized_java.json + */ +class SerializationCompatibilityTest { + companion object { + private val yamlMapper = ObjectMapper(YAMLFactory()) + private val jsonMapper = + ObjectMapper().apply { + enable(SerializationFeature.INDENT_OUTPUT) + configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) + } + + /** Resolve a project-relative path that works from any Gradle working dir. */ + private fun projectFile(relative: String): File { + // Gradle may run from the repo root or from sdk/ + var dir = File(System.getProperty("user.dir")) + while (dir.parentFile != null) { + val candidate = File(dir, relative) + if (candidate.exists()) return candidate + dir = dir.parentFile + } + // Fallback: try relative to cwd + return File(relative) + } + } + + // ----------------------------------------------------------------------- + // YAML → Dag construction + // ----------------------------------------------------------------------- + + @Suppress("UNCHECKED_CAST") + private fun constructDag(params: Map): Dag { + val dagId = params["dag_id"] as String + + return Dag( + dagId = dagId, + description = params["description"] as? String, + schedule = params["schedule"] as? String, + startDate = (params["start_date"] as? String)?.let { Instant.parse(it) }, + endDate = (params["end_date"] as? String)?.let { Instant.parse(it) }, + defaultArgs = (params["default_args"] as? Map) ?: emptyMap(), + maxActiveTasks = (params["max_active_tasks"] as? Number)?.toInt() ?: Dag.DEFAULT_MAX_ACTIVE_TASKS, + maxActiveRuns = (params["max_active_runs"] as? Number)?.toInt() ?: Dag.DEFAULT_MAX_ACTIVE_RUNS, + maxConsecutiveFailedDagRuns = + (params["max_consecutive_failed_dag_runs"] as? Number)?.toInt() + ?: Dag.DEFAULT_MAX_CONSECUTIVE_FAILED_DAG_RUNS, + dagrunTimeout = (params["dagrun_timeout_seconds"] as? Number)?.let { Duration.ofSeconds(it.toLong()) }, + catchup = params["catchup"] as? Boolean ?: false, + docMd = params["doc_md"] as? String, + accessControl = + (params["access_control"] as? Map>)?.mapValues { (_, resources) -> + resources.mapValues { (_, perms) -> + when (perms) { + is List<*> -> perms.filterIsInstance().toSet() + is Set<*> -> perms.filterIsInstance().toSet() + else -> setOf(perms.toString()) + } + } + }, + isPausedUponCreation = params["is_paused_upon_creation"] as? Boolean, + tags = (params["tags"] as? List<*>)?.filterIsInstance()?.toSet() ?: emptySet(), + ownerLinks = (params["owner_links"] as? Map) ?: emptyMap(), + failFast = params["fail_fast"] as? Boolean ?: false, + dagDisplayName = params["dag_display_name"] as? String, + renderTemplateAsNativeObj = params["render_template_as_native_obj"] as? Boolean ?: false, + params = params["params"] as? Map, + ) + } + + // ----------------------------------------------------------------------- + // Dynamic test generation + // ----------------------------------------------------------------------- + + @Suppress("UNCHECKED_CAST") + @TestFactory + fun `serialise all YAML test cases`(): List { + val yamlFile = projectFile("validation/serialization/test_dags.yaml") + if (!yamlFile.exists()) { + return listOf( + DynamicTest.dynamicTest("test_dags.yaml not found — skipping") { + println("WARNING: ${yamlFile.absolutePath} not found, skipping serialisation tests") + }, + ) + } + + val root = yamlMapper.readValue(yamlFile, Map::class.java) as Map + val testCases = root["test_cases"] as List> + + // Accumulate results for JSON output + val allResults = mutableMapOf() + + val tests = + testCases.map { case -> + val name = case["name"] as String + val params = case["params"] as Map + + DynamicTest.dynamicTest(name) { + val dag = constructDag(params) + val serialized = serializeDag(dag) + + // Basic assertions + assertNotNull(serialized["dag_id"], "dag_id must be present") + assertNotNull(serialized["timetable"], "timetable must be present") + assertNotNull(serialized["tasks"], "tasks must be present") + assertFalse( + serialized.containsKey("__error"), + "serialisation must not produce an error entry", + ) + + allResults[name] = serialized + } + } + + // After all dynamic tests, write the combined JSON. + // We add a final "meta" test that writes the file. + val writeTest = + DynamicTest.dynamicTest("_write_serialized_java_json") { + val outputDir = projectFile("validation/serialization") + outputDir.mkdirs() + val outputFile = File(outputDir, "serialized_java.json") + jsonMapper.writeValue(outputFile, allResults.toSortedMap()) + println("Wrote ${allResults.size} serialised DAGs -> ${outputFile.absolutePath}") + } + + return tests + writeTest + } +} diff --git a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/TaskRunnerTest.kt b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/TaskRunnerTest.kt new file mode 100644 index 0000000000000..90593f136eb45 --- /dev/null +++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/TaskRunnerTest.kt @@ -0,0 +1,110 @@ +package org.apache.airflow.sdk.execution + +import org.apache.airflow.sdk.Bundle +import org.apache.airflow.sdk.Client +import org.apache.airflow.sdk.Dag +import org.apache.airflow.sdk.Task +import org.apache.airflow.sdk.execution.api.model.BundleInfo +import org.apache.airflow.sdk.execution.api.model.TIRunContext +import org.apache.airflow.sdk.execution.api.model.TaskInstance +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.time.OffsetDateTime +import java.util.UUID + +class TaskRunnerTest { + @Test + @DisplayName("Should execute task and return success") + fun shouldExecuteTaskAndReturnSuccess() { + val result = TaskRunner.run(bundleWith("success", SuccessTask::class.java), startupDetails(taskId = "success"), noOpClient()) + + Assertions.assertInstanceOf(SucceedTask::class.java, result) + } + + @Test + @DisplayName("Should return removed when task is missing") + fun shouldReturnRemovedWhenTaskIsMissing() { + val result = TaskRunner.run(bundleWith("other", SuccessTask::class.java), startupDetails(taskId = "missing"), noOpClient()) + + Assertions.assertInstanceOf(TaskState::class.java, result) + Assertions.assertEquals("removed", (result as TaskState).state) + } + + @Test + @DisplayName("Should return failed when task throws") + fun shouldReturnFailedWhenTaskThrows() { + val result = TaskRunner.run(bundleWith("failing", FailingTask::class.java), startupDetails(taskId = "failing"), noOpClient()) + + Assertions.assertInstanceOf(TaskState::class.java, result) + Assertions.assertEquals("failed", (result as TaskState).state) + } + + private fun bundleWith( + taskId: String, + taskClass: Class, + ): Bundle { + val dag = Dag("test_dag") + dag.addTask(taskId, taskClass) + return Bundle("1", listOf(dag)) + } + + private fun startupDetails(taskId: String): StartupDetails = + StartupDetails().also { + it.ti = + TaskInstance().also { taskInstance -> + taskInstance.id = UUID.randomUUID() + taskInstance.taskId = taskId + taskInstance.dagId = "test_dag" + taskInstance.runId = "manual__2026-03-31T00:00:00+00:00" + taskInstance.tryNumber = 1 + taskInstance.dagVersionId = UUID.randomUUID() + } + it.dagRelPath = "/dev/null" + it.bundleInfo = + BundleInfo().also { info -> + info.name = "bundle" + info.version = "1" + } + it.startDate = OffsetDateTime.parse("2026-03-31T00:00:00Z") + it.tiContext = TIRunContext() + it.sentryIntegration = "" + } + + private fun noOpClient() = + Client( + startupDetails(taskId = "unused"), + object : org.apache.airflow.sdk.execution.Client { + override fun getConnection(id: String) = throw UnsupportedOperationException("not used in test") + + override fun getVariable(key: String) = throw UnsupportedOperationException("not used in test") + + override fun getXCom( + key: String, + dagId: String, + taskId: String, + runId: String, + mapIndex: Int?, + includePriorDates: Boolean, + ) = throw UnsupportedOperationException("not used in test") + + override fun setXCom( + key: String, + value: Any, + dagId: String, + taskId: String, + runId: String, + mapIndex: Int, + ): Unit = throw UnsupportedOperationException("not used in test") + }, + ) + + class SuccessTask : Task { + override fun execute(client: Client) { + } + } + + class FailingTask : Task { + override fun execute(client: Client): Unit = throw IllegalStateException("boom") + } +} diff --git a/java-sdk/settings.gradle.kts b/java-sdk/settings.gradle.kts new file mode 100644 index 0000000000000..0892437c13d3e --- /dev/null +++ b/java-sdk/settings.gradle.kts @@ -0,0 +1,13 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/9.2.1/userguide/multi_project_builds.html in the Gradle documentation. + */ + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention").version("0.10.0") +} + +rootProject.name = "airflow-java-sdk" +include("example", "sdk") diff --git a/java-sdk/validation/serialization/compare.py b/java-sdk/validation/serialization/compare.py new file mode 100644 index 0000000000000..464f247852c40 --- /dev/null +++ b/java-sdk/validation/serialization/compare.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +# 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. + +""" +Compare serialised DAG output from Python and Java SDKs. + +Usage: + python compare.py serialized_python.json serialized_java.json +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +# Fields that are inherently environment-specific and should be ignored. +IGNORED_FIELDS = frozenset( + { + "fileloc", + "relative_fileloc", + "_processor_dags_folder", + } +) + +# Floating-point tolerance for timestamp / duration comparisons. +FLOAT_TOLERANCE = 1e-6 + + +# --------------------------------------------------------------------------- +# Normalisation +# --------------------------------------------------------------------------- + + +def _normalise(obj, *, _depth: int = 0): + """Recursively normalise an object for comparison.""" + if isinstance(obj, dict): + return { + k: _normalise(v, _depth=_depth + 1) for k, v in sorted(obj.items()) if k not in IGNORED_FIELDS + } + if isinstance(obj, list): + return [_normalise(item, _depth=_depth + 1) for item in obj] + if isinstance(obj, float): + return round(obj, 6) + return obj + + +# --------------------------------------------------------------------------- +# Deep diff +# --------------------------------------------------------------------------- + + +def _deep_diff(python_obj, java_obj, path: str = "") -> list[str]: + """Return a list of human-readable difference descriptions.""" + diffs: list[str] = [] + + if type(python_obj) is not type(java_obj): + # Allow int ↔ float (e.g. 0 vs 0.0) + if isinstance(python_obj, (int, float)) and isinstance(java_obj, (int, float)): + if abs(float(python_obj) - float(java_obj)) > FLOAT_TOLERANCE: + diffs.append(f"{path}: {python_obj!r} != {java_obj!r}") + return diffs + diffs.append( + f"{path}: type mismatch — Python {type(python_obj).__name__}" + f" vs Java {type(java_obj).__name__}" + f" (py={python_obj!r}, java={java_obj!r})" + ) + return diffs + + if isinstance(python_obj, dict): + all_keys = set(python_obj) | set(java_obj) + for key in sorted(all_keys): + child_path = f"{path}.{key}" if path else key + if key not in java_obj: + diffs.append(f"{child_path}: present in Python but missing in Java") + elif key not in python_obj: + diffs.append(f"{child_path}: present in Java but missing in Python") + else: + diffs.extend(_deep_diff(python_obj[key], java_obj[key], child_path)) + elif isinstance(python_obj, list): + if len(python_obj) != len(java_obj): + diffs.append(f"{path}: list length — Python {len(python_obj)} vs Java {len(java_obj)}") + for i, (p, j) in enumerate(zip(python_obj, java_obj)): + diffs.extend(_deep_diff(p, j, f"{path}[{i}]")) + elif isinstance(python_obj, float): + if abs(python_obj - java_obj) > FLOAT_TOLERANCE: + diffs.append(f"{path}: {python_obj!r} != {java_obj!r}") + elif python_obj != java_obj: + diffs.append(f"{path}: {python_obj!r} != {java_obj!r}") + + return diffs + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main() -> None: + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + python_path = Path(sys.argv[1]) + java_path = Path(sys.argv[2]) + + with open(python_path) as fh: + python_data: dict = json.load(fh) + with open(java_path) as fh: + java_data: dict = json.load(fh) + + all_names = sorted(set(python_data) | set(java_data)) + total = len(all_names) + passed = 0 + failed = 0 + + for name in all_names: + if name not in python_data: + print(f"SKIP {name} (missing in Python output)") + continue + if name not in java_data: + print(f"SKIP {name} (missing in Java output)") + continue + + py_dag = python_data[name] + jv_dag = java_data[name] + + # Skip error entries + if isinstance(py_dag, dict) and "__error" in py_dag: + print(f"SKIP {name} (Python error: {py_dag['__error']})") + continue + if isinstance(jv_dag, dict) and "__error" in jv_dag: + print(f"SKIP {name} (Java error: {jv_dag['__error']})") + continue + + py_norm = _normalise(py_dag) + jv_norm = _normalise(jv_dag) + + diffs = _deep_diff(py_norm, jv_norm) + if diffs: + failed += 1 + print(f"FAIL {name}") + for d in diffs: + print(f" {d}") + else: + passed += 1 + print(f"PASS {name}") + + print(f"\n{'=' * 60}") + print(f"Total: {total} | Passed: {passed} | Failed: {failed}") + if failed: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/java-sdk/validation/serialization/serialize_python.py b/java-sdk/validation/serialization/serialize_python.py new file mode 100644 index 0000000000000..7cc6ca00eb3d4 --- /dev/null +++ b/java-sdk/validation/serialization/serialize_python.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# 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. + +""" +Serialize DAGs using the Python Airflow SDK for cross-language comparison. + +Prerequisites: + - Airflow core and task-sdk installed (pip install -e …) + - PyYAML installed (pip install pyyaml) + +Usage: + python serialize_python.py test_dags.yaml serialized_python.json +""" + +from __future__ import annotations + +import json +import sys +from datetime import datetime, timedelta +from pathlib import Path + +import yaml + +# --------------------------------------------------------------------------- +# YAML params → Python DAG constructor kwargs +# --------------------------------------------------------------------------- + + +def _yaml_params_to_dag_kwargs(params: dict) -> dict: + """Convert language-agnostic YAML params to Python DAG constructor kwargs.""" + kwargs: dict = {} + for key, value in params.items(): + if key in ("start_date", "end_date") and isinstance(value, str): + kwargs[key] = datetime.fromisoformat(value) + elif key == "dagrun_timeout_seconds": + kwargs["dagrun_timeout"] = timedelta(seconds=value) + elif key == "tags" and isinstance(value, list): + kwargs["tags"] = set(value) + elif key == "access_control" and isinstance(value, dict): + # Convert innermost lists → sets (permissions) + kwargs["access_control"] = { + role: { + resource: set(perms) if isinstance(perms, list) else perms + for resource, perms in resources.items() + } + for role, resources in value.items() + } + elif key == "params": + kwargs["params"] = value + else: + kwargs[key] = value + return kwargs + + +# --------------------------------------------------------------------------- +# JSON helper +# --------------------------------------------------------------------------- + + +def _make_json_safe(obj): + """Handle types that json.dumps cannot serialise natively.""" + if isinstance(obj, (set, frozenset)): + return sorted(obj) + if isinstance(obj, bytes): + return obj.decode("utf-8") + raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main() -> None: + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + yaml_path = Path(sys.argv[1]) + output_path = Path(sys.argv[2]) + + with open(yaml_path) as fh: + test_data = yaml.safe_load(fh) + + # Lazy-import Airflow so the script fails fast on missing args first. + from airflow.sdk import DAG + from airflow.serialization.serialized_objects import DagSerialization + + results: dict[str, dict] = {} + for case in test_data["test_cases"]: + name = case["name"] + kwargs = _yaml_params_to_dag_kwargs(case["params"]) + print(f" [{name}] ", end="") + try: + dag = DAG(**kwargs) + serialized = DagSerialization.serialize_dag(dag) + results[name] = serialized + print("OK") + except Exception as exc: + print(f"ERROR: {exc}") + results[name] = {"__error": str(exc)} + + with open(output_path, "w") as fh: + json.dump(results, fh, indent=2, sort_keys=True, default=_make_json_safe) + + print(f"\nWrote {len(results)} serialised DAGs → {output_path}") + + +if __name__ == "__main__": + main() diff --git a/java-sdk/validation/serialization/test_dags.yaml b/java-sdk/validation/serialization/test_dags.yaml new file mode 100644 index 0000000000000..f2eb014b60721 --- /dev/null +++ b/java-sdk/validation/serialization/test_dags.yaml @@ -0,0 +1,198 @@ +# 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. + +# Cross-language DAG serialization test cases. +# +# Each entry defines DAG constructor parameters in a language-agnostic way. +# Both the Python and Java serialization scripts read this file, construct +# DAGs from the params, and write the serialized output for comparison. +# +# Type conventions: +# start_date / end_date — ISO-8601 string, parsed to datetime / Instant +# dagrun_timeout_seconds — number of seconds, parsed to timedelta / Duration +# tags — list of strings, converted to set on both sides +# access_control — nested map; innermost lists become sets + +test_cases: + # ---- schedule variants ------------------------------------------------ + - name: "minimal_dag" + params: + dag_id: "example_dag" + + - name: "schedule_daily" + params: + dag_id: "example_dag" + schedule: "@daily" + + - name: "schedule_hourly" + params: + dag_id: "example_dag" + schedule: "@hourly" + + - name: "schedule_once" + params: + dag_id: "example_dag" + schedule: "@once" + + - name: "schedule_continuous" + params: + dag_id: "example_dag" + schedule: "@continuous" + max_active_runs: 1 + + - name: "schedule_cron" + params: + dag_id: "example_dag" + schedule: "0 0 * * *" + + - name: "schedule_cron_complex" + params: + dag_id: "example_dag" + schedule: "30 2 */3 * 1-5" + + # ---- simple scalar fields -------------------------------------------- + - name: "with_description" + params: + dag_id: "example_dag" + description: "This is an example DAG for testing serialization." + + - name: "with_doc_md" + params: + dag_id: "example_dag" + doc_md: "# Example DAG\n\nThis is **markdown** documentation." + + - name: "with_dag_display_name" + params: + dag_id: "example_dag" + dag_display_name: "My Example Pipeline" + + - name: "with_dag_display_name_same_as_id" + params: + dag_id: "example_dag" + dag_display_name: "example_dag" + + # ---- boolean / numeric fields ----------------------------------------- + - name: "with_catchup_and_start_date" + params: + dag_id: "example_dag" + schedule: "@daily" + start_date: "2024-01-01T00:00:00Z" + catchup: true + + - name: "with_fail_fast" + params: + dag_id: "example_dag" + fail_fast: true + + - name: "with_render_template_as_native_obj" + params: + dag_id: "example_dag" + render_template_as_native_obj: true + + - name: "with_is_paused_upon_creation_true" + params: + dag_id: "example_dag" + is_paused_upon_creation: true + + - name: "with_is_paused_upon_creation_false" + params: + dag_id: "example_dag" + is_paused_upon_creation: false + + - name: "with_max_active_tasks" + params: + dag_id: "example_dag" + max_active_tasks: 32 + max_active_runs: 8 + max_consecutive_failed_dag_runs: 5 + + - name: "with_dagrun_timeout" + params: + dag_id: "example_dag" + dagrun_timeout_seconds: 3600 + + - name: "with_start_and_end_date" + params: + dag_id: "example_dag" + start_date: "2024-01-01T00:00:00Z" + end_date: "2024-12-31T23:59:59Z" + + # ---- collection fields ------------------------------------------------ + - name: "with_tags" + params: + dag_id: "example_dag" + tags: ["alpha", "beta", "gamma"] + + - name: "with_owner_links" + params: + dag_id: "example_dag" + owner_links: + data_team: "https://example.com/data-team" + dev_team: "https://example.com/dev-team" + + # ---- decorated / typed fields ----------------------------------------- + - name: "with_default_args" + params: + dag_id: "example_dag" + default_args: + retries: 3 + owner: "test_owner" + + - name: "with_params" + params: + dag_id: "example_dag" + params: + param_string: "value1" + param_int: 42 + param_bool: true + + - name: "with_access_control" + params: + dag_id: "example_dag" + access_control: + viewer_role: + DAGs: + - "can_read" + editor_role: + DAGs: + - "can_read" + - "can_edit" + + # ---- complex combined ------------------------------------------------- + - name: "complex_dag" + params: + dag_id: "complex_dag" + schedule: "0 */6 * * *" + description: "A complex DAG with many parameters" + start_date: "2024-06-15T10:30:00Z" + catchup: true + max_active_tasks: 32 + max_active_runs: 8 + max_consecutive_failed_dag_runs: 3 + dagrun_timeout_seconds: 7200 + tags: ["complex", "production", "etl"] + owner_links: + data_team: "https://example.com/data-team" + fail_fast: true + dag_display_name: "Complex ETL Pipeline" + doc_md: "# Complex Pipeline\nHandles ETL processing." + default_args: + retries: 2 + owner: "data_engineering" + params: + env: "production" + batch_size: 1000 From 30aae4d273430415587eaf3112425502be15411d Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 20 Apr 2026 09:26:18 +0800 Subject: [PATCH 67/80] Add Java SDK e2e tests --- airflow-e2e-tests/docker/java-sdk.yml | 52 ++++ .../tests/airflow_e2e_tests/conftest.py | 212 +++++++++++++++- .../tests/airflow_e2e_tests/constants.py | 22 ++ .../java_sdk_tests/__init__.py | 16 ++ .../java_sdk_tests/dags/stub_dag.py | 51 ++++ .../java_sdk_tests/test_java_sdk.py | 237 ++++++++++++++++++ 6 files changed, 585 insertions(+), 5 deletions(-) create mode 100644 airflow-e2e-tests/docker/java-sdk.yml create mode 100644 airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/__init__.py create mode 100644 airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py create mode 100644 airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/test_java_sdk.py diff --git a/airflow-e2e-tests/docker/java-sdk.yml b/airflow-e2e-tests/docker/java-sdk.yml new file mode 100644 index 0000000000000..025653e8265dc --- /dev/null +++ b/airflow-e2e-tests/docker/java-sdk.yml @@ -0,0 +1,52 @@ +# 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. +# +# Docker Compose override for Java SDK e2e tests. +# +# Overrides the dag-processor and worker services to: +# 1. Mount a pre-downloaded JDK, the Python stub bundle, and the pure Java bundle. +# 2. Set JAVA_HOME and PATH so the Java coordinator can find the JDK. +# +# The Java language provider is expected to be pre-installed in the Docker image. +# +# Environment variables expected (set by conftest.py): +# JAVA_OPENJDK_PATH – host path to the pre-downloaded OpenJDK directory +# JAVA_STUB_BUNDLE_PATH – host path to the stub DAG bundle root (dags/ + jar-bundles/) +# JAVA_BUNDLES_PATH – host path to the pure Java DAG bundle (JARs only) +--- +services: + airflow-dag-processor: + user: "0:0" + environment: + AIRFLOW__CORE__LOAD_EXAMPLES: "false" + JAVA_HOME: /files/openjdk + PATH: /files/openjdk/bin:/home/airflow/.local/bin:/usr/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + volumes: + - ${JAVA_OPENJDK_PATH}:/files/openjdk + - ${JAVA_STUB_BUNDLE_PATH}:/files/python-stub-java-tasks + - ${JAVA_BUNDLES_PATH}:/files/pure-java + + airflow-worker: + user: "0:0" + environment: + AIRFLOW__CORE__LOAD_EXAMPLES: "false" + JAVA_HOME: /files/openjdk + PATH: /files/openjdk/bin:/home/airflow/.local/bin:/usr/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + volumes: + - ${JAVA_OPENJDK_PATH}:/files/openjdk + - ${JAVA_STUB_BUNDLE_PATH}:/files/python-stub-java-tasks + - ${JAVA_BUNDLES_PATH}:/files/pure-java diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py index 4443f7d8a2898..b32f1990c323a 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py @@ -18,9 +18,12 @@ import json import os +import platform +import subprocess +import tempfile from datetime import datetime from pathlib import Path -from shutil import copyfile, copytree +from shutil import copyfile, copytree, ignore_patterns import pytest from rich.console import Console @@ -34,6 +37,16 @@ E2E_DAGS_FOLDER, E2E_TEST_MODE, ELASTICSEARCH_PATH, + JAVA_CONTAINER_PURE_BUNDLE_PATH, + JAVA_CONTAINER_STUB_DAG_BUNDLE_PATH, + JAVA_CONTAINER_STUB_JAVA_BUNDLES_FOLDER_PATH, + JAVA_PURE_BUNDLE_NAME, + JAVA_PURE_DAG_ID, + JAVA_SDK_COMPOSE_PATH, + JAVA_SDK_DAGS_FOLDER, + JAVA_SDK_PATH, + JAVA_STUB_BUNDLE_NAME, + JAVA_STUB_DAG_ID, LOCALSTACK_PATH, LOGS_FOLDER, OPENSEARCH_PATH, @@ -142,8 +155,195 @@ def _setup_xcom_object_storage_integration(dot_env_file, tmp_dir): os.environ["ENV_FILE_PATH"] = str(dot_env_file) -def spin_up_airflow_environment(tmp_path_factory: pytest.TempPathFactory): - tmp_dir = tmp_path_factory.mktemp("airflow-e2e-tests") +def _download_jdk(dest_dir: Path) -> Path: + """Download a Linux JDK matching the host/container architecture into dest_dir/openjdk.""" + machine = platform.machine() + if machine in ("x86_64", "amd64"): + arch = "x64" + elif machine in ("aarch64", "arm64"): + arch = "aarch64" + else: + raise RuntimeError(f"Unsupported architecture: {machine}") + + jdk_url = ( + "https://github.com/adoptium/temurin11-binaries/releases/download/" + f"jdk-11.0.30%2B7/OpenJDK11U-jdk_{arch}_linux_hotspot_11.0.30_7.tar.gz" + ) + + tarball_path = dest_dir / "openjdk-11.tar.gz" + openjdk_dir = dest_dir / "openjdk" + openjdk_dir.mkdir(exist_ok=True) + + console.print(f"[yellow]Downloading OpenJDK 11 ({arch}) for containers...") + result = subprocess.run( + ["curl", "-fL", "-o", str(tarball_path), jdk_url], + capture_output=True, + text=True, + timeout=300, + check=False, + ) + if result.returncode != 0: + raise RuntimeError(f"Failed to download OpenJDK: {result.stderr}") + + console.print("[yellow]Extracting OpenJDK...") + result = subprocess.run( + ["tar", "-xzf", str(tarball_path), "--strip-components=1", "-C", str(openjdk_dir)], + capture_output=True, + text=True, + timeout=120, + check=False, + ) + if result.returncode != 0: + raise RuntimeError(f"Failed to extract OpenJDK: {result.stderr}") + + tarball_path.unlink() + console.print("[green]OpenJDK 11 downloaded and extracted successfully") + return openjdk_dir + + +def _build_java_example(java_sdk_dir: Path) -> list[Path]: + """Build the Java SDK example project using Gradle and return paths to all built JARs.""" + gradlew = java_sdk_dir / "gradlew" + if not gradlew.exists(): + raise FileNotFoundError( + f"Gradle wrapper not found at {gradlew}. " + "Make sure the java-sdk directory exists (e.g. on the feature/java-all branch)." + ) + + console.print("[yellow]Formatting Java sources with spotlessApply...") + fmt_result = subprocess.run( + [str(gradlew), "spotlessApply", "--no-configuration-cache"], + cwd=str(java_sdk_dir), + capture_output=True, + text=True, + timeout=300, + check=False, + ) + if fmt_result.returncode != 0: + console.print(f"[red]spotlessApply failed:\n{fmt_result.stdout}\n{fmt_result.stderr}") + raise RuntimeError("Failed to format Java SDK sources") + + console.print("[yellow]Building Java SDK example with Gradle...") + result = subprocess.run( + [str(gradlew), "clean", ":example:installDist", "-x", "test"], + cwd=str(java_sdk_dir), + capture_output=True, + text=True, + timeout=300, + check=False, + ) + if result.returncode != 0: + console.print(f"[red]Gradle build failed:\n{result.stdout}\n{result.stderr}") + raise RuntimeError("Failed to build Java SDK example") + + console.print("[green]Java SDK example built successfully") + + # The Gradle build produces a full distribution under install/example/lib/ + # containing the example JAR, sdk JAR, and all transitive dependencies. + jar_dir = java_sdk_dir / "example" / "build" / "install" / "example" / "lib" + jars = list(jar_dir.glob("*.jar")) + if not jars: + raise FileNotFoundError(f"No JARs found in {jar_dir} after build") + + return jars + + +def _sed_java_dag_id(java_sdk_dir: Path, dag_id: str): + """Replace the dag_id in JavaExample.java with the given dag_id.""" + java_example = ( + java_sdk_dir + / "example" + / "src" + / "java" + / "org" + / "apache" + / "airflow" + / "example" + / "JavaExample.java" + ) + content = java_example.read_text() + java_example.write_text(content.replace('"java_example"', f'"{dag_id}"')) + console.print(f"[yellow]Set JavaExample.java dag_id to '{dag_id}'") + + +def _setup_java_sdk_integration(dot_env_file, tmp_dir): + """Set up Java SDK integration: download JDK, build JARs with distinct dag_ids, write env and compose override.""" + # Download the Linux JDK on the host so containers get it via bind mount + openjdk_dir = _download_jdk(tmp_dir) + + # Copy java-sdk twice so each build gets its own dag_id without conflicts + ignore = ignore_patterns(".gradle", "build", ".kotlin") + + # Build JAR for the stub DAG bundle + stub_sdk_copy = tmp_dir / "java-sdk-stub" + copytree(JAVA_SDK_PATH, stub_sdk_copy, ignore=ignore) + _sed_java_dag_id(stub_sdk_copy, JAVA_STUB_DAG_ID) + stub_jars = _build_java_example(stub_sdk_copy) + stub_bundle_root = tmp_dir / JAVA_STUB_BUNDLE_NAME + stub_bundle_root.mkdir() + stub_jar_dir = stub_bundle_root / "jar-bundles" + stub_jar_dir.mkdir() + for jar in stub_jars: + copyfile(jar, stub_jar_dir / jar.name) + console.print(f"[yellow]Copied {len(stub_jars)} JAR(s) to {stub_jar_dir}") + + # Build JAR for the pure Java DAG bundle + pure_sdk_copy = tmp_dir / "java-sdk-pure" + copytree(JAVA_SDK_PATH, pure_sdk_copy, ignore=ignore) + _sed_java_dag_id(pure_sdk_copy, JAVA_PURE_DAG_ID) + pure_java_jars = _build_java_example(pure_sdk_copy) + pure_bundle_dir = tmp_dir / JAVA_PURE_BUNDLE_NAME + pure_bundle_dir.mkdir() + for jar in pure_java_jars: + copyfile(jar, pure_bundle_dir / jar.name) + console.print(f"[yellow]Copied {len(pure_java_jars)} JAR(s) to {pure_bundle_dir}") + + # Copy stub_dag.py into the dags subfolder with the matching dag_id + stub_dags_dir = stub_bundle_root / "dags" + stub_dags_dir.mkdir() + copytree(JAVA_SDK_DAGS_FOLDER, stub_dags_dir, dirs_exist_ok=True) + stub_dag = stub_dags_dir / "stub_dag.py" + content = stub_dag.read_text() + stub_dag.write_text(content.replace('"java_example"', f'"{JAVA_STUB_DAG_ID}"')) + console.print(f"[yellow]Set stub_dag.py dag_id to '{JAVA_STUB_DAG_ID}'") + + # Copy the docker-compose override + copyfile(JAVA_SDK_COMPOSE_PATH, tmp_dir / "java-sdk.yml") + + # Set host environment variables consumed by docker-compose variable substitution + os.environ["JAVA_OPENJDK_PATH"] = str(openjdk_dir) + os.environ["JAVA_STUB_BUNDLE_PATH"] = str(stub_bundle_root) + os.environ["JAVA_BUNDLES_PATH"] = str(pure_bundle_dir) + + # Write .env file with bundle config for both DAG bundles + bundle_config = json.dumps( + [ + { + "name": JAVA_STUB_BUNDLE_NAME, + "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle", + "kwargs": {"path": JAVA_CONTAINER_STUB_DAG_BUNDLE_PATH, "refresh_interval": 20}, + }, + { + "name": JAVA_PURE_BUNDLE_NAME, + "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle", + "kwargs": {"path": JAVA_CONTAINER_PURE_BUNDLE_PATH, "refresh_interval": 20}, + }, + ] + ) + + dot_env_file.write_text( + f"AIRFLOW_UID={os.getuid()}\n" + "AIRFLOW__CORE__LOAD_EXAMPLES=false\n" + f"AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST={bundle_config}\n" + f"AIRFLOW__JAVA__BUNDLES_FOLDER={JAVA_CONTAINER_STUB_JAVA_BUNDLES_FOLDER_PATH}\n" + ) + os.environ["ENV_FILE_PATH"] = str(dot_env_file) + + +def spin_up_airflow_environment(): + # We indent to use explicitly created temp directory instead of pytest's tmp_path fixture because we want the directory to persist after the test run for debugging purposes and pytest's tmp_path is automatically deleted after the test run. + tmp_dir = Path(tempfile.mkdtemp(prefix="airflow-e2e-tests-")) + console.print(f"[yellow]Temp directory (persists after test run): {tmp_dir}") console.print(f"[yellow]Using docker compose file: {DOCKER_COMPOSE_PATH}") copyfile(DOCKER_COMPOSE_PATH, tmp_dir / "docker-compose.yaml") @@ -180,6 +380,9 @@ def spin_up_airflow_environment(tmp_path_factory: pytest.TempPathFactory): elif E2E_TEST_MODE == "xcom_object_storage": compose_file_names.append("localstack.yml") _setup_xcom_object_storage_integration(dot_env_file, tmp_dir) + elif E2E_TEST_MODE == "java_sdk": + compose_file_names.append("java-sdk.yml") + _setup_java_sdk_integration(dot_env_file, tmp_dir) # # Please Do not use this Fernet key in any deployments! Please generate your own key. @@ -224,8 +427,7 @@ def _print_logs(compose_instance: DockerCompose): def pytest_sessionstart(session: pytest.Session): - tmp_path_factory = session.config._tmp_path_factory - spin_up_airflow_environment(tmp_path_factory) + spin_up_airflow_environment() console.print("[green]Airflow environment is up and running!") diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py index a2d99aeababa1..7d6e3508abbc2 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py @@ -47,3 +47,25 @@ # s3 bucket name for XComObjectStorageBackend tests. This bucket will be created in the `init-aws.sh` script that is run as part of the LocalStack container initialization. XCOM_BUCKET = "test-xcom-objectstorage-backend" + +# Java SDK e2e test paths +JAVA_SDK_PATH = AIRFLOW_ROOT_PATH / "java-sdk" +JAVA_SDK_COMPOSE_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "docker" / "java-sdk.yml" +JAVA_SDK_DAGS_FOLDER = ( + AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "tests" / "airflow_e2e_tests" / "java_sdk_tests" / "dags" +) + +# Java SDK bundle names (used in DAG bundle config and docker-compose env vars) +JAVA_STUB_BUNDLE_NAME = "python-stub-java-tasks" +JAVA_PURE_BUNDLE_NAME = "pure-java" + +# Java SDK DAG IDs (sed-replaced from the original "java_example" in JavaExample.java / stub_dag.py) +JAVA_STUB_DAG_ID = "java_sdk_stub_example" +JAVA_PURE_DAG_ID = "java_sdk_pure_java_example" + +# In-container paths for Java SDK volumes (mounted by java-sdk.yml) +JAVA_CONTAINER_OPENJDK_PATH = "/files/openjdk" +JAVA_CONTAINER_STUB_BUNDLE_PATH = f"/files/{JAVA_STUB_BUNDLE_NAME}" +JAVA_CONTAINER_STUB_DAG_BUNDLE_PATH = f"/files/{JAVA_STUB_BUNDLE_NAME}/dags" +JAVA_CONTAINER_STUB_JAVA_BUNDLES_FOLDER_PATH = f"/files/{JAVA_STUB_BUNDLE_NAME}/jar-bundles" +JAVA_CONTAINER_PURE_BUNDLE_PATH = f"/files/{JAVA_PURE_BUNDLE_NAME}" diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/__init__.py b/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/__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/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py b/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py new file mode 100644 index 0000000000000..65a0832e6a8d3 --- /dev/null +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py @@ -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. + +from __future__ import annotations + +from airflow.sdk import dag, task + + +@task() +def python_task_1(ti): + print("python_task_1") + print("Push Python Task 'python_task_1' XCom:") + ti.xcom_push(value="value-pushed-from-python_task_1", key="return_value") + + +@task.stub(language="java") +def extract(): ... + + +@task.stub(language="java") +def transform(): ... + + +@task() +def python_task_2(ti): + print("python_task_2") + print("Pull Java Task 'transform' XCom:") + print(ti.xcom_pull(task_ids="transform")) + + +@dag(dag_id="java_example") +def simple_dag(): + + python_task_1() >> extract() >> transform() >> python_task_2() + + +simple_dag() diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/test_java_sdk.py b/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/test_java_sdk.py new file mode 100644 index 0000000000000..8b30f25c1f8b3 --- /dev/null +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/test_java_sdk.py @@ -0,0 +1,237 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest +import requests + +from airflow_e2e_tests.constants import JAVA_PURE_DAG_ID, JAVA_STUB_DAG_ID +from airflow_e2e_tests.e2e_test_utils.clients import AirflowClient + + +class TestJavaSDK: + """End-to-end tests for the Java SDK integration. + + Tests two DAGs that exercise the Java SDK: + + 1. Python stub DAG (stub_dag.py): + python_task_1 -> extract (Java) -> transform (Java) -> python_task_2 + Validates Python-Java interop, XCom flow in both directions, getConnection, getVariable. + + 2. Pure Java DAG (JavaExample.java): + extract -> transform -> load + Validates pure Java DAG parsing. The load task throws RuntimeException("I failed"). + """ + + airflow_client = AirflowClient() + + @pytest.fixture(autouse=True) + def _create_prerequisites(self): + """Create the Airflow connection and variable that the Java tasks depend on.""" + try: + self.airflow_client._make_request( + method="POST", + endpoint="connections", + json={ + "connection_id": "test_http", + "conn_type": "http", + "host": "httpbin.org", + }, + ) + except requests.exceptions.HTTPError as e: + if e.response.status_code != 409: + raise + + try: + self.airflow_client._make_request( + method="POST", + endpoint="variables", + json={ + "key": "my_variable", + "value": "test_value", + }, + ) + except requests.exceptions.HTTPError as e: + if e.response.status_code != 409: + raise + + def test_stub_java_dag(self): + """Trigger the Python stub DAG once and validate all Python-Java interop. + + stub_dag.py defines: python_task_1 -> extract -> transform -> python_task_2 + All 4 tasks should succeed. + """ + resp = self.airflow_client.trigger_dag( + JAVA_STUB_DAG_ID, + json={"logical_date": datetime.now(timezone.utc).isoformat()}, + ) + run_id = resp["dag_run_id"] + state = self.airflow_client.wait_for_dag_run( + dag_id=JAVA_STUB_DAG_ID, + run_id=run_id, + timeout=300, + ) + + assert state == "success", f"Stub DAG run expected to succeed, got: {state}" + + # Validate all 4 task instances succeed + task_instances = self.airflow_client.get_task_instances(JAVA_STUB_DAG_ID, run_id) + ti_map = {ti["task_id"]: ti["state"] for ti in task_instances["task_instances"]} + + expected_tasks = {"python_task_1", "extract", "transform", "python_task_2"} + assert expected_tasks.issubset(ti_map.keys()), ( + f"Missing tasks. Expected {expected_tasks}, got {set(ti_map)}" + ) + for task_id in expected_tasks: + assert ti_map[task_id] == "success", ( + f"Task '{task_id}' expected 'success', got '{ti_map[task_id]}'" + ) + + # Validate Extract task logs + extract_logs = str( + self.airflow_client.get_task_logs( + dag_id=JAVA_STUB_DAG_ID, run_id=run_id, task_id="extract", try_number=1 + ) + ) + assert "Hello from task" in extract_logs, "Expected 'Hello from task' in extract logs" + assert "Got XCom from Python Task 'python_task_1'" in extract_logs, ( + "Extract task did not log receiving XCom from python_task_1" + ) + assert "Got con" in extract_logs, "Extract task did not log getting connection" + assert "Goodbye from task" in extract_logs, "Expected 'Goodbye from task' in extract logs" + + # Validate Transform task logs + transform_logs = str( + self.airflow_client.get_task_logs( + dag_id=JAVA_STUB_DAG_ID, run_id=run_id, task_id="transform", try_number=1 + ) + ) + assert "Got XCom from 'extract'" in transform_logs, ( + "Transform task did not log receiving XCom from extract" + ) + assert "Got variable" in transform_logs, "Transform task did not log getting variable" + assert "Push XCom to python task 2" in transform_logs, "Transform task did not log pushing XCom" + + # Validate XCom flow: Java -> Python (transform -> python_task_2) + python_task_2_logs = str( + self.airflow_client.get_task_logs( + dag_id=JAVA_STUB_DAG_ID, run_id=run_id, task_id="python_task_2", try_number=1 + ) + ) + assert "Pull Java Task" in python_task_2_logs, ( + "python_task_2 did not log pulling XCom from Java transform task" + ) + + # Validate XCom values via API + python_task_1_xcom = self.airflow_client.get_xcom_value( + dag_id=JAVA_STUB_DAG_ID, run_id=run_id, task_id="python_task_1", key="return_value" + ) + assert python_task_1_xcom["value"] == "value-pushed-from-python_task_1", ( + f"python_task_1 XCom expected 'value-pushed-from-python_task_1', got: {python_task_1_xcom['value']}" + ) + + # Java extract and transform tasks push a timestamp (long) via client.setXCom() + extract_xcom = self.airflow_client.get_xcom_value( + dag_id=JAVA_STUB_DAG_ID, run_id=run_id, task_id="extract", key="return_value" + ) + assert extract_xcom["value"] is not None, "extract XCom value should not be None" + + transform_xcom = self.airflow_client.get_xcom_value( + dag_id=JAVA_STUB_DAG_ID, run_id=run_id, task_id="transform", key="return_value" + ) + assert transform_xcom["value"] is not None, "transform XCom value should not be None" + + def test_pure_java_dag(self): + """Trigger the pure Java DAG once and validate JavaExample.java behaviors. + + JavaExample.java defines: extract -> transform -> load + extract and transform succeed; load throws RuntimeException("I failed"). + """ + resp = self.airflow_client.trigger_dag( + JAVA_PURE_DAG_ID, + json={"logical_date": datetime.now(timezone.utc).isoformat()}, + ) + run_id = resp["dag_run_id"] + state = self.airflow_client.wait_for_dag_run( + dag_id=JAVA_PURE_DAG_ID, + run_id=run_id, + timeout=300, + ) + + # DAG run should fail because load throws RuntimeException + assert state == "failed", ( + f"Pure Java DAG expected to fail (load throws RuntimeException), got: {state}" + ) + + # Validate 3 task instances with expected states + task_instances = self.airflow_client.get_task_instances(JAVA_PURE_DAG_ID, run_id) + ti_map = {ti["task_id"]: ti["state"] for ti in task_instances["task_instances"]} + + expected_states = { + "extract": "success", + "transform": "success", + "load": "failed", + } + assert set(expected_states).issubset(ti_map.keys()), ( + f"Missing tasks. Expected {set(expected_states)}, got {set(ti_map)}" + ) + for task_id, expected_state in expected_states.items(): + assert ti_map[task_id] == expected_state, ( + f"Task '{task_id}' expected '{expected_state}', got '{ti_map[task_id]}'" + ) + + # Validate Extract task logs + extract_logs = str( + self.airflow_client.get_task_logs( + dag_id=JAVA_PURE_DAG_ID, run_id=run_id, task_id="extract", try_number=1 + ) + ) + assert "Hello from task" in extract_logs, "Expected 'Hello from task' in extract logs" + assert "Goodbye from task" in extract_logs, "Expected 'Goodbye from task' in extract logs" + + # Validate Transform task logs + transform_logs = str( + self.airflow_client.get_task_logs( + dag_id=JAVA_PURE_DAG_ID, run_id=run_id, task_id="transform", try_number=1 + ) + ) + assert "Got XCom from 'extract'" in transform_logs, ( + "Transform task did not log receiving XCom from extract" + ) + assert "Got variable" in transform_logs, "Transform task did not log getting variable" + + # Validate Load task logs (expected failure) + load_logs = str( + self.airflow_client.get_task_logs( + dag_id=JAVA_PURE_DAG_ID, run_id=run_id, task_id="load", try_number=1 + ) + ) + assert "Got XCom from 'transform'" in load_logs, "Load task did not log receiving XCom from transform" + assert "I failed" in load_logs, "Load task did not log the expected RuntimeException message" + + # Validate XCom values via API for successful tasks + extract_xcom = self.airflow_client.get_xcom_value( + dag_id=JAVA_PURE_DAG_ID, run_id=run_id, task_id="extract", key="return_value" + ) + assert extract_xcom["value"] is not None, "extract XCom value should not be None" + + transform_xcom = self.airflow_client.get_xcom_value( + dag_id=JAVA_PURE_DAG_ID, run_id=run_id, task_id="transform", key="return_value" + ) + assert transform_xcom["value"] is not None, "transform XCom value should not be None" From 07d1e5067ab785fab5792ca98aa3154b94599669 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 20 Apr 2026 14:24:56 +0800 Subject: [PATCH 68/80] Add check-java-serialization-compatibility prek hook --- .pre-commit-config.yaml | 16 ++ .../check_java_serialization_compatibility.py | 186 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100755 scripts/ci/prek/check_java_serialization_compatibility.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a594e3e848a9..066cb3ab6ab61 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1117,6 +1117,22 @@ repos: files: ^airflow-core/src/airflow/migrations/versions/.*\.py$ additional_dependencies: [rich>=13.6.0] require_serial: true + - id: check-java-serialization-compatibility + name: Check Java SDK serialization compatibility + description: >- + Verify that the Java and Python SDKs produce identical DAG serialization + for all test cases defined in java-sdk/validation/serialization/test_dags.yaml + entry: ./scripts/ci/prek/check_java_serialization_compatibility.py + language: python + stages: ['manual'] + pass_filenames: false + require_serial: true + files: > + (?x) + ^java-sdk/sdk/src/.*\.(kt|java)$| + ^java-sdk/validation/serialization/.*$| + ^airflow-core/src/airflow/serialization/.*\.py$| + ^task-sdk/src/airflow/sdk/definitions/.*\.py$ ## ADD MOST PREK HOOK ABOVE THAT LINE # The below prek hooks are those requiring CI image to be built ## ONLY ADD PREK HOOKS HERE THAT REQUIRE CI IMAGE diff --git a/scripts/ci/prek/check_java_serialization_compatibility.py b/scripts/ci/prek/check_java_serialization_compatibility.py new file mode 100755 index 0000000000000..8d2f9a067427a --- /dev/null +++ b/scripts/ci/prek/check_java_serialization_compatibility.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python +# 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. +# /// script +# requires-python = ">=3.10,<3.11" +# dependencies = [ +# "rich>=13.6.0", +# ] +# /// +""" +Check cross-language DAG serialization compatibility between the Python and Java SDKs. + +Workflow: + 1. Run the Java SDK ``SerializationCompatibilityTest`` via Gradle to produce + ``serialized_java.json``. + 2. Run ``serialize_python.py`` via ``uv`` to produce ``serialized_python.json``. + 3. Run ``compare.py`` to deep-diff the two outputs. + +The check fails if any DAG serialises differently across the two languages. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +from common_prek_utils import AIRFLOW_ROOT_PATH, console + +JAVA_SDK_PATH = AIRFLOW_ROOT_PATH / "java-sdk" +VALIDATION_DIR = JAVA_SDK_PATH / "validation" / "serialization" +TEST_DAGS_YAML = VALIDATION_DIR / "test_dags.yaml" +COMPARE_SCRIPT = VALIDATION_DIR / "compare.py" +SERIALIZE_PYTHON_SCRIPT = VALIDATION_DIR / "serialize_python.py" +SERIALIZED_JAVA_JSON = VALIDATION_DIR / "serialized_java.json" + + +def _run_java_serialization() -> bool: + """Run the Java SDK serialization test to generate ``serialized_java.json``.""" + gradlew = JAVA_SDK_PATH / "gradlew" + if not gradlew.exists(): + console.print(f"[red]Gradle wrapper not found at {gradlew}[/]") + return False + + if not shutil.which("java"): + console.print( + "[red]'java' not found on PATH. Install a JDK (17+) or set JAVA_HOME to run this check.[/]" + ) + return False + + console.print("[yellow]Step 1/3: Running Java SDK serialization test...[/]") + result = subprocess.run( + [ + str(gradlew), + "sdk:cleanTest", + "sdk:test", + "--tests", + "org.apache.airflow.sdk.execution.SerializationCompatibilityTest", + "-x", + "spotlessApply", + "-x", + "spotlessCheck", + ], + cwd=str(JAVA_SDK_PATH), + capture_output=True, + text=True, + timeout=300, + check=False, + ) + if result.returncode != 0: + console.print("[red]Java SDK serialization test failed:[/]") + if result.stdout: + console.print(result.stdout) + if result.stderr: + console.print(result.stderr) + return False + + if not SERIALIZED_JAVA_JSON.exists(): + console.print(f"[red]Expected Java output not found: {SERIALIZED_JAVA_JSON}[/]") + return False + + return True + + +def _run_python_serialization(output_path: Path) -> bool: + """Run the Python serialization script to generate ``serialized_python.json``.""" + console.print("[yellow]Step 2/3: Running Python SDK serialization...[/]") + # TODO: Perhaps we need to run this in Breeze container instead of relying on the host environment + result = subprocess.run( + [ + "uv", + "run", + "--project", + str(AIRFLOW_ROOT_PATH / "airflow-core"), + "python", + str(SERIALIZE_PYTHON_SCRIPT), + str(TEST_DAGS_YAML), + str(output_path), + ], + cwd=str(AIRFLOW_ROOT_PATH), + capture_output=True, + text=True, + timeout=120, + check=False, + ) + if result.returncode != 0: + console.print("[red]Python SDK serialization failed:[/]") + if result.stdout: + console.print(result.stdout) + if result.stderr: + console.print(result.stderr) + return False + return True + + +def _run_comparison(python_json: Path, java_json: Path) -> bool: + """Run the comparison script to diff the two serialized outputs.""" + console.print("[yellow]Step 3/3: Comparing serialized outputs...[/]") + result = subprocess.run( + ["python", str(COMPARE_SCRIPT), str(python_json), str(java_json)], + capture_output=True, + text=True, + timeout=30, + check=False, + ) + if result.stdout: + console.print(result.stdout) + if result.returncode != 0: + if result.stderr: + console.print(result.stderr) + return False + return True + + +def main() -> int: + if os.environ.get("SKIP_JAVA_SDK_PREK_HOOKS"): + console.print("[yellow]Skipping Java SDK prek hooks (SKIP_JAVA_SDK_PREK_HOOKS is set)[/]") + return 0 + + if not TEST_DAGS_YAML.exists(): + console.print(f"[red]test_dags.yaml not found: {TEST_DAGS_YAML}[/]") + return 1 + + java_json_existed = SERIALIZED_JAVA_JSON.exists() + + try: + if not _run_java_serialization(): + return 1 + + with tempfile.TemporaryDirectory(prefix="serialization-check-") as tmp: + python_json = Path(tmp) / "serialized_python.json" + + if not _run_python_serialization(python_json): + return 1 + + if not _run_comparison(python_json, SERIALIZED_JAVA_JSON): + console.print("[red]Serialization compatibility check FAILED[/]") + return 1 + + console.print("[green]Serialization compatibility check PASSED[/]") + return 0 + finally: + # Clean up the Java JSON if it was generated by this run + if not java_json_existed and SERIALIZED_JAVA_JSON.exists(): + SERIALIZED_JAVA_JSON.unlink() + + +if __name__ == "__main__": + sys.exit(main()) From 32b135b4c66a960568aa79e1a37febe33e0b6a61 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 22 Apr 2026 14:01:08 +0800 Subject: [PATCH 69/80] Breeze start-airflow setup with Java SDK --- .../tests/airflow_e2e_tests/conftest.py | 128 ++++--------- .../airflow_breeze/commands/common_options.py | 9 + .../commands/developer_commands.py | 4 + .../src/airflow_breeze/global_constants.py | 3 + .../src/airflow_breeze/params/shell_params.py | 3 + .../utils/docker_command_utils.py | 1 + scripts/ci/docker-compose/local.yml | 3 + scripts/docker/entrypoint_ci.sh | 24 +++ scripts/in_container/java_sdk_build.sh | 172 ++++++++++++++++++ scripts/in_container/java_sdk_setup.sh | 2 - 10 files changed, 254 insertions(+), 95 deletions(-) create mode 100755 scripts/in_container/java_sdk_build.sh diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py index b32f1990c323a..7b792b6e916da 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py @@ -23,13 +23,14 @@ import tempfile from datetime import datetime from pathlib import Path -from shutil import copyfile, copytree, ignore_patterns +from shutil import copyfile, copytree import pytest from rich.console import Console from testcontainers.compose import DockerCompose from airflow_e2e_tests.constants import ( + AIRFLOW_ROOT_PATH, AWS_INIT_PATH, DOCKER_COMPOSE_HOST_PORT, DOCKER_COMPOSE_PATH, @@ -201,111 +202,52 @@ def _download_jdk(dest_dir: Path) -> Path: return openjdk_dir -def _build_java_example(java_sdk_dir: Path) -> list[Path]: - """Build the Java SDK example project using Gradle and return paths to all built JARs.""" - gradlew = java_sdk_dir / "gradlew" - if not gradlew.exists(): - raise FileNotFoundError( - f"Gradle wrapper not found at {gradlew}. " - "Make sure the java-sdk directory exists (e.g. on the feature/java-all branch)." - ) - - console.print("[yellow]Formatting Java sources with spotlessApply...") - fmt_result = subprocess.run( - [str(gradlew), "spotlessApply", "--no-configuration-cache"], - cwd=str(java_sdk_dir), - capture_output=True, - text=True, - timeout=300, - check=False, - ) - if fmt_result.returncode != 0: - console.print(f"[red]spotlessApply failed:\n{fmt_result.stdout}\n{fmt_result.stderr}") - raise RuntimeError("Failed to format Java SDK sources") +def _build_java_sdk_bundles(tmp_dir: Path) -> Path: + """Build both Java SDK bundles (stub + pure Java) using the shared build script. + + Returns the bundles output directory containing both bundle subdirectories. + The caller's system Java is used for the Gradle build (not the downloaded Linux JDK). + """ + build_script = AIRFLOW_ROOT_PATH / "scripts" / "in_container" / "java_sdk_build.sh" + bundles_dir = tmp_dir / "java-sdk-bundles" + + build_env = { + **os.environ, + "JAVA_SDK_SRC_DIR": str(JAVA_SDK_PATH), + "BUNDLES_OUTPUT_DIR": str(bundles_dir), + "JAVA_STUB_BUNDLE_NAME": JAVA_STUB_BUNDLE_NAME, + "JAVA_PURE_BUNDLE_NAME": JAVA_PURE_BUNDLE_NAME, + "JAVA_STUB_DAG_ID": JAVA_STUB_DAG_ID, + "JAVA_PURE_DAG_ID": JAVA_PURE_DAG_ID, + "JAVA_SDK_DAGS_DIR": str(JAVA_SDK_DAGS_FOLDER), + } - console.print("[yellow]Building Java SDK example with Gradle...") + console.print("[yellow]Building Java SDK bundles via shared build script...") result = subprocess.run( - [str(gradlew), "clean", ":example:installDist", "-x", "test"], - cwd=str(java_sdk_dir), + ["bash", str(build_script)], + env=build_env, capture_output=True, text=True, - timeout=300, + timeout=600, check=False, ) if result.returncode != 0: - console.print(f"[red]Gradle build failed:\n{result.stdout}\n{result.stderr}") - raise RuntimeError("Failed to build Java SDK example") - - console.print("[green]Java SDK example built successfully") - - # The Gradle build produces a full distribution under install/example/lib/ - # containing the example JAR, sdk JAR, and all transitive dependencies. - jar_dir = java_sdk_dir / "example" / "build" / "install" / "example" / "lib" - jars = list(jar_dir.glob("*.jar")) - if not jars: - raise FileNotFoundError(f"No JARs found in {jar_dir} after build") - - return jars - - -def _sed_java_dag_id(java_sdk_dir: Path, dag_id: str): - """Replace the dag_id in JavaExample.java with the given dag_id.""" - java_example = ( - java_sdk_dir - / "example" - / "src" - / "java" - / "org" - / "apache" - / "airflow" - / "example" - / "JavaExample.java" - ) - content = java_example.read_text() - java_example.write_text(content.replace('"java_example"', f'"{dag_id}"')) - console.print(f"[yellow]Set JavaExample.java dag_id to '{dag_id}'") + console.print(f"[red]Java SDK build failed:\n{result.stdout}\n{result.stderr}") + raise RuntimeError("Failed to build Java SDK bundles") + console.print(result.stdout) + + return bundles_dir def _setup_java_sdk_integration(dot_env_file, tmp_dir): - """Set up Java SDK integration: download JDK, build JARs with distinct dag_ids, write env and compose override.""" + """Set up Java SDK integration: download JDK, build bundles, write env and compose override.""" # Download the Linux JDK on the host so containers get it via bind mount openjdk_dir = _download_jdk(tmp_dir) - # Copy java-sdk twice so each build gets its own dag_id without conflicts - ignore = ignore_patterns(".gradle", "build", ".kotlin") - - # Build JAR for the stub DAG bundle - stub_sdk_copy = tmp_dir / "java-sdk-stub" - copytree(JAVA_SDK_PATH, stub_sdk_copy, ignore=ignore) - _sed_java_dag_id(stub_sdk_copy, JAVA_STUB_DAG_ID) - stub_jars = _build_java_example(stub_sdk_copy) - stub_bundle_root = tmp_dir / JAVA_STUB_BUNDLE_NAME - stub_bundle_root.mkdir() - stub_jar_dir = stub_bundle_root / "jar-bundles" - stub_jar_dir.mkdir() - for jar in stub_jars: - copyfile(jar, stub_jar_dir / jar.name) - console.print(f"[yellow]Copied {len(stub_jars)} JAR(s) to {stub_jar_dir}") - - # Build JAR for the pure Java DAG bundle - pure_sdk_copy = tmp_dir / "java-sdk-pure" - copytree(JAVA_SDK_PATH, pure_sdk_copy, ignore=ignore) - _sed_java_dag_id(pure_sdk_copy, JAVA_PURE_DAG_ID) - pure_java_jars = _build_java_example(pure_sdk_copy) - pure_bundle_dir = tmp_dir / JAVA_PURE_BUNDLE_NAME - pure_bundle_dir.mkdir() - for jar in pure_java_jars: - copyfile(jar, pure_bundle_dir / jar.name) - console.print(f"[yellow]Copied {len(pure_java_jars)} JAR(s) to {pure_bundle_dir}") - - # Copy stub_dag.py into the dags subfolder with the matching dag_id - stub_dags_dir = stub_bundle_root / "dags" - stub_dags_dir.mkdir() - copytree(JAVA_SDK_DAGS_FOLDER, stub_dags_dir, dirs_exist_ok=True) - stub_dag = stub_dags_dir / "stub_dag.py" - content = stub_dag.read_text() - stub_dag.write_text(content.replace('"java_example"', f'"{JAVA_STUB_DAG_ID}"')) - console.print(f"[yellow]Set stub_dag.py dag_id to '{JAVA_STUB_DAG_ID}'") + # Build both bundles (stub + pure Java) using the shared script + bundles_dir = _build_java_sdk_bundles(tmp_dir) + stub_bundle_root = bundles_dir / JAVA_STUB_BUNDLE_NAME + pure_bundle_dir = bundles_dir / JAVA_PURE_BUNDLE_NAME # Copy the docker-compose override copyfile(JAVA_SDK_COMPOSE_PATH, tmp_dir / "java-sdk.yml") diff --git a/dev/breeze/src/airflow_breeze/commands/common_options.py b/dev/breeze/src/airflow_breeze/commands/common_options.py index 12d4b3f40d35e..cdfb1eb587d11 100644 --- a/dev/breeze/src/airflow_breeze/commands/common_options.py +++ b/dev/breeze/src/airflow_breeze/commands/common_options.py @@ -32,6 +32,7 @@ ALLOWED_MYSQL_VERSIONS, ALLOWED_POSTGRES_VERSIONS, ALLOWED_PYTHON_MAJOR_MINOR_VERSIONS, + ALLOWED_SDKS, ALLOWED_TERMINAL_MULTIPLEXERS, ALLOWED_TTY, ALLOWED_USE_AIRFLOW_VERSIONS, @@ -611,6 +612,14 @@ def _normalize_platform(ctx: click.core.Context, param: click.core.Option, value envvar="WORKER_TYPE", ) +option_sdk = click.option( + "--sdk", + help="Enable an SDK integration (installs dependencies, compiles the SDK, and configures DAG bundles).", + type=BetterChoice(ALLOWED_SDKS), + multiple=True, + envvar="SDK", +) + # UI E2E Testing Options option_airflow_ui_base_url = click.option( diff --git a/dev/breeze/src/airflow_breeze/commands/developer_commands.py b/dev/breeze/src/airflow_breeze/commands/developer_commands.py index d1d9b30d2699a..30741f91af1db 100644 --- a/dev/breeze/src/airflow_breeze/commands/developer_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/developer_commands.py @@ -69,6 +69,7 @@ option_project_name, option_python, option_run_db_tests_only, + option_sdk, option_skip_db_tests, option_standalone_dag_processor, option_terminal_multiplexer, @@ -577,6 +578,7 @@ def shell( @option_providers_skip_constraints @option_python @option_restart +@option_sdk @option_standalone_dag_processor @option_terminal_multiplexer @option_use_uv @@ -627,6 +629,7 @@ def start_airflow( providers_skip_constraints: bool, python: str, restart: bool, + sdk: tuple[str, ...], skip_assets_compilation: bool, standalone_dag_processor: bool, terminal_multiplexer: str, @@ -732,6 +735,7 @@ def start_airflow( providers_skip_constraints=providers_skip_constraints, python=python, restart=restart, + sdk=sdk, skip_assets_compilation=skip_assets_compilation, standalone_dag_processor=standalone_dag_processor, start_airflow=True, diff --git a/dev/breeze/src/airflow_breeze/global_constants.py b/dev/breeze/src/airflow_breeze/global_constants.py index 4deaa3bf598b9..fdc09cb04981f 100644 --- a/dev/breeze/src/airflow_breeze/global_constants.py +++ b/dev/breeze/src/airflow_breeze/global_constants.py @@ -196,6 +196,9 @@ GOLANG_WORKER = "go" +JAVA_SDK = "java" +ALLOWED_SDKS = [JAVA_SDK] + DEFAULT_ALLOWED_EXECUTOR = ALLOWED_EXECUTORS[0] ALLOWED_AUTH_MANAGERS = [SIMPLE_AUTH_MANAGER, FAB_AUTH_MANAGER] START_AIRFLOW_ALLOWED_EXECUTORS = [LOCAL_EXECUTOR, CELERY_EXECUTOR, EDGE_EXECUTOR] diff --git a/dev/breeze/src/airflow_breeze/params/shell_params.py b/dev/breeze/src/airflow_breeze/params/shell_params.py index 4905fb200136f..3141afe82a017 100644 --- a/dev/breeze/src/airflow_breeze/params/shell_params.py +++ b/dev/breeze/src/airflow_breeze/params/shell_params.py @@ -229,6 +229,7 @@ class ShellParams: restart: bool = False run_db_tests_only: bool = False run_tests: bool = False + sdk: tuple[str, ...] = () skip_assets_compilation: bool = False skip_db_tests: bool = False skip_environment_initialization: bool = False @@ -663,6 +664,8 @@ def env_variables_for_docker_commands(self) -> dict[str, str]: _set_var(_env, "GITHUB_TOKEN", self.github_token) if "go" in self.worker_type: _set_var(_env, "GO_WORKER", True) + if "java" in self.sdk: + _set_var(_env, "JAVA_SDK", True) _set_var(_env, "HOST_GROUP_ID", self.host_group_id) _set_var(_env, "HOST_OS", self.host_os) _set_var(_env, "HOST_USER_ID", self.host_user_id) 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 19d787f2967b6..479a41944812f 100644 --- a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py +++ b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py @@ -101,6 +101,7 @@ ("docs", "/opt/airflow/docs"), ("generated", "/opt/airflow/generated"), ("go-sdk", "/opt/airflow/go-sdk"), + ("java-sdk", "/opt/airflow/java-sdk"), ("helm-tests", "/opt/airflow/helm-tests"), ("kubernetes-tests", "/opt/airflow/kubernetes-tests"), ("logs", "/root/airflow/logs"), diff --git a/scripts/ci/docker-compose/local.yml b/scripts/ci/docker-compose/local.yml index a59fd4fd8e9cd..9b3430df01f44 100644 --- a/scripts/ci/docker-compose/local.yml +++ b/scripts/ci/docker-compose/local.yml @@ -96,6 +96,9 @@ services: - type: bind source: ../../../go-sdk target: /opt/airflow/go-sdk + - type: bind + source: ../../../java-sdk + target: /opt/airflow/java-sdk - type: bind source: ../../../helm-tests target: /opt/airflow/helm-tests diff --git a/scripts/docker/entrypoint_ci.sh b/scripts/docker/entrypoint_ci.sh index 03f761f858471..31ee591ac404a 100755 --- a/scripts/docker/entrypoint_ci.sh +++ b/scripts/docker/entrypoint_ci.sh @@ -143,6 +143,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 language 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/scripts/in_container/java_sdk_build.sh b/scripts/in_container/java_sdk_build.sh new file mode 100755 index 0000000000000..4060992777e98 --- /dev/null +++ b/scripts/in_container/java_sdk_build.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +# 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. + +# Builds Java SDK bundles (stub + pure Java) from the java-sdk source tree. +# +# Creates two bundle directories under BUNDLES_OUTPUT_DIR: +# python-stub-java-tasks/ +# dags/ - Python stub DAGs with Java task stubs +# jar-bundles/ - compiled JARs for the Java coordinator +# pure-java/ - compiled JARs only (DAG defined entirely in Java) +# +# Each bundle gets a distinct DAG ID so both can run simultaneously. +# +# Usage: +# export JAVA_SDK_SRC_DIR=/path/to/java-sdk +# export BUNDLES_OUTPUT_DIR=/path/to/output +# bash java_sdk_build.sh +# +# After running, source the generated env file to configure Airflow: +# source ${BUNDLES_OUTPUT_DIR}/java_sdk_env.sh +# +# Required environment variables: +# JAVA_SDK_SRC_DIR - path to the java-sdk source tree +# BUNDLES_OUTPUT_DIR - directory where bundle dirs will be created +# +# Optional environment variables: +# JAVA_STUB_BUNDLE_NAME - stub bundle directory name (default: python-stub-java-tasks) +# JAVA_PURE_BUNDLE_NAME - pure Java bundle directory name (default: pure-java) +# JAVA_STUB_DAG_ID - DAG ID for the stub bundle (default: java_sdk_stub_example) +# JAVA_PURE_DAG_ID - DAG ID for the pure Java bundle (default: java_sdk_pure_java_example) +# JAVA_SDK_DAGS_DIR - path to Python stub DAG files (default: JAVA_SDK_SRC_DIR/dags) + +set -euo pipefail + +COLOR_BLUE='\033[0;34m' +COLOR_GREEN='\033[0;32m' +COLOR_RESET='\033[0m' + +# Required +JAVA_SDK_SRC_DIR="${JAVA_SDK_SRC_DIR:?JAVA_SDK_SRC_DIR must be set}" +BUNDLES_OUTPUT_DIR="${BUNDLES_OUTPUT_DIR:?BUNDLES_OUTPUT_DIR must be set}" + +# Optional with defaults +JAVA_STUB_BUNDLE_NAME="${JAVA_STUB_BUNDLE_NAME:-python-stub-java-tasks}" +JAVA_PURE_BUNDLE_NAME="${JAVA_PURE_BUNDLE_NAME:-pure-java}" +JAVA_STUB_DAG_ID="${JAVA_STUB_DAG_ID:-java_sdk_stub_example}" +JAVA_PURE_DAG_ID="${JAVA_PURE_DAG_ID:-java_sdk_pure_java_example}" +JAVA_SDK_DAGS_DIR="${JAVA_SDK_DAGS_DIR:-${JAVA_SDK_SRC_DIR}/dags}" + +WORK_DIR=$(mktemp -d) +trap 'rm -rf "${WORK_DIR}"' EXIT + +echo -e "${COLOR_BLUE}Building Java SDK bundles...${COLOR_RESET}" +echo " Source: ${JAVA_SDK_SRC_DIR}" +echo " Output: ${BUNDLES_OUTPUT_DIR}" + +# Portable sed -i (macOS requires a backup extension, Linux does not) +sed_inplace() { + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +# Copy java-sdk source excluding build artifacts +copy_sdk_source() { + local dest="$1" + mkdir -p "${dest}" + tar -cf - -C "${JAVA_SDK_SRC_DIR}" \ + --exclude='.gradle' --exclude='build' --exclude='.kotlin' . \ + | tar -xf - -C "${dest}" +} + +# Replace the DAG ID in JavaExample.java +set_java_dag_id() { + local sdk_dir="$1" + local dag_id="$2" + local java_file="${sdk_dir}/example/src/java/org/apache/airflow/example/JavaExample.java" + sed_inplace "s/\"java_example\"/\"${dag_id}\"/" "${java_file}" + echo " Set JavaExample.java dag_id -> '${dag_id}'" +} + +# Build the Java SDK example project with Gradle +build_example() { + local sdk_dir="$1" + local gradlew="${sdk_dir}/gradlew" + + if [[ ! -x "${gradlew}" ]]; then + chmod +x "${gradlew}" + fi + + echo -e "${COLOR_BLUE} Running spotlessApply...${COLOR_RESET}" + (cd "${sdk_dir}" && ./gradlew spotlessApply --no-configuration-cache -q) + + echo -e "${COLOR_BLUE} Running :example:installDist...${COLOR_RESET}" + (cd "${sdk_dir}" && ./gradlew clean :example:installDist -x test --no-configuration-cache -q) +} + +# Path to built JARs after installDist +jars_dir() { + echo "$1/example/build/install/example/lib" +} + +mkdir -p "${BUNDLES_OUTPUT_DIR}" + +# ---- Build stub bundle ---- +echo -e "\n${COLOR_BLUE}=== Building stub bundle (${JAVA_STUB_BUNDLE_NAME}) ===${COLOR_RESET}" +STUB_COPY="${WORK_DIR}/java-sdk-stub" +copy_sdk_source "${STUB_COPY}" +set_java_dag_id "${STUB_COPY}" "${JAVA_STUB_DAG_ID}" +build_example "${STUB_COPY}" + +STUB_BUNDLE="${BUNDLES_OUTPUT_DIR}/${JAVA_STUB_BUNDLE_NAME}" +mkdir -p "${STUB_BUNDLE}/jar-bundles" "${STUB_BUNDLE}/dags" +cp "$(jars_dir "${STUB_COPY}")"/*.jar "${STUB_BUNDLE}/jar-bundles/" + +# Copy and patch stub DAGs +cp "${JAVA_SDK_DAGS_DIR}"/*.py "${STUB_BUNDLE}/dags/" +sed_inplace "s/\"java_example\"/\"${JAVA_STUB_DAG_ID}\"/" "${STUB_BUNDLE}/dags/stub_dag.py" +echo " Set stub_dag.py dag_id -> '${JAVA_STUB_DAG_ID}'" + +STUB_JAR_COUNT=$(find "${STUB_BUNDLE}/jar-bundles" -name '*.jar' | wc -l | tr -d ' ') +echo -e "${COLOR_GREEN} Stub bundle: ${STUB_JAR_COUNT} JARs + DAGs${COLOR_RESET}" + +# ---- Build pure Java bundle ---- +echo -e "\n${COLOR_BLUE}=== Building pure Java bundle (${JAVA_PURE_BUNDLE_NAME}) ===${COLOR_RESET}" +PURE_COPY="${WORK_DIR}/java-sdk-pure" +copy_sdk_source "${PURE_COPY}" +set_java_dag_id "${PURE_COPY}" "${JAVA_PURE_DAG_ID}" +build_example "${PURE_COPY}" + +PURE_BUNDLE="${BUNDLES_OUTPUT_DIR}/${JAVA_PURE_BUNDLE_NAME}" +mkdir -p "${PURE_BUNDLE}" +cp "$(jars_dir "${PURE_COPY}")"/*.jar "${PURE_BUNDLE}/" + +PURE_JAR_COUNT=$(find "${PURE_BUNDLE}" -name '*.jar' | wc -l | tr -d ' ') +echo -e "${COLOR_GREEN} Pure Java bundle: ${PURE_JAR_COUNT} JARs${COLOR_RESET}" + +# ---- Generate env file ---- +# This file can be sourced to configure Airflow with both DAG bundles. +# For breeze (in-container), source it directly. +# For e2e tests, the caller sets container paths separately via docker-compose. +ENV_FILE="${BUNDLES_OUTPUT_DIR}/java_sdk_env.sh" +cat > "${ENV_FILE}" < Date: Thu, 23 Apr 2026 10:44:18 +0800 Subject: [PATCH 70/80] Rename stub operator language field as sdk --- .../execution_api/datamodels/taskinstance.py | 2 +- .../api_fastapi/execution_api/versions/v2026_07_01.py | 6 +++--- airflow-core/src/airflow/executors/workloads/task.py | 2 +- ...py => 0112_3_3_0_add_sdk_field_to_task_instance.py} | 10 +++++----- airflow-core/src/airflow/models/taskinstance.py | 6 +++--- .../airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py | 4 ++-- java-sdk/dags/stub_dag.py | 4 ++-- .../kotlin/org/apache/airflow/sdk/execution/Serde.kt | 2 +- task-sdk/src/airflow/sdk/bases/operator.py | 6 +++--- task-sdk/src/airflow/sdk/definitions/mappedoperator.py | 4 ++-- 10 files changed, 23 insertions(+), 23 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/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 191d7406f8f05..131ed0cf4bd73 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -563,7 +563,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) @@ -751,7 +751,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, @@ -922,7 +922,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-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py b/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py index 65a0832e6a8d3..bd606e58e5976 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py @@ -27,11 +27,11 @@ def python_task_1(ti): ti.xcom_push(value="value-pushed-from-python_task_1", key="return_value") -@task.stub(language="java") +@task.stub(sdk="java") def extract(): ... -@task.stub(language="java") +@task.stub(sdk="java") def transform(): ... diff --git a/java-sdk/dags/stub_dag.py b/java-sdk/dags/stub_dag.py index 65a0832e6a8d3..bd606e58e5976 100644 --- a/java-sdk/dags/stub_dag.py +++ b/java-sdk/dags/stub_dag.py @@ -27,11 +27,11 @@ def python_task_1(ti): ti.xcom_push(value="value-pushed-from-python_task_1", key="return_value") -@task.stub(language="java") +@task.stub(sdk="java") def extract(): ... -@task.stub(language="java") +@task.stub(sdk="java") def transform(): ... diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Serde.kt b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Serde.kt index 21a398298662e..4f44b42b9bd6b 100644 --- a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Serde.kt +++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Serde.kt @@ -128,7 +128,7 @@ private fun Class.serialize( "task_id" to id, "task_type" to simpleName, "_task_module" to name.substringBeforeLast('.'), - "language" to "java", + "sdk" to "java", ) if (!dependants.isNullOrEmpty()) { data["downstream_task_ids"] = dependants.sorted() 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]: From fb815c3a6acbb19be18b93129492ff950ad4c0a3 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 11:00:33 +0800 Subject: [PATCH 71/80] Cache gradle deps for breeze start-airflow --- scripts/in_container/java_sdk_build.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/in_container/java_sdk_build.sh b/scripts/in_container/java_sdk_build.sh index 4060992777e98..7425dd05ad8f9 100755 --- a/scripts/in_container/java_sdk_build.sh +++ b/scripts/in_container/java_sdk_build.sh @@ -39,6 +39,7 @@ # BUNDLES_OUTPUT_DIR - directory where bundle dirs will be created # # Optional environment variables: +# GRADLE_USER_HOME - Gradle home for caching wrapper/deps (default: /files/.gradle) # JAVA_STUB_BUNDLE_NAME - stub bundle directory name (default: python-stub-java-tasks) # JAVA_PURE_BUNDLE_NAME - pure Java bundle directory name (default: pure-java) # JAVA_STUB_DAG_ID - DAG ID for the stub bundle (default: java_sdk_stub_example) @@ -55,6 +56,10 @@ COLOR_RESET='\033[0m' JAVA_SDK_SRC_DIR="${JAVA_SDK_SRC_DIR:?JAVA_SDK_SRC_DIR must be set}" BUNDLES_OUTPUT_DIR="${BUNDLES_OUTPUT_DIR:?BUNDLES_OUTPUT_DIR must be set}" +# Cache Gradle wrapper downloads and dependencies on the persistent /files mount +# so they survive container restarts and avoid re-downloading on every build. +export GRADLE_USER_HOME="${GRADLE_USER_HOME:-/files/.gradle}" + # Optional with defaults JAVA_STUB_BUNDLE_NAME="${JAVA_STUB_BUNDLE_NAME:-python-stub-java-tasks}" JAVA_PURE_BUNDLE_NAME="${JAVA_PURE_BUNDLE_NAME:-pure-java}" From 4688d77a571f70737b238f9d726929489d580695 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 11:34:35 +0800 Subject: [PATCH 72/80] 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/docker/entrypoint_ci.sh | 2 +- .../test_providers_manager_runtime.py | 12 +++++----- 31 files changed, 98 insertions(+), 74 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 3f8df8a6efab7..0968ae9e0e91b 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 71891f1b4f0de..57ca40e04d75c 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", @@ -1174,8 +1174,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", @@ -1224,6 +1222,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", @@ -1443,7 +1443,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 @@ -1468,6 +1467,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 @@ -1595,7 +1595,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 @@ -1620,6 +1619,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 @@ -1757,7 +1757,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 } @@ -1782,6 +1781,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 } @@ -1896,7 +1896,6 @@ members = [ "providers/jdbc", "providers/jenkins", "providers/keycloak", - "providers/languages/java", "providers/microsoft/azure", "providers/microsoft/mssql", "providers/microsoft/psrp", @@ -1921,6 +1920,7 @@ members = [ "providers/redis", "providers/salesforce", "providers/samba", + "providers/sdk/java", "providers/segment", "providers/sendgrid", "providers/sftp", diff --git a/scripts/docker/entrypoint_ci.sh b/scripts/docker/entrypoint_ci.sh index 31ee591ac404a..5f10ca08ee687 100755 --- a/scripts/docker/entrypoint_ci.sh +++ b/scripts/docker/entrypoint_ci.sh @@ -148,7 +148,7 @@ function environment_initialization() { echo "${COLOR_BLUE}Setting up Java SDK${COLOR_RESET}" echo - # Install Java and the Java language provider + # 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 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", ] }, ) From 19348e3ac9862cefe5bca344535c52d946344dc7 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 21 Apr 2026 18:37:27 +0800 Subject: [PATCH 73/80] Move TaskInstanceDTO to share to make task_runner retrieve TI.queue --- airflow-core/pyproject.toml | 2 + airflow-core/src/airflow/_shared/workloads | 1 + 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 | 1 + .../airflow/sdk/execution_time/supervisor.py | 1 + .../execution_time/test_supervisor.py | 1 + 12 files changed, 2243 insertions(+) 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 a875d9e9c77e7..0d4d45d7e40db 100644 --- a/airflow-core/pyproject.toml +++ b/airflow-core/pyproject.toml @@ -249,6 +249,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" @@ -337,6 +338,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/pyproject.toml b/pyproject.toml index 57ca40e04d75c..4925ff4be3143 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1335,6 +1335,7 @@ dev = [ "apache-airflow-shared-state", "apache-airflow-shared-template-rendering", "apache-airflow-shared-timezones", + "apache-airflow-shared-workloads", ] # To build docs: @@ -1503,6 +1504,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 @@ -1655,6 +1657,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 @@ -1700,6 +1703,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 } @@ -1839,6 +1843,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 6e4a0b1017d01..ced299cc41bd0 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" @@ -317,6 +318,7 @@ shared_distributions = [ "apache-airflow-shared-secrets-masker", "apache-airflow-shared-serialization", "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 b407e71a9891f..799230737426c 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, diff --git a/task-sdk/src/airflow/sdk/execution_time/supervisor.py b/task-sdk/src/airflow/sdk/execution_time/supervisor.py index 75ac2ce784673..58576c9a70f9a 100644 --- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py +++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py @@ -142,6 +142,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 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 332dd1b537d8f..1f51ad274babf 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 BaseOperator, timezone from airflow.sdk.api import client as sdk_client From 04fa3cfc1a00c4bba845d21bac251857500ccac1 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Wed, 22 Apr 2026 14:16:33 +0800 Subject: [PATCH 74/80] Add [workers/queue_to_runtime_mapping] --- airflow-e2e-tests/docker/java-sdk.yml | 81 ++++++++++++++++--- .../tests/airflow_e2e_tests/conftest.py | 3 + .../java_sdk_tests/dags/stub_dag.py | 3 +- .../src/airflow_shared/workloads/__init__.py | 2 +- 4 files changed, 76 insertions(+), 13 deletions(-) diff --git a/airflow-e2e-tests/docker/java-sdk.yml b/airflow-e2e-tests/docker/java-sdk.yml index 025653e8265dc..947d673c56d20 100644 --- a/airflow-e2e-tests/docker/java-sdk.yml +++ b/airflow-e2e-tests/docker/java-sdk.yml @@ -17,7 +17,8 @@ # # Docker Compose override for Java SDK e2e tests. # -# Overrides the dag-processor and worker services to: +# Overrides the dag-processor and worker services and adds a dedicated +# Java queue worker to: # 1. Mount a pre-downloaded JDK, the Python stub bundle, and the pure Java bundle. # 2. Set JAVA_HOME and PATH so the Java coordinator can find the JDK. # @@ -28,25 +29,83 @@ # JAVA_STUB_BUNDLE_PATH – host path to the stub DAG bundle root (dags/ + jar-bundles/) # JAVA_BUNDLES_PATH – host path to the pure Java DAG bundle (JARs only) --- +x-java-env: &java-env + AIRFLOW__CORE__LOAD_EXAMPLES: "false" + JAVA_HOME: /files/openjdk + PATH: /files/openjdk/bin:/home/airflow/.local/bin:/usr/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +x-java-volumes: &java-volumes + - ${JAVA_OPENJDK_PATH}:/files/openjdk + - ${JAVA_STUB_BUNDLE_PATH}:/files/python-stub-java-tasks + - ${JAVA_BUNDLES_PATH}:/files/pure-java + services: airflow-dag-processor: user: "0:0" environment: - AIRFLOW__CORE__LOAD_EXAMPLES: "false" - JAVA_HOME: /files/openjdk - PATH: /files/openjdk/bin:/home/airflow/.local/bin:/usr/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - volumes: - - ${JAVA_OPENJDK_PATH}:/files/openjdk - - ${JAVA_STUB_BUNDLE_PATH}:/files/python-stub-java-tasks - - ${JAVA_BUNDLES_PATH}:/files/pure-java + <<: *java-env + volumes: *java-volumes airflow-worker: user: "0:0" environment: - AIRFLOW__CORE__LOAD_EXAMPLES: "false" - JAVA_HOME: /files/openjdk - PATH: /files/openjdk/bin:/home/airflow/.local/bin:/usr/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + <<: *java-env + volumes: *java-volumes + + # Dedicated Celery worker that only consumes from "java-queue". + # Since this is a new service (not an override of the base docker-compose), + # it needs the full service definition including the common Airflow config + # that the base compose provides via x-airflow-common. + # AIRFLOW__WORKERS__QUEUE_TO_RUNTIME_MAPPING is picked up from the .env file + # via env_file (set by conftest.py). + airflow-java-worker: + image: ${AIRFLOW_IMAGE_NAME:-apache/airflow:|version|} + env_file: + - ${ENV_FILE_PATH:-.env} + command: celery worker --queues java-queue + user: "0:0" + environment: + <<: *java-env + # Mirrors x-airflow-common-env from the base docker-compose.yaml + AIRFLOW__CORE__EXECUTOR: CeleryExecutor + AIRFLOW__CORE__AUTH_MANAGER: airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager + AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow + AIRFLOW__CELERY__RESULT_BACKEND: db+postgresql+psycopg2://airflow:airflow@postgres/airflow + AIRFLOW__CELERY__BROKER_URL: redis://:@redis:6379/0 + AIRFLOW__CORE__FERNET_KEY: ${FERNET_KEY} + AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION: 'true' + AIRFLOW__CORE__EXECUTION_API_SERVER_URL: 'http://airflow-apiserver:8080/execution/' + AIRFLOW__API_AUTH__JWT_SECRET: ${AIRFLOW__API_AUTH__JWT_SECRET:-airflow_jwt_secret} + AIRFLOW__API_AUTH__JWT_ISSUER: ${AIRFLOW__API_AUTH__JWT_ISSUER:-airflow} + AIRFLOW__SCHEDULER__ENABLE_HEALTH_CHECK: 'true' + _PIP_ADDITIONAL_REQUIREMENTS: ${_PIP_ADDITIONAL_REQUIREMENTS:-} + AIRFLOW_CONFIG: '/opt/airflow/config/airflow.cfg' + # Required to handle warm shutdown of the celery workers properly + # See https://airflow.apache.org/docs/docker-stack/entrypoint.html#signal-propagation + DUMB_INIT_SETSID: "0" volumes: + - ${AIRFLOW_PROJ_DIR:-.}/dags:/opt/airflow/dags + - ${AIRFLOW_PROJ_DIR:-.}/logs:/opt/airflow/logs + - ${AIRFLOW_PROJ_DIR:-.}/config:/opt/airflow/config + - ${AIRFLOW_PROJ_DIR:-.}/plugins:/opt/airflow/plugins - ${JAVA_OPENJDK_PATH}:/files/openjdk - ${JAVA_STUB_BUNDLE_PATH}:/files/python-stub-java-tasks - ${JAVA_BUNDLES_PATH}:/files/pure-java + healthcheck: + # yamllint disable rule:line-length + test: ["CMD-SHELL", 'celery --app airflow.providers.celery.executors.celery_executor.app inspect ping -d "celery@$${HOSTNAME}" || celery --app airflow.executors.celery_executor.app inspect ping -d "celery@$${HOSTNAME}"'] + # yamllint enable rule:line-length + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + restart: always + depends_on: + redis: + condition: service_healthy + postgres: + condition: service_healthy + airflow-apiserver: + condition: service_healthy + airflow-init: + condition: service_completed_successfully diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py index 7b792b6e916da..3d86f00d3635a 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py @@ -272,10 +272,13 @@ def _setup_java_sdk_integration(dot_env_file, tmp_dir): }, ] ) + queue_to_runtime_mapping = json.dumps({"java-queue": "java"}) dot_env_file.write_text( f"AIRFLOW_UID={os.getuid()}\n" "AIRFLOW__CORE__LOAD_EXAMPLES=false\n" + "AIRFLOW__LOGGING__LOGGING_LEVEL=DEBUG\n" + f"AIRFLOW__WORKERS__QUEUE_TO_RUNTIME_MAPPING={queue_to_runtime_mapping}\n" f"AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST={bundle_config}\n" f"AIRFLOW__JAVA__BUNDLES_FOLDER={JAVA_CONTAINER_STUB_JAVA_BUNDLES_FOLDER_PATH}\n" ) diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py b/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py index bd606e58e5976..34f6a5aba4516 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py @@ -31,7 +31,8 @@ def python_task_1(ti): def extract(): ... -@task.stub(sdk="java") +# If the worker set [workers/queue_to_runtime_mapping] to map "java-queue" to "java" runtime, then the task with queue="java-queue" will be executed in Java runtime. +@task.stub(queue="java-queue") def transform(): ... 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 From bc6eb1709af7769d44d1e3d80336da2a4cf1831e Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 23 Apr 2026 20:29:29 +0800 Subject: [PATCH 75/80] 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 | 2 - ...12_3_3_0_add_sdk_field_to_task_instance.py | 50 ------------------- .../src/airflow/models/taskinstance.py | 3 -- .../java_sdk_tests/dags/stub_dag.py | 3 +- java-sdk/dags/stub_dag.py | 4 +- scripts/in_container/java_sdk_build.sh | 1 + .../src/airflow_shared/workloads/__init__.py | 2 - task-sdk/src/airflow/sdk/bases/operator.py | 3 -- 10 files changed, 4 insertions(+), 95 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 41ec428d4fb0a..9af3f33c10efd 100644 --- a/airflow-core/src/airflow/executors/workloads/task.py +++ b/airflow-core/src/airflow/executors/workloads/task.py @@ -55,8 +55,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 131ed0cf4bd73..e5b19f2768da9 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -563,7 +563,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) @@ -751,7 +750,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, @@ -922,7 +920,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/java_sdk_tests/dags/stub_dag.py b/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py index 34f6a5aba4516..5bc41d1001640 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/java_sdk_tests/dags/stub_dag.py @@ -27,11 +27,10 @@ def python_task_1(ti): ti.xcom_push(value="value-pushed-from-python_task_1", key="return_value") -@task.stub(sdk="java") +@task.stub(queue="java-queue") def extract(): ... -# If the worker set [workers/queue_to_runtime_mapping] to map "java-queue" to "java" runtime, then the task with queue="java-queue" will be executed in Java runtime. @task.stub(queue="java-queue") def transform(): ... diff --git a/java-sdk/dags/stub_dag.py b/java-sdk/dags/stub_dag.py index bd606e58e5976..5bc41d1001640 100644 --- a/java-sdk/dags/stub_dag.py +++ b/java-sdk/dags/stub_dag.py @@ -27,11 +27,11 @@ def python_task_1(ti): ti.xcom_push(value="value-pushed-from-python_task_1", key="return_value") -@task.stub(sdk="java") +@task.stub(queue="java-queue") def extract(): ... -@task.stub(sdk="java") +@task.stub(queue="java-queue") def transform(): ... diff --git a/scripts/in_container/java_sdk_build.sh b/scripts/in_container/java_sdk_build.sh index 7425dd05ad8f9..644bd8fea1abb 100755 --- a/scripts/in_container/java_sdk_build.sh +++ b/scripts/in_container/java_sdk_build.sh @@ -164,6 +164,7 @@ echo -e "${COLOR_GREEN} Pure Java bundle: ${PURE_JAR_COUNT} JARs${COLOR_RESET}" ENV_FILE="${BUNDLES_OUTPUT_DIR}/java_sdk_env.sh" cat > "${ENV_FILE}" < Date: Thu, 23 Apr 2026 20:52:27 +0800 Subject: [PATCH 76/80] Rename `[workers] queue_to_runtime_mapping` to `[sdk] queue_to_sdk` --- airflow-e2e-tests/docker/java-sdk.yml | 2 +- airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py | 4 ++-- scripts/in_container/java_sdk_build.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/airflow-e2e-tests/docker/java-sdk.yml b/airflow-e2e-tests/docker/java-sdk.yml index 947d673c56d20..e707245a8b6ec 100644 --- a/airflow-e2e-tests/docker/java-sdk.yml +++ b/airflow-e2e-tests/docker/java-sdk.yml @@ -56,7 +56,7 @@ services: # Since this is a new service (not an override of the base docker-compose), # it needs the full service definition including the common Airflow config # that the base compose provides via x-airflow-common. - # AIRFLOW__WORKERS__QUEUE_TO_RUNTIME_MAPPING is picked up from the .env file + # AIRFLOW__SDK__QUEUE_TO_SDK is picked up from the .env file # via env_file (set by conftest.py). airflow-java-worker: image: ${AIRFLOW_IMAGE_NAME:-apache/airflow:|version|} diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py index 3d86f00d3635a..b679c6d75fae6 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py @@ -272,13 +272,13 @@ def _setup_java_sdk_integration(dot_env_file, tmp_dir): }, ] ) - queue_to_runtime_mapping = json.dumps({"java-queue": "java"}) + queue_to_sdk = json.dumps({"java-queue": "java"}) dot_env_file.write_text( f"AIRFLOW_UID={os.getuid()}\n" "AIRFLOW__CORE__LOAD_EXAMPLES=false\n" "AIRFLOW__LOGGING__LOGGING_LEVEL=DEBUG\n" - f"AIRFLOW__WORKERS__QUEUE_TO_RUNTIME_MAPPING={queue_to_runtime_mapping}\n" + f"AIRFLOW__SDK__QUEUE_TO_SDK={queue_to_sdk}\n" f"AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST={bundle_config}\n" f"AIRFLOW__JAVA__BUNDLES_FOLDER={JAVA_CONTAINER_STUB_JAVA_BUNDLES_FOLDER_PATH}\n" ) diff --git a/scripts/in_container/java_sdk_build.sh b/scripts/in_container/java_sdk_build.sh index 644bd8fea1abb..42198cedd9e54 100755 --- a/scripts/in_container/java_sdk_build.sh +++ b/scripts/in_container/java_sdk_build.sh @@ -164,7 +164,7 @@ echo -e "${COLOR_GREEN} Pure Java bundle: ${PURE_JAR_COUNT} JARs${COLOR_RESET}" ENV_FILE="${BUNDLES_OUTPUT_DIR}/java_sdk_env.sh" cat > "${ENV_FILE}" < Date: Fri, 24 Apr 2026 19:32:49 +0800 Subject: [PATCH 77/80] Simplify coordinator-related names (#1569) Tweak coordinator class names, attribute names, and method names to be shorter and avoid the term 'runtime'. --- .../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 ++--- .../airflow/sdk/providers_manager_runtime.py | 28 ++++++++--------- .../test_providers_manager_runtime.py | 6 ++-- 10 files changed, 51 insertions(+), 51 deletions(-) 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/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/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 21dbbda4e3d809156af72deb9dbf65d63ed87b67 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Tue, 28 Apr 2026 08:52:27 +0800 Subject: [PATCH 78/80] Add ADRs for JavaSDK and Coordinator --- .../adr/0001-java-sdk-airflow-integration.md | 343 +++++++++++++ java-sdk/adr/0002-dag-parsing.md | 370 ++++++++++++++ java-sdk/adr/0003-workload-execution.md | 473 ++++++++++++++++++ java-sdk/adr/0004-pure-java-dags.md | 228 +++++++++ 4 files changed, 1414 insertions(+) create mode 100644 java-sdk/adr/0001-java-sdk-airflow-integration.md create mode 100644 java-sdk/adr/0002-dag-parsing.md create mode 100644 java-sdk/adr/0003-workload-execution.md create mode 100644 java-sdk/adr/0004-pure-java-dags.md diff --git a/java-sdk/adr/0001-java-sdk-airflow-integration.md b/java-sdk/adr/0001-java-sdk-airflow-integration.md new file mode 100644 index 0000000000000..1609e99a413e2 --- /dev/null +++ b/java-sdk/adr/0001-java-sdk-airflow-integration.md @@ -0,0 +1,343 @@ + + +# ADR-0001: Java SDK Airflow Integration + +## Status + +Accepted + +## Context + +Airflow's current execution model is Python-only: DAGs are Python files, tasks are Python callables, and the task runner forks a Python process. To support DAGs and tasks authored in other languages (starting with Java), we need an architecture that: + +- Allows entire DAGs to be written in a non-Python language (pure Java DAG). +- Allows non-Python tasks to coexist with Python tasks in the same DAG (`@task.stub`). +- Reuses the existing task-runner two-layer design (task-runner process + forked child process) so Airflow extensions (XCom backends, connections, variables) stay in Python. +- Is extensible to other languages (Go, Rust, etc.) without per-language changes to Airflow Core. + +The existing task runner already uses a two-layer design. When an executor wants to run a task, it starts a task-runner process that talks to Airflow Core through the Execution API, and forks another process that talks to the task-runner through TCP to run the actual task code. All the Airflow extensions simply go into the task-runner process, keeping them in Python. + +The only thing missing is a way for the task-runner process to run tasks in another language. + +## Decision + +### Writing a Non-Python Task + +There is one way to write a non-Python task: implement the language SDK's task interface. For Java, this is the `Task` interface with a single `execute(Client client)` method. The `Client` provides access to Airflow services (connections, variables, XCom). + +### Two Ways to Integrate Non-Python Tasks into a DAG + +We provide two approaches for integrating non-Python tasks into a DAG: + +**a) Pure Java DAG** — define the entire DAG in Java, with no Python file at all. +The Java SDK provides `DagBundle`, `Dag`, and `Task` interfaces: + +```java +public class JavaExample implements DagBundle { + + public static class Extract implements Task { + public void execute(Client client) throws Exception { + var connection = client.getConnection("test_http"); + client.setXCom(new Date().getTime()); + } + } + + public static class Transform implements Task { + public void execute(Client client) { + var extract_xcom = client.getXCom("extract"); + client.setXCom(new Date().getTime()); + } + } + + @Override + public List getDags() { + var dag = new Dag("java_example", null, "@daily"); + dag.addTask("extract", Extract.class, List.of()); + dag.addTask("transform", Transform.class, List.of("extract")); + return List.of(dag); + } +} +``` + +**b) `@task.stub` in a Python DAG** — for mixed-language pipelines where Python and +Java tasks coexist in the same DAG. The `@task.stub` syntax is already supported for +the Go SDK; the same pattern applies to Java: + +```python +@task() +def python_task_1(ti): + ti.xcom_push(value="from-python", key="return_value") + + +@task.stub(queue="java") +def extract(): ... + + +@task.stub(queue="java") +def transform(): ... + + +@dag(dag_id="java_example") +def simple_dag(): + python_task_1() >> extract() >> transform() +``` + +Both approaches are supported in parallel. A pure Java DAG needs no Python at all for authoring. A `@task.stub` DAG requires a Python file but lets you mix Python operators and non-Python tasks in a single pipeline. + +> **Note:** The current `DagBundle` interface used in pure Java DAGs is subject to review before the SDK reaches 1.0. Subclassing `Dag` directly may be a more natural fit and is being considered for post-OSS-integration. + +### The Coordinator Layer + +We introduce a **Coordinator** layer. When a DAG bundle is loaded, it not only tells Airflow how to find the DAGs (and tasks in them), but also how to *run* each task. Current Python tasks use a Python code path that runs them by forking. A new **Java Coordinator** instructs the task runner how to run tasks in JAR files. + +The base interface (`BaseCoordinator`) lives in `airflow.sdk.execution_time` and is selected automatically via `ProvidersManagerTaskRuntime`. The Java Coordinator lives in a provider under the `airflow.providers.sdk.java` namespace, and new language coordinators follow the same pattern. + +### Architecture Overview + +``` + Airflow Backend Language Runtime Subprocess (Java in this example) + ─────────────── ────────────────────────────────────────────────── + + ┌──────────────────────────────┐ + │ DAG File (Python or JAR) │ + │ │ + │ @task.stub(queue="java") │ + │ def my_java_task(): │ + │ ... │ + └──────────────┬───────────────┘ + │ + ┌──────────────▼───────────────┐ ┌──────────────────────────────┐ + │ DAG File Processor │ │ Runtime Subprocess (Java) │ + │ │ can_handle_dag │ │ + │ For each file in bundle: │ _file() == True │ dag_parsing_cmd() │ + │ ┌ coordinator handles it? ──┼───────────────────►│ │ + │ │ Yes ──► delegate parse │ │ Java SDK parses JAR, builds │ + │ │ No ──► Python path │ SDK Serialized │ SDK-compatible Serialized │ + │ │ │◄─── DAG JSON ──────┤ DAG JSON (sdk, tasks, etc.) │ + │ └ │ │ │ + └──────────────┬───────────────┘ └──────────────────────────────┘ + │ + ┌──────────────▼───────────────┐ + │ Metadata DB │ + │ │ + │ serialized_dag: { │ Stored as-is from the language runtime's + │ "relative_fileloc": │ SDK Serialized DAG JSON + │ "path/to/example.jar" │ + │ } │ + │ task_instance.queue │ + └──────────────┬───────────────┘ + │ + ┌──────────────▼───────────────┐ + │ Scheduler │ + │ │ + │ Reads queue from TI │ + │ ──► ExecuteTask workload │ + │ (includes queue) │ + └──────────────┬───────────────┘ + │ + ┌──────────────▼───────────────┐ ┌──────────────────────────────┐ + │ Execution API │ │ Runtime Subprocess (Java) │ + │ │ │ │ + │ TI.queue ──► Startup │ │ task_execution_cmd() │ + │ Details │ │ Executes task in JVM │ + └──────────────┬───────────────┘ │ │ + │ └──────────────▲───────────────┘ + ┌──────────────▼───────────────┐ │ + │ Task Runner │ │ + │ │ │ + │ QueueToCoordinatorMapper │ │ + │ maps queue via `[sdk] │ │ + │ queue_to_sdk` config ───────┼───────────────────────────────────┘ + │ to matching coordinator │ + └──────────────────────────────┘ +``` + +### The `BaseCoordinator` Interface + +This is the central abstraction that language providers implement. It lives in the Task SDK (`task-sdk/src/airflow/sdk/execution_time/coordinator.py`) and handles both DAG parsing and task execution for a specific language runtime. + +```python +class BaseCoordinator: + """ + Base coordinator for runtime-specific DAG file processing and task execution. + + Providers register subclasses in their ``provider.yaml`` under + ``coordinators``. Both ProvidersManager (airflow-core) and + ProvidersManagerTaskRuntime (task-sdk) discover coordinators through + this extension point. + + Subclasses represent a specific language runtime (Java, Go, etc.) and + implement three methods. The base class owns the full bridge lifecycle: + TCP servers, subprocess management, selector-based I/O loop, and cleanup. + """ + + sdk: str # e.g. "java", "go" — matches sdk field on operator/TI + + # Discovery (called by DAG File Processor) + + @classmethod + def can_handle_dag_file(cls, bundle_name: str, path: str | os.PathLike) -> bool: + """Return True if this coordinator should parse the file at *path*.""" + ... + + @classmethod + def get_code_from_file(cls, fileloc: str) -> str: + """Return the actual DAG code (the content of JavaExample.java in this case""" + ... + + # DAG Parsing (called in forked DagFileProcessor child process) + + @classmethod + def dag_parsing_cmd( + cls, + *, + dag_file_path: str, # Absolute path to DAG file + bundle_name: str, # Name of the DAG bundle + bundle_path: str, # Root path of the bundle + comm_addr: str, # host:port for msgpack comm channel + logs_addr: str, # host:port for structured JSON log channel + ) -> list[str]: + """Return the subprocess command for DAG file parsing.""" + ... + + # Task Execution (called in forked worker child process) + + @classmethod + def task_execution_cmd( + cls, + *, + what: TaskInstance, + dag_rel_path: str | os.PathLike, # Relative path to DAG file within bundle + bundle_info: BundleInfo, + comm_addr: str, + logs_addr: str, + ) -> list[str]: + """Return the subprocess command for task execution.""" + ... + + # Lifecycle (owned by base class, not overridden) + + @classmethod + def run_dag_parsing(cls, *, path, bundle_name, bundle_path) -> None: ... + + @classmethod + def run_task_execution(cls, *, what, dag_rel_path, bundle_info, startup_details) -> None: ... +``` + +### Provider Registration + +Language providers register their coordinators in `provider.yaml`: + +```yaml +# providers/sdk/java/provider.yaml +process-coordinators: + - airflow.providers.sdk.java.coordinator.JavaCoordinator +``` + +### Example: `JavaCoordinator` + +```python +class JavaCoordinator(BaseCoordinator): + sdk = "java" + + @classmethod + def can_handle_dag_file(cls, bundle_name, path): + """True when path is a JAR with a Main-Class manifest entry.""" + ... + + @classmethod + def dag_parsing_cmd(cls, *, dag_file_path, bundle_name, bundle_path, comm_addr, logs_addr): + main_class = find_main_class(Path(dag_file_path)) + return [ + "java", + "-classpath", + f"{bundle_path}/*", + main_class, + f"--comm={comm_addr}", + f"--logs={logs_addr}", + ] + + @classmethod + def task_execution_cmd(cls, *, what, dag_rel_path, bundle_info, comm_addr, logs_addr): + jar_path = Path(dag_rel_path) + main_class = find_main_class(jar_path) + return [ + "java", + "-classpath", + f"{jar_path.parent}/*", + main_class, + f"--comm={comm_addr}", + f"--logs={logs_addr}", + ] +``` + +### Integration Points — Required Changes + +**1. Decorator — DAG Author Interface** + +DAG authors declare a non-Python task using `@task.stub` and specify a queue: + +```python +@task.stub(queue="java") +def my_java_task(): ... +``` + +**2. Serialization — Each Language SDK Produces SDK-Compatible Serialized DAG JSON** + +Serialization is the language runtime's responsibility, not Airflow Core's. Each language SDK implements its own serializer that understands the language-specific DAG and task structure and produces a Task SDK-compatible Serialized DAG JSON — the same schema that the Python SDK's `SerializedDAG` produces. + +The language runtime subprocess returns this JSON to the DAG File Processor through the msgpack comm channel. The DAG File Processor and Airflow Core treat it identically to Python-serialized DAGs — it is stored as-is in the metadata DB. + +We have already added compatibility validation between the Python SDK and Java SDK serialized DAG JSON formats to ensure both produce structurally equivalent output. + +**3. Execution API — Task Queues Routed to the Worker** + +A new configuration is added to map each task's `queue` to a language runtime: + +```ini +[sdk] +queue_to_sdk = {"java": "java"} +``` + +This specifies tasks in the `java` queue should be routed to `JavaCoordinator` since it has `sdk = "java"`. + +## Consequences + +### New Interfaces + +| Component | New Interface | Change Type | +|-----------|--------------|-------------| +| `BaseCoordinator` | Abstract base defined in Task SDK | New class | +| `coordinators` | Provider extension point in `provider.yaml` | New extension point | +| `@task.stub` decorator | `queue: str \| None` parameter | Additive | +| `[sdk] queue_to_sdk` | Airflow configuration | New option | +| `_resolve_runtime_entrypoint` | Route by `queue` → `sdk` match | Behavioral | + +### What Becomes Easier + +- Adding a new language runtime requires only a `BaseCoordinator` subclass, a language SDK, and a `provider.yaml` entry — no changes to Airflow Core. +- DAG authors can mix Python and non-Python tasks in the same pipeline. +- The existing task-runner two-layer design is preserved, keeping all Airflow extensions in Python. + +### What Becomes Harder + +- Each language SDK must independently produce compatible serialized DAG JSON, which requires cross-language validation infrastructure. +- The coordinator subprocess bridge adds a TCP hop and process management overhead per non-Python task. +- Debugging non-Python tasks requires understanding the bridge layer between the task runner and the language runtime. diff --git a/java-sdk/adr/0002-dag-parsing.md b/java-sdk/adr/0002-dag-parsing.md new file mode 100644 index 0000000000000..235658d5460bd --- /dev/null +++ b/java-sdk/adr/0002-dag-parsing.md @@ -0,0 +1,370 @@ + + +# ADR-0002: DAG Parsing — Language-Specific DAG File Processing + +## Status + +Accepted + +## Context + +Airflow's standard DAG file processor only understands Python files. To support DAGs defined in other languages (Java, Go, Rust, etc.), the pipeline needs an extension point where a language-specific processor can intercept the parsing request, delegate to an external runtime, and return a result in the same format the Airflow scheduler expects. + +This ADR details the DAG parsing side of the coordinator architecture described in [ADR-0001](0001-java-sdk-airflow-integration.md). It starts with the generic model — the abstract contracts and expected behavior that any language must implement — then walks through Java as a concrete example. + +## Decision + +### Extension Point: `BaseCoordinator` + +A single abstract base class — `BaseCoordinator` — handles both DAG parsing and task execution. It is registered in `provider.yaml` under `coordinators`. For DAG parsing, a subclass must implement two methods: + +| Method | Signature | Responsibility | +|---|---|---| +| `can_handle_dag_file` | `(bundle_name, path) -> bool` | Return `True` if this coordinator should handle the given file. Default returns `False`; subclasses add language-specific checks (e.g., "is this a JAR with a Main-Class?"). | +| `dag_parsing_cmd` | `(dag_file_path, bundle_name, bundle_path, comm_addr, logs_addr) -> list[str]` | Return the full command to launch the language runtime. `comm_addr` and `logs_addr` are `host:port` strings the process must connect to. | + +### Registration + +In the provider's `provider.yaml`: + +```yaml +process-coordinators: + - airflow.providers.sdk..coordinator. +``` + +A single registration covers both DAG parsing and task execution — there are no separate `dag-file-processors` or `task-coordinators` keys. + +### Discovery: `_resolve_processor_target()` + +When `DagFileProcessorProcess.start()` needs to parse a file: + +``` +_resolve_processor_target(path, bundle_name, bundle_path) + for each class_path in ProvidersManager().coordinators: + coordinator_cls = import_string(class_path) + if coordinator_cls.can_handle_dag_file(bundle_name, path): + return functools.partial(coordinator_cls.run_dag_parsing, path=..., bundle_name=..., bundle_path=...) + return None # fall back to default Python parser +``` + +The first coordinator whose `can_handle_dag_file()` returns `True` wins. If none match, the default Python `_parse_file_entrypoint` runs. + +### What the Base Class Handles Automatically + +The matched coordinator's `run_dag_parsing()` (a concrete method on `BaseCoordinator`) delegates to `_runtime_subprocess_entrypoint()`, which handles all the TCP/process plumbing: + +1. Creates two TCP servers on `127.0.0.1` with random ports (comm + logs) +2. Creates a stderr socketpair +3. Calls `dag_parsing_cmd()` to get the command +4. Spawns the subprocess with `stdin=DEVNULL` (does NOT inherit fd 0) +5. Accepts TCP connections from the subprocess +6. Wraps fd 0 as `supervisor_comm` via `os.dup(0)` +7. Runs `_bridge()` — a raw byte forwarder between fd 0 and the TCP comm socket + +### Expected E2E Flow + +``` +Airflow Dag-Processor + │ + ▼ +DagFileProcessorProcess.start(path, bundle_name, bundle_path) + │ + ├─ _resolve_processor_target() + │ └─ iterates process-coordinators from provider.yaml + │ └─ first can_handle_dag_file() == True wins + │ + ▼ +WatchedSubprocess.start(target=coordinator.run_dag_parsing) + │ + [fork — child process gets fd 0 as Unix domain socket to supervisor] + │ + ▼ (in child) +Coordinator.run_dag_parsing(path, bundle_name, bundle_path) + │ + ▼ +BaseCoordinator._runtime_subprocess_entrypoint(DagParsingInfo) + │ + ├─ 1. Create TCP comm_server + logs_server on 127.0.0.1:random + ├─ 2. Create stderr socketpair + ├─ 3. Call dag_parsing_cmd() → get launch command + ├─ 4. Popen(cmd, stdin=DEVNULL, stderr=child_stderr) + ├─ 5. Accept TCP connections from the language runtime + ├─ 6. supervisor_comm = socket(fileno=os.dup(0)) + └─ 7. _bridge() — raw byte forwarding until process exits +``` + +### Expected Message Sequence + +Once the bridge is running, the Airflow supervisor and the language runtime communicate directly through the bridge (raw bytes, no re-encoding): + +``` +Airflow Supervisor Bridge Language Runtime + │ │ │ + ├── DagFileParseRequest ──────────┼──────────────────────►│ + │ [4-byte len][msgpack frame] │ raw byte forward │ + │ │ │ + │ │ ├── parse DAGs from + │ │ │ bundle/file + │ │ │ + │◄── DagFileParsingResult ────────┼───────────────────────┤ + │ [4-byte len][msgpack frame] │ raw byte forward │ + │ │ │ + │ │ └── exit(0) + │ │ + │ └── drain remaining bytes (5s deadline) + │ close all sockets +``` + +### DagFileParsingResult Format + +The language runtime must produce a `DagFileParsingResult` that matches Python Airflow's DagSerialization format exactly. The Airflow scheduler deserializes this into its internal model — any divergence causes parsing failures. + +**Envelope:** + +``` +{ + "type": "DagFileParsingResult", + "fileloc": "", + "serialized_dags": [ + { + "data": { + "__version": 3, + "dag": { } + } + }, + ... + ] +} +``` + +**Serialized DAG structure** (version 3): + +| Field | Type | Required | Description | +|---|---|---|---| +| `dag_id` | string | yes | Unique identifier | +| `fileloc` | string | yes | Source file path (can be empty) | +| `relative_fileloc` | string | yes | Relative source path (can be empty) | +| `timezone` | string | yes | Always `"UTC"` | +| `timetable` | `{__type, __var}` | yes | Schedule timetable (see below) | +| `tasks` | list | yes | Serialized task list | +| `dag_dependencies` | list | yes | Empty list for non-Python DAGs | +| `task_group` | map | yes | Flat root task group | +| `edge_info` | map | yes | Empty map | +| `params` | list | yes | DAG-level parameters | +| `description` | string | if set | | +| `start_date` | float (epoch) | if set | Unwrapped from `__type`/`__var` | +| `end_date` | float (epoch) | if set | Unwrapped from `__type`/`__var` | +| `tags` | list | if non-empty | Unwrapped from `__type`/`__var` | +| `catchup` | bool | if `true` | | +| `max_active_tasks` | int | if non-default | | +| `max_active_runs` | int | if non-default | | + +**Timetable encoding:** + +| Schedule | `__type` | `__var` | +|---|---|---| +| `null` | `airflow.timetables.simple.NullTimetable` | `{}` | +| `@once` | `airflow.timetables.simple.OnceTimetable` | `{}` | +| `@continuous` | `airflow.timetables.simple.ContinuousTimetable` | `{}` | +| cron expr | `airflow.timetables.trigger.CronTriggerTimetable` | `{expression, timezone, interval, run_immediately}` | + +**Task encoding:** + +``` +{ + "__type": "operator", + "__var": { + "task_id": "", + "task_type": "", + "_task_module": "", + "downstream_task_ids": [""] // only if non-empty + } +} +``` + +**Value type encoding** (for complex fields): + +| Type | Encoding | +|---|---| +| datetime | `{"__type": "datetime", "__var": }` | +| timedelta | `{"__type": "timedelta", "__var": }` | +| dict | `{"__type": "dict", "__var": {k: serialize(v), ...}}` | +| set | `{"__type": "set", "__var": [sorted_items]}` | +| list | `[serialize(item), ...]` (no wrapper) | +| primitives | pass through unchanged | + +**Non-decorated vs decorated fields:** Some fields (like `start_date`, `end_date`, `tags`) are "non-decorated" — they are serialized with `__type`/`__var` wrapping but then unwrapped to just the `__var` value before inclusion in the DAG dict. Other fields (like `default_args`, `access_control`) are "decorated" — they keep the `__type`/`__var` wrapper. This matches Python's `serialize_to_json` behavior. + +### What a Language Provider Must Implement + +For DAG parsing, a new language provider needs: + +1. **A `BaseCoordinator` subclass** with: + - `can_handle_dag_file()` — language-specific file detection (e.g., "is this a JAR?", "is this a .go file?") + - `dag_parsing_cmd()` — returns the command to launch the runtime + +2. **A runtime process** that: + - Accepts `--comm=host:port` and `--logs=host:port` CLI arguments + - Connects to both TCP addresses + - Reads a `DagFileParseRequest` msgpack frame from the comm channel + - Parses the DAGs from the bundle + - Serializes the result to DagSerialization v3 format + - Sends back a `DagFileParsingResult` msgpack frame + - Exits + +3. **Registration** in `provider.yaml` under `process-coordinators` + +### Java as a Concrete Example + +**JavaCoordinator:** + +The Java provider implements all DAG-parsing contracts in a single `BaseCoordinator` subclass: + +```python +# providers/sdk/java/coordinator.py +class JavaCoordinator(BaseCoordinator): + sdk = "java" + + @classmethod + def can_handle_dag_file(cls, bundle_name, path) -> bool: + # Returns 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_cmd(cls, *, dag_file_path, bundle_name, bundle_path, comm_addr, logs_addr): + main_class = find_main_class(Path(dag_file_path)) + return [ + "java", + "-classpath", + f"{bundle_path}/*", + main_class, + f"--comm={comm_addr}", + f"--logs={logs_addr}", + ] +``` + +`can_handle_dag_file()` checks that the file is a JAR with a `Main-Class` in its manifest. This ensures the coordinator only claims files it can actually handle. + +The classpath is `/*` — a wildcard that includes all JARs in the directory (the application JAR plus its dependencies). + +No separate `JavaDagFileProcessor` class is needed — `BaseCoordinator` consolidates file detection, DAG parsing, and task execution into a single extension point. + +**Java SDK Bundle Process:** + +The Java bundle process (`Server.kt`) starts, connects to both TCP servers, and enters `CoordinatorComm.startProcessing()`. When it receives a `DagFileParseRequest`: + +``` +CoordinatorComm.handleIncoming(frame) + │ + ├── frame.body is DagFileParseRequest + │ file: String ← the path from the request + │ + ▼ +DagParser(request.file).parse(bundle) + │ + ├── Returns DagParsingResult(fileloc=file, dags=bundle.dags) + │ The DAGs were already loaded into the Bundle at startup + │ via DagBundle.getDags() + │ + ▼ +sendMessage(frame.id, result) + │ + ├── CoordinatorComm.encode(OutgoingFrame(id, result)) + │ ├── detects DagParsingResult type + │ └── calls result.serialize() ← Serde.kt + │ + ├── DagParsingResult.serialize() + │ ├── Wraps each DAG: {"data": {"__version": 3, "dag": dag.serialize(id)}} + │ ├── Dag.serialize() produces the full v3 format: + │ │ timetable, tasks, task_group, params, optional fields... + │ ├── Task.serialize() wraps as {"__type": "operator", "__var": {...}} + │ └── serializeValue() handles datetime/timedelta/dict/set encoding + │ + ├── TaskSdkFrames.encodeRequest(id, serializedMap) + │ ├── Converts map to msgpack: [id, body] + │ └── Returns byte array + │ + └── Writes [4-byte length prefix][msgpack payload] to comm channel + +shutDownRequested = true ← one-shot, process will exit +``` + +**Java SDK DagBundle Interface:** + +Bundle authors implement `DagBundle` to define their DAGs: + +```java +public class JavaExample implements DagBundle { + @Override + public List getDags() { + var dag = new Dag("java_example", null, "@daily"); + dag.addTask("extract", Extract.class, List.of()); + dag.addTask("transform", Transform.class, List.of("extract")); + dag.addTask("load", Load.class, List.of("transform")); + return List.of(dag); + } + + public static void main(String[] args) { + var example = new JavaExample(); + var bundle = new Bundle( + JavaExample.class.getPackage().getImplementationVersion(), + example.getDags() + ); + Server.create(args).serve(bundle); + } +} +``` + +The `Dag` class provides a fluent API: + +- `dagId`, `description`, `schedule` (cron or preset), `startDate`, `endDate`, and all standard Airflow DAG parameters +- `addTask(id, taskClass, dependsOn)` — registers a task and its upstream dependencies +- Dependencies are stored as a `dependants` map (parent → set of children), serialized as `downstream_task_ids` + +**Java SDK Serialization Compatibility:** + +The serialization in `Serde.kt` is validated against Python's output: + +```bash +# 1. Java generates serialized output +./gradlew sdk:test +# → writes validation/serialization/serialized_java.json + +# 2. Python generates the same DAGs +uv run validation/serialization/serialize_python.py \ + validation/serialization/test_dags.yaml \ + validation/serialization/serialized_python.json + +# 3. Field-by-field comparison +uv run validation/serialization/compare.py \ + validation/serialization/serialized_python.json \ + validation/serialization/serialized_java.json +``` + +Both share test cases defined in `test_dags.yaml`, ensuring the Java SDK produces byte-identical output to Python's `DagSerialization.serialize_dag()` for the same inputs. + +## Consequences + +- The DAG file processor can be extended to any language without modifying Airflow Core — only a provider with a `BaseCoordinator` subclass is needed. +- The language runtime must produce exact DagSerialization v3 JSON, requiring cross-language validation infrastructure (e.g., `test_dags.yaml` + `compare.py`). +- The base class absorbs all TCP/process plumbing, so language providers only implement two methods for DAG parsing. +- The subprocess bridge adds latency and a process boundary; DAG parsing for non-Python files is inherently slower than in-process Python parsing. diff --git a/java-sdk/adr/0003-workload-execution.md b/java-sdk/adr/0003-workload-execution.md new file mode 100644 index 0000000000000..afa90a16e9e71 --- /dev/null +++ b/java-sdk/adr/0003-workload-execution.md @@ -0,0 +1,473 @@ + + +# ADR-0003: Workload Execution — Language-Specific Task Execution + +## Status + +Accepted + +## Context + +Airflow's standard task runner executes Python callables. To support tasks written in other languages, the pipeline needs an extension point where a language-specific coordinator can intercept the execution, delegate to an external runtime process, and bridge the Task SDK protocol so the external process can access Airflow services (connections, variables, XCom) during execution. + +This ADR details the task execution side of the coordinator architecture described in [ADR-0001](0001-java-sdk-airflow-integration.md). It starts with the generic model — the abstract contracts and expected behavior that any language must implement — then walks through Java as a concrete example. + +## Decision + +### Extension Point: `BaseCoordinator` + +The same `BaseCoordinator` base class that handles DAG parsing also handles task execution. It is registered in `provider.yaml` under `coordinators`. For task execution, a subclass must implement: + +| Method | Signature | Responsibility | +|---|---|---| +| `task_execution_cmd` | `(what, dag_rel_path, bundle_info, comm_addr, logs_addr) -> list[str]` | Return the full command to launch the language runtime for task execution. `comm_addr` and `logs_addr` are `host:port` strings the process must connect to. | + +The base class provides `run_task_execution()` as a concrete method that handles all TCP/process plumbing automatically (same pattern as `run_dag_parsing()` for the DAG parsing side). + +**Parameters passed to `run_task_execution()`:** + +| Parameter | Type | Description | +|---|---|---| +| `what` | `TaskInstance` | The task instance to execute (id, dag_id, task_id, run_id, try_number, etc.) | +| `dag_rel_path` | `str \| PathLike` | Relative path to the DAG file / bundle within the bundle root | +| `bundle_info` | `BundleInfo` | Bundle name and version | +| `startup_details` | `StartupDetails` | Full startup context (task instance, DAG rel path, bundle info, run context, start date) — already consumed from fd 0 | + +### Registration + +The same `coordinators` entry in `provider.yaml` covers both DAG parsing and task execution — no separate registration needed: + +```yaml +coordinators: + - airflow.providers.sdk..coordinator. +``` + +### Discovery: `_resolve_runtime_entrypoint()` + +When `task_runner.main()` starts, before any Python task execution: + +``` +task_runner.main() + → startup_details = get_startup_details() # reads from fd 0 + → _resolve_runtime_entrypoint(startup_details) + for each class_path in ProvidersManagerTaskRuntime().process_coordinators: + coordinator_cls = import_string(class_path) + if not hasattr(coordinator_cls, "run_task_execution"): + continue + return functools.partial(coordinator_cls.run_task_execution, + what=..., dag_rel_path=..., bundle_info=..., startup_details=...) + return None # fall back to default Python execution + + → if runtime_entrypoint is not None: + runtime_entrypoint() # language-specific execution + return # short-circuit — skip Python execution entirely +``` + +> **Note:** Currently the first coordinator with `run_task_execution` wins. `QueueToCoordinatorMapper` maps the task's `queue` to the correct coordinator via the `[sdk] queue_to_sdk` configuration. + +### Expected E2E Flow + +``` +Airflow Executor (dispatches task) + │ + ▼ +WatchedSubprocess.start(target=task_runner.main) + │ + [fork — child process gets fd 0 as Unix domain socket to supervisor] + │ + ▼ (in child) +task_runner.main() + │ + ├─ get_startup_details() ← reads StartupDetails from fd 0 + │ + ├─ _resolve_runtime_entrypoint() + │ └─ iterates coordinators from provider.yaml + │ └─ first with run_task_execution wins + │ + ▼ +Coordinator.run_task_execution(what, dag_rel_path, bundle_info, startup_details) + │ + ▼ +BaseCoordinator._runtime_subprocess_entrypoint(TaskExecutionInfo) + │ + ├─ 1. Create TCP comm_server + logs_server on 127.0.0.1:random + ├─ 2. Create stderr socketpair + ├─ 3. Call task_execution_cmd() → get launch command + ├─ 4. Popen(cmd, stdin=DEVNULL, stderr=child_stderr) + ├─ 5. Accept TCP connections from the language runtime + ├─ 6. _send_startup_details(runtime_comm, startup_details) + │ └─ re-serializes with model_dump(mode="json") to avoid + │ msgpack extension types non-Python decoders can't handle + ├─ 7. supervisor_comm = socket(fileno=os.dup(0)) + └─ 8. _bridge() — raw byte forwarding until process exits +``` + +Key difference from DAG parsing: In task execution, `task_runner.main()` has already consumed `StartupDetails` from fd 0. The bridge must re-send `StartupDetails` to the language runtime over TCP before starting the byte-forwarding bridge. This is done via `_send_startup_details()`, which re-serializes using JSON mode to avoid msgpack extension types (like `Timestamp`) that non-Python decoders may not support. + +### Expected Message Sequence + +Task execution is a multi-round conversation, unlike DAG parsing's single request/response: + +``` +Airflow Supervisor Bridge Language Runtime + │ │ │ + │ [StartupDetails sent by bridge directly] │ + │ ├── StartupDetails ────►│ + │ │ │ + │ │ ├── Look up task + │ │ │ from bundle + │ │ │ + │ │ ┌───────────────────┤ + │ │ │ Task code runs │ + │ │ │ and may request: │ + │ │ │ │ + │◄── GetConnection(conn_id) ──────┼───┤ │ + │ │ │ │ + ├── ConnectionResult ─────────────┼──►│ │ + │ │ │ │ + │◄── GetVariable(key) ────────────┼───┤ │ + │ │ │ │ + ├── VariableResult ───────────────┼──►│ │ + │ │ │ │ + │◄── GetXCom(key, dag_id, ...) ───┼───┤ │ + │ │ │ │ + ├── XComResult ───────────────────┼──►│ │ + │ │ │ │ + │◄── SetXCom(key, value, ...) ────┼───┤ │ + │ │ │ │ + ├── (empty response) ─────────────┼──►│ │ + │ │ │ │ + │ │ └───────────────────┤ + │ │ │ + │◄── SucceedTask / TaskState ─────┼───────────────────────┤ + │ (terminal — no response) │ │ + │ │ └── exit(0) + │ │ + │ └── drain, close sockets +``` + +### Task SDK Protocol Messages + +The language runtime exchanges these message types with the Airflow supervisor: + +**Runtime → Supervisor (requests):** + +| Message | Fields | Purpose | +|---|---|---| +| `GetConnection` | `conn_id` | Fetch an Airflow connection by ID | +| `GetVariable` | `key` | Fetch an Airflow variable by key | +| `GetXCom` | `key`, `dag_id`, `task_id`, `run_id`, `map_index?`, `include_prior_dates?` | Fetch an XCom value | +| `SetXCom` | `key`, `value`, `dag_id`, `task_id`, `run_id`, `map_index`, `mapped_length?` | Store an XCom value | +| `SucceedTask` | `end_date`, `task_outlets?`, `outlet_events?` | Terminal: task succeeded | +| `TaskState` | `state` (`"failed"`, `"removed"`, `"skipped"`), `end_date` | Terminal: task ended non-successfully | + +**Supervisor → Runtime (responses):** + +| Message | Fields | In response to | +|---|---|---| +| `ConnectionResult` | `conn_id`, `conn_type`, `host`, `schema`, `login`, `password`, `port`, `extra` | `GetConnection` | +| `VariableResult` | `key`, `value` | `GetVariable` | +| `XComResult` | `key`, `value` | `GetXCom` | +| (empty) | | `SetXCom` | +| `ErrorResponse` | `error`, `detail` | Any request that failed server-side | + +**Framing:** Every message is a length-prefixed msgpack frame. Requests are `[id, body]` (2-element array); responses are `[id, body, error]` (3-element array). The `id` field correlates request/response pairs. + +### Request/Response Semantics + +The task execution follows a synchronous request/response pattern from the runtime's perspective: + +1. The runtime sends a request frame (e.g., `GetVariable`) with an incrementing `id` +2. The supervisor reads the frame, fulfills the request (e.g., calls the Execution API), and sends back a response with the same `id` +3. The runtime blocks until it receives the response +4. This repeats for each Airflow service call the task code makes +5. When the task finishes, the runtime sends a terminal message (`SucceedTask` or `TaskState`) — no response is expected, and the process exits + +### StartupDetails + +The first message the runtime receives is `StartupDetails`, which provides full context for the task: + +| Field | Type | Description | +|---|---|---| +| `ti` | `TaskInstance` | id, task_id, dag_id, run_id, try_number, dag_version_id, map_index, context_carrier | +| `dag_rel_path` | string | Relative path to the DAG file / bundle | +| `bundle_info` | `BundleInfo` | name, version | +| `start_date` | datetime | When this task attempt started | +| `ti_context` | `TIRunContext` | DAG run context (logical date, data interval, etc.) | +| `sentry_integration` | string | Sentry DSN for error reporting (optional) | + +### What a Language Provider Must Implement + +For task execution, a new language provider needs: + +1. **A `BaseCoordinator` subclass** with: + - `task_execution_cmd()` — returns the command to launch the runtime + - (This is the same subclass that implements `can_handle_dag_file()` and `dag_parsing_cmd()` for DAG parsing — one class covers both) + +2. **A runtime process** that: + - Accepts `--comm=host:port` and `--logs=host:port` CLI arguments + - Connects to both TCP addresses + - Reads a `StartupDetails` msgpack frame from the comm channel + - Looks up the task to execute from its bundle using `ti.dag_id` and `ti.task_id` + - Executes the task, making `GetConnection`/`GetVariable`/`GetXCom`/`SetXCom` requests as needed + - Sends `SucceedTask` on success or `TaskState("failed")` on failure + - Exits + +3. **A task interface** that user code implements (analogous to Python's `@task` decorator or `BaseOperator`) + +4. **A client API** that wraps the socket protocol behind a simple interface (get_connection, get_variable, get_xcom, set_xcom) so task authors don't deal with framing + +5. **Registration** in `provider.yaml` under `coordinators` (same entry as DAG parsing — no separate registration) + +### Java as a Concrete Example + +**JavaCoordinator (Python side):** + +The same `JavaCoordinator` that handles DAG parsing also handles task execution — no separate `JavaTaskCoordinator` class is needed: + +```python +# providers/sdk/java/coordinator.py +class JavaCoordinator(BaseCoordinator): + sdk = "java" + + @classmethod + def can_handle_dag_file(cls, bundle_name, path) -> bool: + with contextlib.suppress(FileNotFoundError): + return find_main_class(Path(path)) is not None + return False + + @classmethod + def dag_parsing_cmd(cls, *, dag_file_path, bundle_name, bundle_path, comm_addr, logs_addr): + main_class = find_main_class(Path(dag_file_path)) + return [ + "java", + "-classpath", + f"{bundle_path}/*", + main_class, + f"--comm={comm_addr}", + f"--logs={logs_addr}", + ] + + @classmethod + def task_execution_cmd(cls, *, what, dag_rel_path, bundle_info, comm_addr, logs_addr): + jar_path = Path(dag_rel_path) + main_class = find_main_class(jar_path) + return [ + "java", + "-classpath", + f"{jar_path.parent}/*", + main_class, + f"--comm={comm_addr}", + f"--logs={logs_addr}", + ] +``` + +One class, one `provider.yaml` entry, covers both DAG parsing and task execution. + +**Java SDK Task Interface:** + +User task code implements a single-method interface: + +```java +// sdk: org.apache.airflow.sdk.Task +public interface Task { + void execute(Client client) throws Exception; +} +``` + +The `Client` provides access to Airflow services: + +```java +// sdk: org.apache.airflow.sdk.Client +public class Client { + // Access task metadata + public StartupDetails getDetails(); + + // Airflow services + public Connection getConnection(String id); + public Object getVariable(String key); + public Object getXCom(String key, String dagId, String taskId, String runId, ...); + public void setXCom(String key, Object value); // defaults: key="return_value", dagId/taskId/runId from current task +} +``` + +**Java SDK Task Execution Flow:** + +When the bundle process receives `StartupDetails`: + +``` +CoordinatorComm.handleIncoming(frame) + │ + ├── frame.body is StartupDetails + │ ti: TaskInstance (id, dagId, taskId, runId, tryNumber, ...) + │ dagRelPath, bundleInfo, startDate, tiContext + │ + ▼ +TaskRunner.run(bundle, request, comm) + │ + ├── Create Client(request, CoordinatorClient(comm)) + │ CoordinatorClient wraps the comm channel behind the Client interface + │ + ├── Look up task class: + │ bundle.dags[request.ti.dagId]?.tasks[request.ti.taskId] + │ └── if not found → return TaskState("removed") + │ + ├── Instantiate task: + │ task.getDeclaredConstructor().newInstance() + │ + ├── Execute: + │ try { + │ instance.execute(client) ← USER TASK CODE RUNS HERE + │ return SucceedTask() + │ } catch (Exception e) { + │ return TaskState("failed") + │ } + │ + ▼ +sendMessage(frame.id, result) ← sends SucceedTask or TaskState back +shutDownRequested = true ← one-shot, process will exit +``` + +**Java SDK Airflow Service Access:** + +When user task code calls `client.getVariable("my_key")`, the call chain is: + +``` +client.getVariable("my_key") // Client.kt (public SDK) + │ + └── impl.getVariable("my_key") // CoordinatorClient (execution) + │ + └── runBlocking { // blocks the calling thread + comm.communicate( // CoordinatorComm + GetVariable(key = "my_key") + ) + } + │ + ├── sendMessage(nextId++, GetVariable) // encode + write to comm socket + │ ├── encode: [id, {"type": "GetVariable", "key": "my_key"}] + │ └── write: [4-byte len][msgpack] + │ + ├── processOnce(::handle) // block until response arrives + │ ├── read 4-byte length prefix + │ ├── read payload + │ └── decode: [id, {"type": "VariableResult", ...}, null] + │ + └── return response.value // unwrap VariableResponse +``` + +This is fully synchronous from the task code's perspective — `getVariable()` blocks until the supervisor responds. + +**Java SDK Example Task Implementation:** + +```java +public static class Extract implements Task { + public void execute(Client client) throws Exception { + // Read XCom from a Python task in the same DAG + var pythonXcom = client.getXCom("python_task_1"); + + // Access Airflow connections + var connection = client.getConnection("test_http"); + + // Do work... + Thread.sleep(6000); + + // Push XCom for downstream tasks (Java or Python) + client.setXCom(new Date().getTime()); + } +} + +public static class Transform implements Task { + public void execute(Client client) { + // Read XCom from upstream Java task + var extractXcom = client.getXCom("extract"); + + // Access Airflow variables + var variable = client.getVariable("my_variable"); + + // Push XCom (readable by downstream Python tasks) + client.setXCom(new Date().getTime()); + } +} + +public static class Load implements Task { + public void execute(Client client) { + var xcom = client.getXCom("transform"); + throw new RuntimeException("I failed"); + // Exception → TaskRunner catches → sends TaskState("failed") + } +} +``` + +**Java SDK Complete Bundle Entry Point:** + +```java +public class JavaExample implements DagBundle { + @Override + public List getDags() { + var dag = new Dag("java_example", null, "@daily"); + dag.addTask("extract", Extract.class, List.of()); + dag.addTask("transform", Transform.class, List.of("extract")); + dag.addTask("load", Load.class, List.of("transform")); + return List.of(dag); + } + + public static void main(String[] args) { + var bundle = new Bundle( + JavaExample.class.getPackage().getImplementationVersion(), + new JavaExample().getDags() + ); + Server.create(args).serve(bundle); // parses --comm/--logs, connects, enters message loop + } +} +``` + +The same `main()` entry point handles both DAG parsing and task execution — the first message received (`DagFileParseRequest` or `StartupDetails`) determines the mode. + +**Java SDK Java-side Supervisor (Alternative Execution Path):** + +The Java SDK also provides `Supervisor.kt` for execution contexts where there is no Python process (e.g., the Edge Worker). In this path, the Supervisor terminates the protocol directly instead of bridging: + +``` +Supervisor.run(request) + │ + ├── Create TCP comm + logs servers + ├── Spawn Java bundle process with --comm/--logs + ├── Accept connections + ├── HTTP PATCH task → running state + ├── Send StartupDetails to bundle via comm socket + │ + └── serveTaskSdkRequests() loop: + Read frame from bundle + ├── GetConnection → HTTP GET /connections/{id} → send response + ├── GetVariable → HTTP GET /variables/{key} → send response + ├── GetXCom → HTTP GET /xcom/... → send response + ├── SetXCom → HTTP POST /xcom/... → send response + └── SucceedTask/TaskState → HTTP PATCH terminal state → exit loop +``` + +The bundle process behaves identically in both paths — it is unaware of whether its comm channel leads to a Python bridge or a Java Supervisor. This is the core design invariant of the Java SDK. + +## Consequences + +- Task execution for any language reuses the same coordinator + bridge pattern as DAG parsing, keeping the extension surface small. +- The multi-round protocol (GetConnection, GetVariable, etc.) means the language runtime has full access to Airflow services without reimplementing them — they stay in Python. +- The synchronous request/response model is simple for language SDK authors but adds a round-trip per service call. +- The Java-side Supervisor (`Supervisor.kt`) provides an alternative execution path for environments without Python, but requires the Java SDK to implement HTTP calls to the Execution API directly. +- Task authors interact with a simple `Client` interface, completely abstracted from the underlying socket protocol. diff --git a/java-sdk/adr/0004-pure-java-dags.md b/java-sdk/adr/0004-pure-java-dags.md new file mode 100644 index 0000000000000..a3c42672c24cf --- /dev/null +++ b/java-sdk/adr/0004-pure-java-dags.md @@ -0,0 +1,228 @@ + + +# ADR-0004: Pure Java DAGs — Build-Time Packaging and Code Visibility + +## Status + +Accepted + +## Context + +[ADR-0001](0001-java-sdk-airflow-integration.md) introduces two ways to integrate non-Python tasks: `@task.stub` (mixed Python+Java DAGs) and pure Java DAGs (entire DAG in Java via `DagBundle`). [ADR-0002](0002-dag-parsing.md) and [ADR-0003](0003-workload-execution.md) describe the coordinator infrastructure for DAG parsing and task execution respectively. + +This ADR focuses on the Java-SDK-specific concerns that make pure Java DAGs work end-to-end — build-time metadata generation, source code packaging for UI visibility, and JAR manifest conventions — rather than the shared coordinator infrastructure already covered in those ADRs. + +The central challenge is that Airflow Core expects to read DAG metadata and source code from files on disk or from the metadata DB. A JAR is an opaque binary — Airflow cannot `open()` it and read Python source. The Java SDK must bridge this gap at build time by embedding machine-readable metadata and human-readable source into the JAR itself. + +## Decision + +### JAR Manifest Conventions + +The JAR manifest (`META-INF/MANIFEST.MF`) carries three SDK-specific attributes that Airflow and the Java SDK use to bootstrap a bundle: + +| Attribute | Example Value | Purpose | +|---|---|---| +| `Main-Class` | `org.apache.airflow.example.JavaExample` | Standard Java attribute; the coordinator uses it to launch the JVM | +| `Airflow-Java-SDK-Metadata` | `airflow-metadata.yaml` | Points to the embedded metadata file (dag IDs, task IDs) | +| `Airflow-Java-SDK-Dag-Code` | `JavaExample.java` | Points to the embedded source file for Airflow UI display | + +These attributes are set in the Gradle build (see [Build-Time Packaging](#build-time-packaging-gradle) below). The Python-side coordinator reads `Main-Class` to construct the launch command; `BundleScanner` reads `Airflow-Java-SDK-Metadata` to discover DAG IDs without launching the JVM. + +### Build-Time Metadata: `airflow-metadata.yaml` + +At build time, the SDK runs `BundleInspector` — a build-time utility that reflectively instantiates the user's `DagBundle` class, calls `getDags()`, and writes a YAML file listing every DAG ID and its task IDs: + +```yaml +dags: + java_example: + tasks: + - extract + - transform + - load +``` + +This file is embedded in the JAR root and referenced by the `Airflow-Java-SDK-Metadata` manifest attribute. + +**Why build-time, not runtime?** The metadata must be available before the JVM starts. `BundleScanner` reads it from the JAR to discover which DAG IDs a bundle contains — this is used for `@task.stub` routing (mapping a `dag_id` to the correct bundle's classpath) without paying JVM startup cost. For pure Java DAGs, the coordinator already knows the bundle path, but the metadata is still useful for validation and tooling. + +**`BundleInspector`:** + +```kotlin +object BundleInspector { + @JvmStatic + fun main(args: Array) { + val className = args[0] + val outputPath = args[1] + val clazz = Class.forName(className) + val instance = clazz.getDeclaredConstructor().newInstance() as? DagBundle + ?: error("$className does not implement DagBundle") + val dags = instance.getDags() + File(outputPath).apply { parentFile.mkdirs() }.writeText(toYaml(dags)) + } + + internal fun toYaml(dags: List): String = buildString { + appendLine("dags:") + for (dag in dags) { + appendLine(" ${dag.dagId}:") + appendLine(" tasks:") + for (taskId in dag.tasks.keys) { + appendLine(" - $taskId") + } + } + } +} +``` + +### Source Code Packaging for UI Visibility + +Airflow stores DAG source code in the `dag_code` table and displays it in the web UI. For Python DAGs this is trivial — `DagCode.write_code()` reads the `.py` file from disk. For a JAR, the raw bytecode is not human-readable. + +The solution: pack the original `.java` source file into the JAR at build time. The `Airflow-Java-SDK-Dag-Code` manifest attribute tells the coordinator which file to extract. + +On the Python side, `get_code_from_file()` on the coordinator: + +1. Opens the JAR as a ZIP +2. Reads the `Airflow-Java-SDK-Dag-Code` attribute from the manifest +3. Extracts and returns the raw `.java` source + +This lets Airflow's existing `DagCode` infrastructure store and display Java source code with no changes to Airflow Core. + +### Build-Time Packaging (Gradle) + +The `example/build.gradle.kts` shows the complete packaging pattern: + +```kotlin +val bundleMainClass = application.mainClass.get() +val metadataFileName = "airflow-metadata.yaml" +val metadataOutputDir = layout.buildDirectory.dir("airflow-metadata") +val dagCodeSourcePath = bundleMainClass.replace('.', '/') + ".java" +val dagCodeFileName = bundleMainClass.substringAfterLast('.') + ".java" + +// 1. Run BundleInspector at compile time to generate metadata +val inspectBundle = tasks.register("inspectBundle") { + dependsOn("classes") + classpath = sourceSets.main.get().runtimeClasspath + mainClass.set("org.apache.airflow.sdk.BundleInspector") + args = listOf(bundleMainClass, metadataOutputDir.get().file(metadataFileName).asFile.absolutePath) +} + +// 2. Pack metadata + source into the JAR +tasks.withType { + dependsOn(inspectBundle) + from(metadataOutputDir) // airflow-metadata.yaml + from("src/java/$dagCodeSourcePath") // raw .java source file + manifest { + attributes( + "Main-Class" to bundleMainClass, + "Airflow-Java-SDK-Version" to project.version, + "Airflow-Java-SDK-Metadata" to metadataFileName, + "Airflow-Java-SDK-Dag-Code" to dagCodeFileName, + ) + } +} +``` + +The resulting JAR contains: + +``` +example.jar +├── META-INF/MANIFEST.MF (Main-Class, SDK attributes) +├── airflow-metadata.yaml (dag IDs + task IDs) +├── JavaExample.java (raw source for UI display) +├── org/apache/airflow/example/ +│ ├── JavaExample.class (compiled bundle entry point) +│ ├── JavaExample$Extract.class +│ ├── JavaExample$Transform.class +│ └── JavaExample$Load.class +└── ... (SDK + dependency classes) +``` + +### `BundleScanner` — Runtime Bundle Discovery + +`BundleScanner` reads JAR manifests at runtime to discover bundles without launching the JVM. This is used by the `@task.stub` path to resolve which bundle contains a given `dag_id`. + +```kotlin +data class ResolvedBundle( + val mainClass: String, // From Main-Class manifest attribute + val classpath: String, // All JARs in bundle directory, colon-separated +) + +fun scanBundles(bundlesDir: Path): Map +``` + +It supports two directory layouts: + +- **Nested**: each subdirectory of `bundlesDir` is a bundle home (e.g., `bundles/my-app/lib/*.jar`) +- **Flat**: `bundlesDir` itself contains the JARs (e.g., `bundles/*.jar`) + +For each JAR, it reads the `Airflow-Java-SDK-Metadata` manifest attribute, extracts the referenced YAML, parses DAG IDs, and returns a mapping from `dag_id` to `ResolvedBundle`. + +### The DagBundle Authoring API + +Bundle authors implement `DagBundle` to define their DAGs: + +```java +public class JavaExample implements DagBundle { + + public static class Extract implements Task { + public void execute(Client client) throws Exception { + var connection = client.getConnection("test_http"); + client.setXCom(new Date().getTime()); + } + } + + public static class Transform implements Task { + public void execute(Client client) { + var extract_xcom = client.getXCom("extract"); + client.setXCom(new Date().getTime()); + } + } + + @Override + public List getDags() { + var dag = new Dag("java_example", null, "@daily"); + dag.addTask("extract", Extract.class, List.of()); + dag.addTask("transform", Transform.class, List.of("extract")); + return List.of(dag); + } + + public static void main(String[] args) { + var example = new JavaExample(); + var bundle = new Bundle( + JavaExample.class.getPackage().getImplementationVersion(), + example.getDags() + ); + Server.create(args).serve(bundle); + } +} +``` + +The `main()` method is the JVM entry point that the coordinator launches. It wires the `DagBundle` to the SDK's TCP communication layer (`Server` → `CoordinatorComm`), which handles DAG parsing requests and task execution commands as described in [ADR-0002](0002-dag-parsing.md) and [ADR-0003](0003-workload-execution.md). + +> **Note:** The current `DagBundle` interface is subject to review before the SDK reaches 1.0. Subclassing `Dag` directly may be a more natural fit and is being considered for post-OSS-integration. + +## Consequences + +- JAR bundles are self-contained: metadata, source, and compiled code are all in one artifact, simplifying deployment (copy one directory of JARs). +- Build-time metadata generation means DAG IDs can be discovered without JVM startup — important for `BundleScanner` and tooling. +- Source code packaging enables Airflow UI display with no changes to Airflow Core's `DagCode` infrastructure. +- The manifest convention (`Airflow-Java-SDK-*` attributes) is extensible — future attributes can carry additional metadata without breaking existing tooling. +- The build-time `BundleInspector` step adds a compile-time dependency on the SDK and requires the `DagBundle` class to be instantiable without side effects (no I/O, no connections in the constructor). +- Bundle authors must follow the Gradle packaging pattern (or replicate it in Maven/other build tools) — this is SDK-specific boilerplate that doesn't exist for Python DAGs. From 4d4bb81dd470b73f17ab4f83964c9632581bf375 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Thu, 7 May 2026 09:58:58 +0800 Subject: [PATCH 79/80] Update new coordinator config setup in java_sdk_build.sh --- scripts/in_container/java_sdk_build.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/in_container/java_sdk_build.sh b/scripts/in_container/java_sdk_build.sh index 42198cedd9e54..5a824e24d7dff 100755 --- a/scripts/in_container/java_sdk_build.sh +++ b/scripts/in_container/java_sdk_build.sh @@ -164,8 +164,10 @@ echo -e "${COLOR_GREEN} Pure Java bundle: ${PURE_JAR_COUNT} JARs${COLOR_RESET}" ENV_FILE="${BUNDLES_OUTPUT_DIR}/java_sdk_env.sh" cat > "${ENV_FILE}" < Date: Thu, 7 May 2026 11:54:41 +0800 Subject: [PATCH 80/80] Update JavaSDK e2e setup with the latest coordinator config --- .dockerignore | 1 + Dockerfile | 4 +- airflow-e2e-tests/docker/java-sdk.yml | 14 +- .../tests/airflow_e2e_tests/conftest.py | 19 ++- .../tests/airflow_e2e_tests/constants.py | 1 - .../install_from_docker_context_files.sh | 4 +- uv.lock | 148 +++++++++++------- 7 files changed, 120 insertions(+), 71 deletions(-) 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/Dockerfile b/Dockerfile index 60c33e922df1a..d3ace9cdcb6bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1048,8 +1048,8 @@ function install_airflow_and_providers_from_docker_context_files(){ install_airflow_core_distribution=("apache-airflow-core==${AIRFLOW_VERSION}") fi - # Find Provider/TaskSDK/CTL distributions in docker-context files - readarray -t airflow_distributions< <(python /scripts/docker/get_distribution_specs.py /docker-context-files/apache?airflow?{providers,task?sdk,airflowctl}*.{whl,tar.gz} 2>/dev/null || true) + # Find Provider/TaskSDK/CTL/Coordinator distributions in docker-context files + readarray -t airflow_distributions< <(python /scripts/docker/get_distribution_specs.py /docker-context-files/apache?airflow?{providers,task?sdk,airflowctl,coordinators?java}*.{whl,tar.gz} 2>/dev/null || true) echo echo "${COLOR_BLUE}Found provider distributions in docker-context-files folder: ${airflow_distributions[*]}${COLOR_RESET}" echo diff --git a/airflow-e2e-tests/docker/java-sdk.yml b/airflow-e2e-tests/docker/java-sdk.yml index e707245a8b6ec..27a489c02122d 100644 --- a/airflow-e2e-tests/docker/java-sdk.yml +++ b/airflow-e2e-tests/docker/java-sdk.yml @@ -22,12 +22,14 @@ # 1. Mount a pre-downloaded JDK, the Python stub bundle, and the pure Java bundle. # 2. Set JAVA_HOME and PATH so the Java coordinator can find the JDK. # -# The Java language provider is expected to be pre-installed in the Docker image. +# The apache-airflow-coordinators-java distribution is expected to be +# pre-installed in the Docker image (baked in by `breeze prod-image build` +# via the workspace `uv sync --all-packages`). # # Environment variables expected (set by conftest.py): -# JAVA_OPENJDK_PATH – host path to the pre-downloaded OpenJDK directory -# JAVA_STUB_BUNDLE_PATH – host path to the stub DAG bundle root (dags/ + jar-bundles/) -# JAVA_BUNDLES_PATH – host path to the pure Java DAG bundle (JARs only) +# JAVA_OPENJDK_PATH – host path to the pre-downloaded OpenJDK directory +# JAVA_STUB_BUNDLE_PATH – host path to the stub DAG bundle root (dags/ + jar-bundles/) +# JAVA_BUNDLES_PATH – host path to the pure Java DAG bundle (JARs only) --- x-java-env: &java-env AIRFLOW__CORE__LOAD_EXAMPLES: "false" @@ -56,8 +58,8 @@ services: # Since this is a new service (not an override of the base docker-compose), # it needs the full service definition including the common Airflow config # that the base compose provides via x-airflow-common. - # AIRFLOW__SDK__QUEUE_TO_SDK is picked up from the .env file - # via env_file (set by conftest.py). + # AIRFLOW__SDK__COORDINATORS and AIRFLOW__SDK__QUEUE_TO_COORDINATOR are picked + # up from the .env file via env_file (set by conftest.py). airflow-java-worker: image: ${AIRFLOW_IMAGE_NAME:-apache/airflow:|version|} env_file: diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py index b679c6d75fae6..431a88c430a01 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py @@ -220,6 +220,10 @@ def _build_java_sdk_bundles(tmp_dir: Path) -> Path: "JAVA_STUB_DAG_ID": JAVA_STUB_DAG_ID, "JAVA_PURE_DAG_ID": JAVA_PURE_DAG_ID, "JAVA_SDK_DAGS_DIR": str(JAVA_SDK_DAGS_FOLDER), + # The build script defaults GRADLE_USER_HOME to /files/.gradle for breeze + # in-container caching; on the host we use the standard ~/.gradle so the + # Gradle wrapper can write its lock files. + "GRADLE_USER_HOME": os.environ.get("GRADLE_USER_HOME", str(Path.home() / ".gradle")), } console.print("[yellow]Building Java SDK bundles via shared build script...") @@ -272,15 +276,24 @@ def _setup_java_sdk_integration(dot_env_file, tmp_dir): }, ] ) - queue_to_sdk = json.dumps({"java-queue": "java"}) + coordinators = json.dumps( + [ + { + "name": "java", + "classpath": "airflow.sdk.coordinators.java.JavaCoordinator", + "kwargs": {"bundles_folder": JAVA_CONTAINER_STUB_JAVA_BUNDLES_FOLDER_PATH}, + } + ] + ) + queue_to_coordinator = json.dumps({"java-queue": "java"}) dot_env_file.write_text( f"AIRFLOW_UID={os.getuid()}\n" "AIRFLOW__CORE__LOAD_EXAMPLES=false\n" "AIRFLOW__LOGGING__LOGGING_LEVEL=DEBUG\n" - f"AIRFLOW__SDK__QUEUE_TO_SDK={queue_to_sdk}\n" + f"AIRFLOW__SDK__COORDINATORS={coordinators}\n" + f"AIRFLOW__SDK__QUEUE_TO_COORDINATOR={queue_to_coordinator}\n" f"AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST={bundle_config}\n" - f"AIRFLOW__JAVA__BUNDLES_FOLDER={JAVA_CONTAINER_STUB_JAVA_BUNDLES_FOLDER_PATH}\n" ) os.environ["ENV_FILE_PATH"] = str(dot_env_file) diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py index 7d6e3508abbc2..9c7c05c537eea 100644 --- a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py +++ b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py @@ -54,7 +54,6 @@ JAVA_SDK_DAGS_FOLDER = ( AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "tests" / "airflow_e2e_tests" / "java_sdk_tests" / "dags" ) - # Java SDK bundle names (used in DAG bundle config and docker-compose env vars) JAVA_STUB_BUNDLE_NAME = "python-stub-java-tasks" JAVA_PURE_BUNDLE_NAME = "pure-java" diff --git a/scripts/docker/install_from_docker_context_files.sh b/scripts/docker/install_from_docker_context_files.sh index 5ce45267f8fbd..29753b440eb4f 100644 --- a/scripts/docker/install_from_docker_context_files.sh +++ b/scripts/docker/install_from_docker_context_files.sh @@ -85,8 +85,8 @@ function install_airflow_and_providers_from_docker_context_files(){ install_airflow_core_distribution=("apache-airflow-core==${AIRFLOW_VERSION}") fi - # Find Provider/TaskSDK/CTL distributions in docker-context files - readarray -t airflow_distributions< <(python /scripts/docker/get_distribution_specs.py /docker-context-files/apache?airflow?{providers,task?sdk,airflowctl}*.{whl,tar.gz} 2>/dev/null || true) + # Find Provider/TaskSDK/CTL/Coordinator distributions in docker-context files + readarray -t airflow_distributions< <(python /scripts/docker/get_distribution_specs.py /docker-context-files/apache?airflow?{providers,task?sdk,airflowctl,coordinators?java}*.{whl,tar.gz} 2>/dev/null || true) echo echo "${COLOR_BLUE}Found provider distributions in docker-context-files folder: ${airflow_distributions[*]}${COLOR_RESET}" echo diff --git a/uv.lock b/uv.lock index 4d1a0fab00286..c35fa2b674112 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 @@ -97,6 +97,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 @@ -129,7 +130,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 @@ -153,6 +153,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] @@ -227,7 +228,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", @@ -252,6 +252,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", @@ -288,6 +289,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", @@ -1004,7 +1006,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" }, @@ -1029,6 +1030,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" }, @@ -1255,9 +1257,6 @@ kerberos = [ keycloak = [ { name = "apache-airflow-providers-keycloak" }, ] -languages-java = [ - { name = "apache-airflow-providers-languages-java" }, -] ldap = [ { name = "python-ldap" }, ] @@ -1351,6 +1350,9 @@ salesforce = [ samba = [ { name = "apache-airflow-providers-samba" }, ] +sdk-java = [ + { name = "apache-airflow-providers-sdk-java" }, +] segment = [ { name = "apache-airflow-providers-segment" }, ] @@ -1447,6 +1449,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" }, ] @@ -1594,8 +1597,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" }, @@ -1644,6 +1645,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" }, @@ -1694,7 +1697,7 @@ requires-dist = [ { name = "sentry-sdk", marker = "extra == 'sentry'", specifier = ">=2.30.0" }, { name = "uv", marker = "extra == 'uv'", specifier = ">=0.11.8" }, ] -provides-extras = ["all-core", "async", "graphviz", "gunicorn", "kerberos", "memray", "otel", "statsd", "all-task-sdk", "airbyte", "akeyless", "alibaba", "amazon", "apache-cassandra", "apache-drill", "apache-druid", "apache-flink", "apache-hdfs", "apache-hive", "apache-iceberg", "apache-impala", "apache-kafka", "apache-kylin", "apache-livy", "apache-pig", "apache-pinot", "apache-spark", "apache-tinkerpop", "apprise", "arangodb", "asana", "atlassian-jira", "celery", "cloudant", "cncf-kubernetes", "cohere", "common-ai", "common-compat", "common-io", "common-messaging", "common-sql", "databricks", "datadog", "dbt-cloud", "dingding", "discord", "docker", "edge3", "elasticsearch", "exasol", "fab", "facebook", "ftp", "git", "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", "all", "aiobotocore", "apache-atlas", "apache-webhdfs", "amazon-aws-auth", "cloudpickle", "github-enterprise", "google-auth", "ldap", "pandas", "polars", "rabbitmq", "sentry", "s3fs", "uv"] +provides-extras = ["all-core", "async", "graphviz", "gunicorn", "kerberos", "memray", "otel", "statsd", "all-task-sdk", "airbyte", "akeyless", "alibaba", "amazon", "apache-cassandra", "apache-drill", "apache-druid", "apache-flink", "apache-hdfs", "apache-hive", "apache-iceberg", "apache-impala", "apache-kafka", "apache-kylin", "apache-livy", "apache-pig", "apache-pinot", "apache-spark", "apache-tinkerpop", "apprise", "arangodb", "asana", "atlassian-jira", "celery", "cloudant", "cncf-kubernetes", "cohere", "common-ai", "common-compat", "common-io", "common-messaging", "common-sql", "databricks", "datadog", "dbt-cloud", "dingding", "discord", "docker", "edge3", "elasticsearch", "exasol", "fab", "facebook", "ftp", "git", "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", "all", "aiobotocore", "apache-atlas", "apache-webhdfs", "amazon-aws-auth", "cloudpickle", "github-enterprise", "google-auth", "ldap", "pandas", "polars", "rabbitmq", "sentry", "s3fs", "uv"] [package.metadata.requires-dev] dev = [ @@ -1722,6 +1725,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" }, ] @@ -2777,7 +2781,7 @@ packages = [] [[package]] name = "apache-airflow-providers-airbyte" -version = "5.4.1" +version = "5.4.2" source = { editable = "providers/airbyte" } dependencies = [ { name = "airbyte-api" }, @@ -2904,7 +2908,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-amazon" -version = "9.27.0" +version = "9.28.0" source = { editable = "providers/amazon" } dependencies = [ { name = "apache-airflow" }, @@ -4067,7 +4071,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-cncf-kubernetes" -version = "10.16.1" +version = "10.17.0" source = { editable = "providers/cncf/kubernetes" } dependencies = [ { name = "aiofiles" }, @@ -4155,7 +4159,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-common-ai" -version = "0.1.1" +version = "0.2.0" source = { editable = "providers/common/ai" } dependencies = [ { name = "apache-airflow" }, @@ -4383,7 +4387,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-common-sql" -version = "1.35.0" +version = "1.36.0" source = { editable = "providers/common/sql" } dependencies = [ { name = "apache-airflow" }, @@ -4484,7 +4488,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-databricks" -version = "7.13.0" +version = "7.14.0" source = { editable = "providers/databricks" } dependencies = [ { name = "aiohttp" }, @@ -4626,7 +4630,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-dbt-cloud" -version = "4.8.1" +version = "4.8.2" source = { editable = "providers/dbt/cloud" } dependencies = [ { name = "aiohttp" }, @@ -4720,7 +4724,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-discord" -version = "3.12.2" +version = "3.12.3" source = { editable = "providers/discord" } dependencies = [ { name = "apache-airflow" }, @@ -4798,7 +4802,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-edge3" -version = "3.5.0" +version = "3.6.0" source = { editable = "providers/edge3" } dependencies = [ { name = "aiofiles" }, @@ -4937,7 +4941,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-fab" -version = "3.6.2" +version = "3.6.3" source = { editable = "providers/fab" } dependencies = [ { name = "apache-airflow" }, @@ -5101,7 +5105,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-git" -version = "0.3.1" +version = "0.3.2" source = { editable = "providers/git" } dependencies = [ { name = "apache-airflow" }, @@ -5175,7 +5179,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-google" -version = "21.2.0" +version = "21.3.0" source = { editable = "providers/google" } dependencies = [ { name = "apache-airflow" }, @@ -5855,38 +5859,9 @@ 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.1.2" +version = "13.2.0" source = { editable = "providers/microsoft/azure" } dependencies = [ { name = "adlfs" }, @@ -5962,7 +5937,7 @@ requires-dist = [ { name = "apache-airflow-providers-common-messaging", marker = "extra == 'common-messaging'", editable = "providers/common/messaging" }, { name = "apache-airflow-providers-oracle", marker = "extra == 'oracle'", editable = "providers/oracle" }, { name = "apache-airflow-providers-sftp", marker = "extra == 'sftp'", editable = "providers/sftp" }, - { name = "azure-batch", specifier = ">=8.0.0" }, + { name = "azure-batch", specifier = ">=8.0.0,<15.0.0" }, { name = "azure-cosmos", specifier = ">=4.6.0" }, { name = "azure-datalake-store", specifier = ">=0.0.45" }, { name = "azure-identity", specifier = ">=1.3.1" }, @@ -6174,7 +6149,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-mysql" -version = "6.5.2" +version = "6.5.3" source = { editable = "providers/mysql" } dependencies = [ { name = "aiomysql" }, @@ -6404,7 +6379,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-openlineage" -version = "2.15.0" +version = "2.16.0" source = { editable = "providers/openlineage" } dependencies = [ { name = "apache-airflow" }, @@ -7098,6 +7073,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" @@ -7502,7 +7517,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-standard" -version = "1.12.3" +version = "1.13.0" source = { editable = "providers/standard" } dependencies = [ { name = "apache-airflow" }, @@ -7884,7 +7899,7 @@ docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "d [[package]] name = "apache-airflow-providers-yandex" -version = "4.4.2" +version = "4.5.0" source = { editable = "providers/yandex" } dependencies = [ { name = "apache-airflow" }, @@ -8497,6 +8512,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" }

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